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