1use crate::token_source::auth_token_generator::GoogleAuthTokenGenerator;
2use futures::{Future, TryFutureExt};
3use hyper::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
4use jiff::Timestamp;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::{Context, Poll};
8use tonic::client::GrpcService;
9use tower::Service;
10use tower_layer::Layer;
11use tracing::*;
12
13const X_GOOG_API_CLIENT: HeaderName = HeaderName::from_static("x-goog-api-client");
14const GOOGLE_CLOUD_RESOURCE_PREFIX: HeaderName =
15 HeaderName::from_static("google-cloud-resource-prefix");
16
17fn default_headers(cloud_resource_prefix: Option<String>) -> crate::error::Result<HeaderMap> {
18 let mut headers = HeaderMap::new();
19 let default_agent =
20 HeaderValue::from_static(concat!("gcloud-sdk-rs/", env!("CARGO_PKG_VERSION")));
21 headers.insert(USER_AGENT, default_agent.clone());
22 headers.insert(X_GOOG_API_CLIENT, default_agent);
23 if let Some(prefix) = cloud_resource_prefix {
24 headers.insert(
25 GOOGLE_CLOUD_RESOURCE_PREFIX,
26 HeaderValue::from_str(&prefix)?,
27 );
28 }
29 Ok(headers)
30}
31
32fn append_header_value(
35 headers: &HeaderMap,
36 name: &HeaderName,
37 extra: &str,
38) -> crate::error::Result<HeaderValue> {
39 let combined = match headers.get(name).and_then(|v| v.to_str().ok()) {
40 Some(current) => format!("{current} {extra}"),
41 None => extra.to_string(),
42 };
43 Ok(HeaderValue::from_str(&combined)?)
44}
45
46#[derive(Clone)]
47pub struct GoogleAuthMiddlewareService<T> {
48 inner: T,
49 token_generator: Arc<GoogleAuthTokenGenerator>,
50 headers: Arc<HeaderMap>,
52}
53
54impl<T> GoogleAuthMiddlewareService<T> {
55 pub fn new(
56 service: T,
57 token_generator: Arc<GoogleAuthTokenGenerator>,
58 cloud_resource_prefix: Option<String>,
59 ) -> crate::error::Result<GoogleAuthMiddlewareService<T>> {
60 Ok(GoogleAuthMiddlewareService {
61 inner: service,
62 token_generator,
63 headers: Arc::new(default_headers(cloud_resource_prefix)?),
64 })
65 }
66
67 pub fn set_user_agent(&mut self, user_agent: String) -> crate::error::Result<()> {
68 let value = HeaderValue::from_str(&user_agent)?;
69 Arc::make_mut(&mut self.headers).insert(USER_AGENT, value);
70 Ok(())
71 }
72
73 pub fn set_x_goog_api_client(&mut self, x_goog_api_client: String) -> crate::error::Result<()> {
74 let value = HeaderValue::from_str(&x_goog_api_client)?;
75 Arc::make_mut(&mut self.headers).insert(X_GOOG_API_CLIENT, value);
76 Ok(())
77 }
78
79 pub fn set_cloud_resource_prefix(
80 &mut self,
81 cloud_resource_prefix: String,
82 ) -> crate::error::Result<()> {
83 let value = HeaderValue::from_str(&cloud_resource_prefix)?;
84 Arc::make_mut(&mut self.headers).insert(GOOGLE_CLOUD_RESOURCE_PREFIX, value);
85 Ok(())
86 }
87
88 pub fn append_user_agent(&mut self, user_agent: String) -> crate::error::Result<()> {
89 let value = append_header_value(&self.headers, &USER_AGENT, &user_agent)?;
90 Arc::make_mut(&mut self.headers).insert(USER_AGENT, value);
91 Ok(())
92 }
93
94 pub fn append_x_goog_api_client(
95 &mut self,
96 x_goog_api_client: String,
97 ) -> crate::error::Result<()> {
98 let value = append_header_value(&self.headers, &X_GOOG_API_CLIENT, &x_goog_api_client)?;
99 Arc::make_mut(&mut self.headers).insert(X_GOOG_API_CLIENT, value);
100 Ok(())
101 }
102
103 pub fn set_additional_headers(&mut self, additional_headers: HeaderMap) {
104 Arc::make_mut(&mut self.headers).extend(additional_headers);
105 }
106
107 pub(crate) fn with_inner<U>(
112 &self,
113 inner: U,
114 cloud_resource_prefix: Option<String>,
115 ) -> crate::error::Result<GoogleAuthMiddlewareService<U>> {
116 let mut headers = HeaderMap::clone(&self.headers);
117 headers.remove(GOOGLE_CLOUD_RESOURCE_PREFIX);
118 if let Some(prefix) = cloud_resource_prefix {
119 headers.insert(
120 GOOGLE_CLOUD_RESOURCE_PREFIX,
121 HeaderValue::from_str(&prefix)?,
122 );
123 }
124 Ok(GoogleAuthMiddlewareService {
125 inner,
126 token_generator: Arc::clone(&self.token_generator),
127 headers: Arc::new(headers),
128 })
129 }
130}
131
132impl<T, RequestBody> Service<hyper::Request<RequestBody>> for GoogleAuthMiddlewareService<T>
133where
134 T: GrpcService<RequestBody> + Send + Clone + 'static,
135 T::Future: 'static + Send,
136 RequestBody: 'static + Send,
137 T::ResponseBody: 'static + Send,
138 T::Error: 'static + Send,
139{
140 type Response = hyper::Response<T::ResponseBody>;
141 type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
142 type Future =
143 Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
144
145 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
146 self.inner.poll_ready(cx).map_err(Into::into)
147 }
148
149 fn call(&mut self, mut req: hyper::Request<RequestBody>) -> Self::Future {
150 let generator = Arc::clone(&self.token_generator);
151 let headers = Arc::clone(&self.headers);
152
153 let clone = self.inner.clone();
156 let mut inner = std::mem::replace(&mut self.inner, clone);
157
158 Box::pin(async move {
159 let begin_time = Timestamp::now();
160 let authorization = generator.authorization_header().await.map_err(Box::new)?;
161 let token_generated_time = Timestamp::now();
162
163 let req_headers = req.headers_mut();
164 req_headers.insert(hyper::header::AUTHORIZATION, authorization);
165 for name in headers.keys() {
170 req_headers.remove(name);
171 }
172 req_headers.extend(
173 headers
174 .iter()
175 .map(|(name, value)| (name.clone(), value.clone())),
176 );
177
178 let req_uri = req.uri().clone();
179 inner
180 .call(req)
181 .map_ok(|x| {
182 let finished_time = Timestamp::now();
183 debug!(
184 %req_uri,
185 "OK: took {}ms (incl. token gen: {}ms)",
186 finished_time.duration_since(begin_time).as_millis(),
187 token_generated_time.duration_since(begin_time).as_millis()
188 );
189 x
190 })
191 .await
192 .map_err(|e| {
193 let finished_time = Timestamp::now();
194 error!(
195 %req_uri,
196 "Err: took {}ms (incl. token gen: {}ms)",
197 finished_time.duration_since(begin_time).as_millis(),
198 token_generated_time.duration_since(begin_time).as_millis()
199 );
200 e.into()
201 })
202 })
203 }
204}
205
206pub struct GoogleAuthMiddlewareLayer {
207 token_generator: Arc<GoogleAuthTokenGenerator>,
208 headers: Arc<HeaderMap>,
209}
210
211impl GoogleAuthMiddlewareLayer {
212 pub fn new(
213 token_generator: GoogleAuthTokenGenerator,
214 cloud_resource_prefix: Option<String>,
215 ) -> crate::error::Result<Self> {
216 Ok(GoogleAuthMiddlewareLayer {
217 token_generator: Arc::new(token_generator),
218 headers: Arc::new(default_headers(cloud_resource_prefix)?),
219 })
220 }
221
222 pub fn amend_user_agent(mut self, user_agent: String) -> crate::error::Result<Self> {
223 let value = append_header_value(&self.headers, &USER_AGENT, &user_agent)?;
224 Arc::make_mut(&mut self.headers).insert(USER_AGENT, value);
225 Ok(self)
226 }
227
228 pub fn amend_x_goog_api_client(
229 mut self,
230 x_goog_api_client: String,
231 ) -> crate::error::Result<Self> {
232 let value = append_header_value(&self.headers, &X_GOOG_API_CLIENT, &x_goog_api_client)?;
233 Arc::make_mut(&mut self.headers).insert(X_GOOG_API_CLIENT, value);
234 Ok(self)
235 }
236
237 pub fn set_additional_headers(&mut self, additional_headers: HeaderMap) {
238 Arc::make_mut(&mut self.headers).extend(additional_headers);
239 }
240}
241
242impl<S> Layer<S> for GoogleAuthMiddlewareLayer {
243 type Service = GoogleAuthMiddlewareService<S>;
244
245 fn layer(&self, service: S) -> GoogleAuthMiddlewareService<S> {
246 GoogleAuthMiddlewareService {
247 inner: service,
248 token_generator: Arc::clone(&self.token_generator),
249 headers: Arc::clone(&self.headers),
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::token_source::{Source, Token, TokenSourceType};
258 use async_trait::async_trait;
259 use hyper::{Request, Response};
260 use jiff::{SignedDuration, Timestamp};
261 use secret_vault_value::SecretValue;
262 use std::convert::Infallible;
263
264 struct DummySource;
265
266 #[async_trait]
267 impl Source for DummySource {
268 async fn token(&self) -> crate::error::Result<Token> {
269 Ok(Token {
270 token_type: "Bearer".to_string(),
271 token: SecretValue::from("dummy-token"),
272 expiry: Timestamp::now() + SignedDuration::from_hours(1),
273 })
274 }
275 }
276
277 struct CountingSource {
278 calls: Arc<std::sync::atomic::AtomicUsize>,
279 }
280
281 #[async_trait]
282 impl Source for CountingSource {
283 async fn token(&self) -> crate::error::Result<Token> {
284 self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
285 Ok(Token {
286 token_type: "Bearer".to_string(),
287 token: SecretValue::from("counted-token"),
288 expiry: Timestamp::now() + SignedDuration::from_hours(1),
289 })
290 }
291 }
292
293 #[derive(Clone)]
294 struct DummyService {
295 tx: Arc<tokio::sync::mpsc::Sender<Request<String>>>,
296 }
297
298 impl Service<Request<String>> for DummyService {
299 type Response = Response<String>;
300 type Error = Infallible;
301 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
302
303 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
304 Poll::Ready(Ok(()))
305 }
306
307 fn call(&mut self, req: Request<String>) -> Self::Future {
308 let tx = self.tx.clone();
309 Box::pin(async move {
310 tx.send(req).await.unwrap();
311 Ok(Response::builder()
312 .status(200)
313 .body("".to_string())
314 .unwrap())
315 })
316 }
317 }
318
319 #[tokio::test]
320 async fn test_headers_presence() {
321 let token_generator = GoogleAuthTokenGenerator::new(
322 TokenSourceType::ExternalSource(Box::new(DummySource)),
323 vec![],
324 )
325 .await
326 .unwrap();
327
328 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
329 let dummy_service = DummyService { tx: Arc::new(tx) };
330 let mut service =
331 GoogleAuthMiddlewareService::new(dummy_service, Arc::new(token_generator), None)
332 .unwrap();
333
334 let req = Request::builder()
335 .uri("http://example.com")
336 .body("".to_string())
337 .unwrap();
338
339 tower::Service::call(&mut service, req).await.unwrap();
340
341 let captured_req = rx.recv().await.unwrap();
342 let expected_default = format!("gcloud-sdk-rs/{}", env!("CARGO_PKG_VERSION"));
343 assert_eq!(
344 captured_req
345 .headers()
346 .get(hyper::header::USER_AGENT)
347 .unwrap(),
348 expected_default.as_str()
349 );
350 assert_eq!(
351 captured_req.headers().get("x-goog-api-client").unwrap(),
352 expected_default.as_str()
353 );
354 assert_eq!(
355 captured_req.headers().get("authorization").unwrap(),
356 "Bearer dummy-token"
357 );
358 }
359
360 #[tokio::test]
361 async fn authorization_header_is_marked_sensitive() {
362 let token_generator = GoogleAuthTokenGenerator::new(
363 TokenSourceType::ExternalSource(Box::new(DummySource)),
364 vec![],
365 )
366 .await
367 .unwrap();
368
369 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
370 let dummy_service = DummyService { tx: Arc::new(tx) };
371 let mut service =
372 GoogleAuthMiddlewareService::new(dummy_service, Arc::new(token_generator), None)
373 .unwrap();
374
375 let req = Request::builder()
376 .uri("http://example.com")
377 .body("".to_string())
378 .unwrap();
379
380 tower::Service::call(&mut service, req).await.unwrap();
381
382 let captured_req = rx.recv().await.unwrap();
383 assert!(captured_req
384 .headers()
385 .get("authorization")
386 .unwrap()
387 .is_sensitive());
388 }
389
390 #[tokio::test]
391 async fn test_headers_amend() {
392 let token_generator = GoogleAuthTokenGenerator::new(
393 TokenSourceType::ExternalSource(Box::new(DummySource)),
394 vec![],
395 )
396 .await
397 .unwrap();
398
399 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
400 let dummy_service = DummyService { tx: Arc::new(tx) };
401
402 let layer = GoogleAuthMiddlewareLayer::new(token_generator, None)
403 .unwrap()
404 .amend_user_agent("extra-ua".to_string())
405 .unwrap()
406 .amend_x_goog_api_client("extra-client".to_string())
407 .unwrap();
408
409 let mut service = layer.layer(dummy_service);
410
411 let req = Request::builder()
412 .uri("http://example.com")
413 .body("".to_string())
414 .unwrap();
415
416 tower::Service::call(&mut service, req).await.unwrap();
417
418 let captured_req = rx.recv().await.unwrap();
419 let expected_ua = format!("gcloud-sdk-rs/{} extra-ua", env!("CARGO_PKG_VERSION"));
420 let expected_client = format!("gcloud-sdk-rs/{} extra-client", env!("CARGO_PKG_VERSION"));
421
422 assert_eq!(
423 captured_req
424 .headers()
425 .get(hyper::header::USER_AGENT)
426 .unwrap(),
427 expected_ua.as_str()
428 );
429 assert_eq!(
430 captured_req.headers().get("x-goog-api-client").unwrap(),
431 expected_client.as_str()
432 );
433 }
434
435 #[tokio::test]
436 async fn invalid_user_agent_is_rejected_at_setter() {
437 let token_generator = GoogleAuthTokenGenerator::new(
438 TokenSourceType::ExternalSource(Box::new(DummySource)),
439 vec![],
440 )
441 .await
442 .unwrap();
443
444 let layer_result = GoogleAuthMiddlewareLayer::new(token_generator, None)
445 .unwrap()
446 .amend_user_agent("bad\nvalue".to_string());
447
448 match layer_result {
449 Err(e) => assert!(matches!(
450 e.into_kind(),
451 crate::error::ErrorKind::HeaderValue(_)
452 )),
453 Ok(_) => panic!("expected an invalid header value to be rejected"),
454 }
455 }
456
457 #[tokio::test]
458 async fn amended_clone_does_not_change_sibling() {
459 let token_generator = GoogleAuthTokenGenerator::new(
460 TokenSourceType::ExternalSource(Box::new(DummySource)),
461 vec![],
462 )
463 .await
464 .unwrap();
465
466 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
467 let dummy_service = DummyService { tx: Arc::new(tx) };
468 let base_service =
469 GoogleAuthMiddlewareService::new(dummy_service, Arc::new(token_generator), None)
470 .unwrap();
471
472 let mut amended = base_service.clone();
473 amended.append_user_agent("extra".to_string()).unwrap();
474
475 let mut sibling = base_service.clone();
476
477 let req = Request::builder()
478 .uri("http://example.com")
479 .body("".to_string())
480 .unwrap();
481
482 tower::Service::call(&mut sibling, req).await.unwrap();
483
484 let captured_req = rx.recv().await.unwrap();
485 let expected_default = format!("gcloud-sdk-rs/{}", env!("CARGO_PKG_VERSION"));
486 assert_eq!(
487 captured_req
488 .headers()
489 .get(hyper::header::USER_AGENT)
490 .unwrap(),
491 expected_default.as_str()
492 );
493 }
494
495 #[tokio::test]
496 async fn test_additional_headers() {
497 let token_generator = GoogleAuthTokenGenerator::new(
498 TokenSourceType::ExternalSource(Box::new(DummySource)),
499 vec![],
500 )
501 .await
502 .unwrap();
503
504 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
505 let dummy_service = DummyService { tx: Arc::new(tx) };
506 let mut service =
507 GoogleAuthMiddlewareService::new(dummy_service, Arc::new(token_generator), None)
508 .unwrap();
509 let mut test_headers = hyper::HeaderMap::new();
510 test_headers.insert("x-test-header", "test-value".parse().unwrap());
511 service.set_additional_headers(test_headers);
512
513 let req = Request::builder()
514 .uri("http://example.com")
515 .body("".to_string())
516 .unwrap();
517
518 tower::Service::call(&mut service, req).await.unwrap();
519
520 let captured_req = rx.recv().await.unwrap();
521 assert_eq!(
522 captured_req.headers().get("x-test-header").unwrap(),
523 "test-value"
524 );
525 }
526
527 #[tokio::test]
528 async fn additional_headers_keep_every_value_of_a_repeated_name() {
529 let token_generator = GoogleAuthTokenGenerator::new(
530 TokenSourceType::ExternalSource(Box::new(DummySource)),
531 vec![],
532 )
533 .await
534 .unwrap();
535
536 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
537 let dummy_service = DummyService { tx: Arc::new(tx) };
538 let mut service =
539 GoogleAuthMiddlewareService::new(dummy_service, Arc::new(token_generator), None)
540 .unwrap();
541
542 let mut test_headers = hyper::HeaderMap::new();
543 test_headers.append("x-multi", "first".parse().unwrap());
544 test_headers.append("x-multi", "second".parse().unwrap());
545 service.set_additional_headers(test_headers);
546
547 let req = Request::builder()
548 .uri("http://example.com")
549 .body("".to_string())
550 .unwrap();
551
552 tower::Service::call(&mut service, req).await.unwrap();
553
554 let captured_req = rx.recv().await.unwrap();
555 let values: Vec<&str> = captured_req
556 .headers()
557 .get_all("x-multi")
558 .iter()
559 .map(|v| v.to_str().unwrap())
560 .collect();
561 assert_eq!(values, vec!["first", "second"]);
562 }
563
564 #[tokio::test]
565 async fn with_inner_shares_the_token_and_replaces_the_resource_prefix() {
566 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
567 let token_generator = GoogleAuthTokenGenerator::new(
568 TokenSourceType::ExternalSource(Box::new(CountingSource {
569 calls: calls.clone(),
570 })),
571 vec![],
572 )
573 .await
574 .unwrap();
575
576 let (tx, mut rx) = tokio::sync::mpsc::channel(2);
577 let mut extra = hyper::HeaderMap::new();
578 extra.insert("x-test-header", "test-value".parse().unwrap());
579 let mut layer =
580 GoogleAuthMiddlewareLayer::new(token_generator, Some("projects/first".to_string()))
581 .unwrap()
582 .amend_user_agent("extra-ua".to_string())
583 .unwrap();
584 layer.set_additional_headers(extra);
585 let mut first = layer.layer(DummyService {
586 tx: Arc::new(tx.clone()),
587 });
588
589 let (other_tx, mut other_rx) = tokio::sync::mpsc::channel(2);
590 let other = DummyService {
591 tx: Arc::new(other_tx),
592 };
593 let mut with_prefix = first
594 .with_inner(other.clone(), Some("projects/second".to_string()))
595 .unwrap();
596 let mut without_prefix = first.with_inner(other, None).unwrap();
597
598 for service in [&mut first, &mut with_prefix, &mut without_prefix] {
599 let req = Request::builder()
600 .uri("http://example.com")
601 .body("".to_string())
602 .unwrap();
603 tower::Service::call(service, req).await.unwrap();
604 }
605
606 let first_req = rx.recv().await.unwrap();
607 let with_prefix_req = other_rx.recv().await.unwrap();
608 let without_prefix_req = other_rx.recv().await.unwrap();
609
610 let expected_ua = format!("gcloud-sdk-rs/{} extra-ua", env!("CARGO_PKG_VERSION"));
611 for req in [&first_req, &with_prefix_req, &without_prefix_req] {
612 assert_eq!(
613 req.headers().get(hyper::header::USER_AGENT).unwrap(),
614 expected_ua.as_str()
615 );
616 assert_eq!(req.headers().get("x-test-header").unwrap(), "test-value");
617 assert_eq!(
618 req.headers().get("authorization").unwrap(),
619 "Bearer counted-token"
620 );
621 }
622 assert_eq!(
623 first_req
624 .headers()
625 .get(GOOGLE_CLOUD_RESOURCE_PREFIX)
626 .unwrap(),
627 "projects/first"
628 );
629 assert_eq!(
630 with_prefix_req
631 .headers()
632 .get(GOOGLE_CLOUD_RESOURCE_PREFIX)
633 .unwrap(),
634 "projects/second"
635 );
636 assert!(without_prefix_req
637 .headers()
638 .get(GOOGLE_CLOUD_RESOURCE_PREFIX)
639 .is_none());
640 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
641 }
642}