1use std::error::Error;
10use std::fmt;
11use std::fs;
12use std::future::Future;
13use std::io;
14use std::path::{Path, PathBuf};
15use std::pin::Pin;
16use std::sync::{Arc, Mutex};
17use std::task::{Context, Poll};
18use std::time::{Duration, Instant, SystemTime};
19
20use bytes::Bytes;
21use http_body::{Body as HttpBody, Frame};
22use http_body_util::BodyExt;
23use tonic::body::Body;
24use tonic::codegen::{BoxFuture, Service, http};
25use tonic::metadata::MetadataValue;
26use tonic::service::Interceptor;
27use tonic::service::interceptor::{InterceptedService, ResponseBody};
28use tonic::transport::Channel;
29use tonic::{Request, Status};
30
31type RejectedTokenRefreshFuture<'a> =
32 Pin<Box<dyn Future<Output = Result<String, CredentialError>> + Send + 'a>>;
33
34#[cfg(feature = "api-key-credential")]
35#[path = "credential/api_key_credential.rs"]
36mod api_key_credential;
37#[cfg(feature = "api-key-credential")]
38pub use api_key_credential::{ApiKeyCredential, ApiKeyCredentialBuilder, ApiKeyCredentialError};
39
40pub trait Credential: Send + Sync {
69 fn token(&self) -> Result<String, CredentialError>;
75
76 fn discard_rejected_token(&self, _rejected_token: &str) {}
83
84 fn token_after_rejection(&self) -> Option<RejectedTokenRefreshFuture<'_>> {
92 None
93 }
94}
95
96impl fmt::Debug for dyn Credential {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 formatter.write_str("Credential")
99 }
100}
101
102#[derive(Debug, Clone)]
113pub struct CredentialError {
114 message: String,
115}
116
117impl CredentialError {
118 pub fn new(message: impl Into<String>) -> Self {
123 Self {
124 message: message.into(),
125 }
126 }
127}
128
129impl fmt::Display for CredentialError {
130 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131 formatter.write_str(&self.message)
132 }
133}
134
135impl Error for CredentialError {}
136
137pub struct FileCredential {
156 path: PathBuf,
157 state: Mutex<FileCredentialState>,
158}
159
160struct FileCredentialState {
161 token: Result<String, CredentialError>,
162 modified: SystemTime,
163 size: u64,
164 checked_at: Instant,
165}
166
167impl fmt::Debug for FileCredential {
168 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169 formatter
170 .debug_struct("FileCredential")
171 .field("path", &self.path)
172 .field("token", &"<redacted>")
173 .finish()
174 }
175}
176
177impl FileCredential {
178 pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
185 let path = path.as_ref().to_path_buf();
186 let metadata = fs::metadata(&path)?;
187 let token = read_token(&path)?;
188 Ok(Self {
189 path,
190 state: Mutex::new(FileCredentialState {
191 token: Ok(token),
192 modified: metadata.modified()?,
193 size: metadata.len(),
194 checked_at: Instant::now(),
195 }),
196 })
197 }
198}
199
200impl Credential for FileCredential {
201 fn token(&self) -> Result<String, CredentialError> {
202 let mut state = self.state.lock().expect("credential file lock poisoned");
203 if state.checked_at.elapsed() >= Duration::from_secs(1) {
204 state.checked_at = Instant::now();
205 state.token = match fs::metadata(&self.path) {
206 Ok(metadata) => match metadata.modified() {
207 Ok(modified)
208 if state.token.is_ok()
209 && modified == state.modified
210 && metadata.len() == state.size =>
211 {
212 state.token.clone()
213 }
214 Ok(modified) => {
215 let token = read_token(&self.path).map_err(credential_file_error);
216 if token.is_ok() {
217 state.modified = modified;
218 state.size = metadata.len();
219 }
220 token
221 }
222 Err(error) => Err(credential_file_error(error)),
223 },
224 Err(error) => Err(credential_file_error(error)),
225 };
226 }
227 state.token.clone()
228 }
229}
230
231fn read_token(path: &Path) -> io::Result<String> {
232 let token = fs::read_to_string(path)?.trim().to_string();
233 if token.is_empty() {
234 return Err(io::Error::new(
235 io::ErrorKind::InvalidData,
236 format!("credential file {} is empty", path.display()),
237 ));
238 }
239 Ok(token)
240}
241
242fn credential_file_error(error: io::Error) -> CredentialError {
243 CredentialError::new(format!("read credential file: {error}"))
244}
245
246#[derive(Clone)]
247pub(crate) struct CredentialChannel {
248 service: InterceptedService<RetryChannel, CredentialInterceptor>,
249 credential: Option<Arc<dyn Credential>>,
250}
251
252impl CredentialChannel {
253 pub(crate) fn new(channel: Channel, interceptor: CredentialInterceptor) -> Self {
254 let credential = interceptor.credential.clone();
255 let retry_channel = RetryChannel {
256 channel,
257 credential: credential.clone(),
258 };
259 Self {
260 service: InterceptedService::new(retry_channel, interceptor),
261 credential,
262 }
263 }
264
265 pub(crate) fn credential(&self) -> Option<Arc<dyn Credential>> {
266 self.credential.clone()
267 }
268}
269
270impl Service<http::Request<Body>> for CredentialChannel {
271 type Response = http::Response<ResponseBody<Body>>;
272 type Error = tonic::transport::Error;
273 type Future = BoxFuture<Self::Response, Self::Error>;
274
275 fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
276 self.service.poll_ready(context)
277 }
278
279 fn call(&mut self, request: http::Request<Body>) -> Self::Future {
280 Box::pin(self.service.call(request))
281 }
282}
283
284#[derive(Clone)]
285struct RetryChannel {
286 channel: Channel,
287 credential: Option<Arc<dyn Credential>>,
288}
289
290impl Service<http::Request<Body>> for RetryChannel {
291 type Response = http::Response<Body>;
292 type Error = tonic::transport::Error;
293 type Future = BoxFuture<Self::Response, Self::Error>;
294
295 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
296 Poll::Ready(Ok(()))
297 }
298
299 fn call(&mut self, request: http::Request<Body>) -> Self::Future {
300 let mut channel = self.channel.clone();
301 let credential = self.credential.clone();
302 let deadline = request_deadline(&request);
303 Box::pin(async move {
304 let attempt = async {
305 let mut request = request;
306 let mut rejected_token = match request.headers().get("authorization") {
307 Some(value) => match value.to_str() {
308 Ok(value) => value.strip_prefix("Bearer ").map(str::to_owned),
309 Err(_) => None,
310 },
311 None => None,
312 };
313 if rejected_token.is_none()
314 && let Some(credential) = &credential
315 && let Some(fresh_token) = credential.token_after_rejection()
316 {
317 let fresh_token = match fresh_token.await {
318 Ok(token) => token,
319 Err(error) => {
320 return Ok(Status::unauthenticated(error.to_string()).into_http());
321 }
322 };
323 let authorization = match bearer_header(&fresh_token) {
324 Ok(value) => value,
325 Err(status) => return Ok(status.into_http()),
326 };
327 request.headers_mut().insert("authorization", authorization);
328 rejected_token = Some(fresh_token);
329 }
330 let is_poll_stream =
331 request.uri().path() == "/dex.WorkerTaskService/PollWorkerTask";
332 if is_poll_stream || credential.is_none() {
333 let response = send_channel_request(&mut channel, request).await?;
334 let Some(credential) = credential else {
335 return Ok(response);
336 };
337 return Ok(observe_stream_rejection(
338 response,
339 credential,
340 rejected_token,
341 ));
342 }
343 let (parts, body) = request.into_parts();
344 let request_body = match body.collect().await {
345 Ok(body) => body,
346 Err(status) => return Ok(status.into_http()),
347 };
348 let request_trailers = request_body.trailers().cloned();
349 let request_bytes = request_body.to_bytes();
350 let retry_method = parts.method.clone();
351 let retry_uri = parts.uri.clone();
352 let retry_version = parts.version;
353 let retry_headers = parts.headers.clone();
354 let request = http::Request::from_parts(
355 parts,
356 buffered_body(request_bytes.clone(), request_trailers.clone()),
357 );
358 let response = send_channel_request(&mut channel, request).await?;
359 let (response_parts, response_body) = response.into_parts();
360 let response_body = match response_body.collect().await {
361 Ok(body) => body,
362 Err(status) => return Ok(status.into_http()),
363 };
364 let response_trailers = response_body.trailers().cloned();
365 let is_rejected = is_unauthenticated(&response_parts.headers)
366 || response_trailers.as_ref().is_some_and(is_unauthenticated);
367 if !is_rejected {
368 return Ok(http::Response::from_parts(
369 response_parts,
370 buffered_body(response_body.to_bytes(), response_trailers),
371 ));
372 }
373 let Some(credential) = credential else {
374 return Ok(http::Response::from_parts(
375 response_parts,
376 buffered_body(response_body.to_bytes(), response_trailers),
377 ));
378 };
379 let Some(rejected_token) = rejected_token else {
380 return Ok(http::Response::from_parts(
381 response_parts,
382 buffered_body(response_body.to_bytes(), response_trailers),
383 ));
384 };
385 credential.discard_rejected_token(&rejected_token);
386 let Some(fresh_token) = credential.token_after_rejection() else {
387 return Ok(http::Response::from_parts(
388 response_parts,
389 buffered_body(response_body.to_bytes(), response_trailers),
390 ));
391 };
392 let fresh_token = match fresh_token.await {
393 Ok(token) => token,
394 Err(error) => return Ok(Status::unauthenticated(error.to_string()).into_http()),
395 };
396 let authorization = match bearer_header(&fresh_token) {
397 Ok(value) => value,
398 Err(status) => return Ok(status.into_http()),
399 };
400 let mut retry = http::Request::new(buffered_body(request_bytes, request_trailers));
401 *retry.method_mut() = retry_method;
402 *retry.uri_mut() = retry_uri;
403 *retry.version_mut() = retry_version;
404 *retry.headers_mut() = retry_headers;
405 retry.headers_mut().insert("authorization", authorization);
406 if let Some(deadline) = deadline
407 && !set_remaining_timeout(&mut retry, deadline)
408 {
409 return Ok(Status::deadline_exceeded("request deadline exceeded").into_http());
410 }
411 let response = send_channel_request(&mut channel, retry).await?;
412 let (parts, body) = response.into_parts();
413 let response = observe_stream_rejection(
414 http::Response::from_parts(parts, body),
415 credential,
416 Some(fresh_token),
417 );
418 Ok(response)
419 };
420 if let Some(deadline) = deadline {
421 match tokio::time::timeout_at(deadline.into(), attempt).await {
422 Ok(result) => result,
423 Err(_) => {
424 Ok(Status::deadline_exceeded("request deadline exceeded").into_http())
425 }
426 }
427 } else {
428 attempt.await
429 }
430 })
431 }
432}
433
434fn bearer_header(token: &str) -> Result<http::HeaderValue, Status> {
435 http::HeaderValue::from_str(&format!("Bearer {token}"))
436 .map_err(|_| Status::unauthenticated("invalid bearer token metadata"))
437}
438
439fn request_deadline(request: &http::Request<Body>) -> Option<Instant> {
440 let timeout = request.headers().get("grpc-timeout")?.to_str().ok()?;
441 let (amount, unit) = timeout.split_at(timeout.len().checked_sub(1)?);
442 if amount.is_empty() || amount.len() > 8 {
443 return None;
444 }
445 let amount = amount.parse::<u64>().ok()?;
446 let duration = match unit {
447 "H" => Duration::from_secs(amount.checked_mul(3_600)?),
448 "M" => Duration::from_secs(amount.checked_mul(60)?),
449 "S" => Duration::from_secs(amount),
450 "m" => Duration::from_millis(amount),
451 "u" => Duration::from_micros(amount),
452 "n" => Duration::from_nanos(amount),
453 _ => return None,
454 };
455 Instant::now().checked_add(duration)
456}
457
458fn set_remaining_timeout(request: &mut http::Request<Body>, deadline: Instant) -> bool {
459 let remaining = deadline.saturating_duration_since(Instant::now());
460 if remaining.is_zero() {
461 return false;
462 }
463 let nanoseconds = remaining.as_nanos();
464 let timeout = [
465 (1, "n"),
466 (1_000, "u"),
467 (1_000_000, "m"),
468 (1_000_000_000, "S"),
469 (60_000_000_000, "M"),
470 (3_600_000_000_000, "H"),
471 ]
472 .into_iter()
473 .find_map(|(unit, suffix)| {
474 let amount = nanoseconds.div_ceil(unit);
475 (amount <= 99_999_999).then(|| format!("{amount}{suffix}"))
476 })
477 .expect("gRPC timeout fits an eight-digit hour value");
478 request.headers_mut().insert(
479 "grpc-timeout",
480 http::HeaderValue::from_str(&timeout).expect("numeric gRPC timeout header"),
481 );
482 true
483}
484
485async fn send_channel_request(
486 channel: &mut Channel,
487 request: http::Request<Body>,
488) -> Result<http::Response<Body>, tonic::transport::Error> {
489 std::future::poll_fn(|context| channel.poll_ready(context)).await?;
490 channel.call(request).await
491}
492
493fn is_unauthenticated(headers: &http::HeaderMap) -> bool {
494 headers
495 .get("grpc-status")
496 .is_some_and(|status| status == "16")
497}
498
499fn observe_stream_rejection(
500 response: http::Response<Body>,
501 credential: Arc<dyn Credential>,
502 rejected_token: Option<String>,
503) -> http::Response<Body> {
504 let (parts, body) = response.into_parts();
505 if is_unauthenticated(&parts.headers)
506 && let Some(token) = &rejected_token
507 {
508 credential.discard_rejected_token(token);
509 }
510 http::Response::from_parts(
511 parts,
512 Body::new(ObservedBody {
513 body,
514 credential,
515 rejected_token,
516 }),
517 )
518}
519
520struct ObservedBody {
521 body: Body,
522 credential: Arc<dyn Credential>,
523 rejected_token: Option<String>,
524}
525
526impl HttpBody for ObservedBody {
527 type Data = Bytes;
528 type Error = Status;
529
530 fn poll_frame(
531 mut self: Pin<&mut Self>,
532 context: &mut Context<'_>,
533 ) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
534 let frame = std::task::ready!(Pin::new(&mut self.body).poll_frame(context));
535 if let Some(Ok(frame)) = &frame
536 && frame.trailers_ref().is_some_and(is_unauthenticated)
537 && let Some(token) = &self.rejected_token
538 {
539 self.credential.discard_rejected_token(token);
540 }
541 Poll::Ready(frame)
542 }
543}
544
545fn buffered_body(data: Bytes, trailers: Option<http::HeaderMap>) -> Body {
546 Body::new(BufferedBody {
547 data: Some(data),
548 trailers,
549 })
550}
551
552struct BufferedBody {
553 data: Option<Bytes>,
554 trailers: Option<http::HeaderMap>,
555}
556
557impl HttpBody for BufferedBody {
558 type Data = Bytes;
559 type Error = Status;
560
561 fn poll_frame(
562 mut self: Pin<&mut Self>,
563 _context: &mut Context<'_>,
564 ) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
565 if let Some(data) = self.data.take() {
566 return Poll::Ready(Some(Ok(Frame::data(data))));
567 }
568 Poll::Ready(
569 self.trailers
570 .take()
571 .map(|trailers| Ok(Frame::trailers(trailers))),
572 )
573 }
574}
575
576#[derive(Clone)]
577pub(crate) struct CredentialInterceptor {
578 credential: Option<Arc<dyn Credential>>,
579}
580
581impl CredentialInterceptor {
582 pub(crate) fn new(credential: Option<Arc<dyn Credential>>) -> Self {
583 Self { credential }
584 }
585}
586
587impl Interceptor for CredentialInterceptor {
588 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
589 if let Some(credential) = &self.credential {
590 match credential.token() {
591 Ok(token) => {
592 let value = MetadataValue::try_from(format!("Bearer {token}"))
593 .map_err(|_| Status::unauthenticated("invalid bearer token metadata"))?;
594 request.metadata_mut().insert("authorization", value);
595 }
596 Err(_) if credential.token_after_rejection().is_some() => {}
597 Err(error) => return Err(Status::unauthenticated(error.to_string())),
598 }
599 }
600 Ok(request)
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use std::convert::Infallible;
607 use std::sync::atomic::{AtomicUsize, Ordering};
608 use std::task::{Context, Poll};
609
610 use dex_protocol::dex::worker_task_service_client::WorkerTaskServiceClient;
611 use dex_protocol::dex::worker_task_service_server::{
612 WorkerTaskService, WorkerTaskServiceServer,
613 };
614 use dex_protocol::dex::{
615 HealthInfo, PollWorkerTaskRequest, PollWorkerTaskResponse, ServerInfo,
616 };
617 use tokio::net::TcpListener;
618 use tokio::sync::{mpsc, oneshot};
619 use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream};
620 use tonic::codegen::{BoxFuture, Service, http};
621 use tonic::server::{NamedService, UnaryService};
622 use tonic::transport::{Endpoint, Server};
623 use tonic::{Response, Streaming};
624
625 use super::*;
626 use crate::{
627 BlobCache, BlobCacheConfig, Client, ClientOptions, PullWorkerOptions, Registry, Worker,
628 WorkerOptions,
629 };
630
631 #[derive(Clone)]
632 struct FakeFlowService {
633 headers: Arc<Mutex<Vec<Option<String>>>>,
634 }
635
636 impl NamedService for FakeFlowService {
637 const NAME: &'static str = "dex.FlowService";
638 }
639
640 impl<B> Service<http::Request<B>> for FakeFlowService
641 where
642 B: tonic::codegen::Body + Send + 'static,
643 B::Error: Into<tonic::codegen::StdError> + Send + 'static,
644 {
645 type Response = http::Response<tonic::body::Body>;
646 type Error = Infallible;
647 type Future = BoxFuture<Self::Response, Self::Error>;
648
649 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
650 Poll::Ready(Ok(()))
651 }
652
653 fn call(&mut self, request: http::Request<B>) -> Self::Future {
654 let is_server_info = request.uri().path() == "/dex.FlowService/GetServerInfo";
655 let header = request
656 .headers()
657 .get("authorization")
658 .map(|value| value.to_str().expect("ASCII bearer header").to_string());
659 self.headers.lock().expect("headers lock").push(header);
660 Box::pin(async move {
661 if is_server_info {
662 let mut grpc = tonic::server::Grpc::new(tonic_prost::ProstCodec::default());
663 Ok(grpc.unary(GetServerInfo, request).await)
664 } else {
665 let mut grpc = tonic::server::Grpc::new(tonic_prost::ProstCodec::default());
666 Ok(grpc.unary(HealthCheck, request).await)
667 }
668 })
669 }
670 }
671
672 struct HealthCheck;
673
674 impl UnaryService<()> for HealthCheck {
675 type Response = HealthInfo;
676 type Future = BoxFuture<Response<Self::Response>, Status>;
677
678 fn call(&mut self, _: Request<()>) -> Self::Future {
679 Box::pin(async { Ok(Response::new(HealthInfo::default())) })
680 }
681 }
682
683 struct GetServerInfo;
684
685 impl UnaryService<()> for GetServerInfo {
686 type Response = ServerInfo;
687 type Future = BoxFuture<Response<Self::Response>, Status>;
688
689 fn call(&mut self, _: Request<()>) -> Self::Future {
690 Box::pin(async { Ok(Response::new(ServerInfo::default())) })
691 }
692 }
693
694 #[derive(Clone)]
695 struct FakeTaskService {
696 headers: Arc<Mutex<Vec<Option<String>>>>,
697 }
698
699 #[tonic::async_trait]
700 impl WorkerTaskService for FakeTaskService {
701 type PollWorkerTaskStream = ReceiverStream<Result<PollWorkerTaskResponse, Status>>;
702
703 async fn poll_worker_task(
704 &self,
705 request: Request<Streaming<PollWorkerTaskRequest>>,
706 ) -> Result<Response<Self::PollWorkerTaskStream>, Status> {
707 let header = request
708 .metadata()
709 .get("authorization")
710 .map(|value| value.to_str().expect("ASCII bearer header").to_string());
711 self.headers.lock().expect("headers lock").push(header);
712 let (_sender, receiver) = mpsc::channel(1);
713 Ok(Response::new(ReceiverStream::new(receiver)))
714 }
715 }
716
717 struct MutableCredential {
718 token: Mutex<Result<String, CredentialError>>,
719 calls: AtomicUsize,
720 }
721
722 impl Credential for MutableCredential {
723 fn token(&self) -> Result<String, CredentialError> {
724 self.calls.fetch_add(1, Ordering::SeqCst);
725 self.token.lock().expect("token lock").clone()
726 }
727 }
728
729 #[test]
730 fn file_credential_rotates_and_fails_closed() {
731 let directory = tempfile::tempdir().expect("token directory");
732 let path = directory.path().join("token");
733 assert!(FileCredential::new(&path).is_err());
734 fs::write(&path, " \n").expect("write empty token");
735 assert!(FileCredential::new(&path).is_err());
736 fs::write(&path, " first\n").expect("write initial token");
737 let credential = FileCredential::new(&path).expect("initial token");
738 assert_eq!(credential.token().expect("initial token"), "first");
739 fs::write(&path, " second-token \n").expect("rotate token");
740 assert_eq!(credential.token().expect("cached token"), "first");
741 credential.state.lock().expect("file state lock").checked_at -= Duration::from_secs(1);
742 assert_eq!(credential.token().expect("rotated token"), "second-token");
743 fs::write(&path, " \n").expect("empty token");
744 credential.state.lock().expect("file state lock").checked_at -= Duration::from_secs(1);
745 assert!(credential.token().is_err());
746 fs::remove_file(&path).expect("remove token");
747 credential.state.lock().expect("file state lock").checked_at -= Duration::from_secs(1);
748 assert!(credential.token().is_err());
749 fs::write(&path, "restored").expect("restore token");
750 credential.state.lock().expect("file state lock").checked_at -= Duration::from_secs(1);
751 assert_eq!(credential.token().expect("restored token"), "restored");
752 }
753
754 #[test]
755 fn file_credential_debug_redacts_token() {
756 let directory = tempfile::tempdir().expect("token directory");
757 let path = directory.path().join("token");
758 fs::write(&path, "private-token-value").expect("write token");
759 let credential = Arc::new(FileCredential::new(&path).expect("file credential"));
760 let debug = format!("{credential:?}");
761 assert!(!debug.contains("private-token-value"));
762 assert!(debug.contains("<redacted>"));
763 }
764
765 #[test]
766 fn flow_service_unary_uses_current_credential_and_default_has_no_header() {
767 let runtime = tokio::runtime::Runtime::new().expect("server runtime");
768 let listener = runtime
769 .block_on(TcpListener::bind("127.0.0.1:0"))
770 .expect("bind fake FlowService");
771 let address = listener.local_addr().expect("fake server address");
772 let headers = Arc::new(Mutex::new(Vec::new()));
773 let (shutdown, stopped) = oneshot::channel::<()>();
774 runtime.spawn(
775 Server::builder()
776 .add_service(FakeFlowService {
777 headers: Arc::clone(&headers),
778 })
779 .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async {
780 stopped.await.expect("FlowService shutdown signal");
781 }),
782 );
783 let credential = Arc::new(MutableCredential {
784 token: Mutex::new(Ok("first".to_string())),
785 calls: AtomicUsize::new(0),
786 });
787 let directory = tempfile::tempdir().expect("blob cache directory");
788 let cache = Arc::new(
789 BlobCache::open(
790 BlobCacheConfig::new(directory.path(), 1024 * 1024, 0).expect("cache config"),
791 )
792 .expect("open cache"),
793 );
794 let without_credential = Client::try_new(
795 Registry::new(),
796 Arc::clone(&cache),
797 ClientOptions::new().server_address(address.to_string()),
798 )
799 .expect("default Client");
800 without_credential.health_check().expect("default call");
801 let with_credential = Client::try_new(
802 Registry::new(),
803 Arc::clone(&cache),
804 ClientOptions::new()
805 .server_address(address.to_string())
806 .credential(credential.clone()),
807 )
808 .expect("authenticated Client");
809 with_credential.health_check().expect("first call");
810 *credential.token.lock().expect("token lock") = Ok("second".to_string());
811 with_credential.health_check().expect("rotated call");
812 *credential.token.lock().expect("token lock") =
813 Err(CredentialError::new("token unavailable"));
814 let error = with_credential.health_check().expect_err("local error");
815 assert_eq!(
816 error.service_error().expect("service error").code(),
817 tonic::Code::Unauthenticated
818 );
819 *credential.token.lock().expect("token lock") = Ok("worker".to_string());
820 let worker = Worker::try_new(
821 Registry::new(),
822 cache,
823 WorkerOptions::new()
824 .server_address(address.to_string())
825 .credential(credential.clone())
826 .pull(PullWorkerOptions::new()),
827 )
828 .expect("authenticated Worker");
829 worker
830 .start()
831 .expect_err("fake protocol interval is invalid");
832 assert_eq!(credential.calls.load(Ordering::SeqCst), 4);
833 assert_eq!(
834 *headers.lock().expect("headers lock"),
835 vec![
836 None,
837 Some("Bearer first".to_string()),
838 Some("Bearer second".to_string()),
839 Some("Bearer worker".to_string())
840 ]
841 );
842 shutdown.send(()).expect("stop fake FlowService");
843 }
844
845 #[tokio::test(flavor = "multi_thread")]
846 async fn pull_stream_open_reevaluates_credential() {
847 let listener = TcpListener::bind("127.0.0.1:0")
848 .await
849 .expect("bind fake WorkerTaskService");
850 let address = listener.local_addr().expect("fake server address");
851 let headers = Arc::new(Mutex::new(Vec::new()));
852 let (shutdown, stopped) = oneshot::channel::<()>();
853 let server = tokio::spawn(
854 Server::builder()
855 .add_service(WorkerTaskServiceServer::new(FakeTaskService {
856 headers: Arc::clone(&headers),
857 }))
858 .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async {
859 stopped.await.expect("WorkerTaskService shutdown signal");
860 }),
861 );
862 let credential = Arc::new(MutableCredential {
863 token: Mutex::new(Ok("first".to_string())),
864 calls: AtomicUsize::new(0),
865 });
866 let channel = Endpoint::from_shared(format!("http://{address}"))
867 .expect("endpoint")
868 .connect()
869 .await
870 .expect("connect fake WorkerTaskService");
871 let mut default_client = WorkerTaskServiceClient::new(CredentialChannel::new(
872 channel.clone(),
873 CredentialInterceptor::new(None),
874 ));
875 default_client
876 .poll_worker_task(tokio_stream::empty::<PollWorkerTaskRequest>())
877 .await
878 .expect("open default task stream");
879 let mut client = WorkerTaskServiceClient::new(CredentialChannel::new(
880 channel,
881 CredentialInterceptor::new(Some(credential.clone())),
882 ));
883 for token in ["first", "second"] {
884 *credential.token.lock().expect("token lock") = Ok(token.to_string());
885 client
886 .poll_worker_task(tokio_stream::empty::<PollWorkerTaskRequest>())
887 .await
888 .expect("open task stream");
889 }
890 *credential.token.lock().expect("token lock") =
891 Err(CredentialError::new("token unavailable"));
892 let error = client
893 .poll_worker_task(tokio_stream::empty::<PollWorkerTaskRequest>())
894 .await
895 .expect_err("local task stream error");
896 assert_eq!(error.code(), tonic::Code::Unauthenticated);
897 assert_eq!(credential.calls.load(Ordering::SeqCst), 3);
898 assert_eq!(
899 *headers.lock().expect("headers lock"),
900 vec![
901 None,
902 Some("Bearer first".to_string()),
903 Some("Bearer second".to_string())
904 ]
905 );
906 shutdown.send(()).expect("stop fake WorkerTaskService");
907 server
908 .await
909 .expect("join fake WorkerTaskService")
910 .expect("serve task service");
911 }
912}