1use std::marker::PhantomData;
2use std::time::Duration;
3
4use crate::token_source::auth_token_generator::GoogleAuthTokenGenerator;
5use async_trait::async_trait;
6use once_cell::sync::Lazy;
7use tonic::transport::Channel;
8use tower::ServiceBuilder;
9use tracing::*;
10
11use crate::middleware::{GoogleAuthMiddlewareLayer, GoogleAuthMiddlewareService};
12use crate::token_source::credentials::CredentialsInfo;
13use crate::token_source::*;
14
15#[async_trait]
16pub trait GoogleApiClientBuilder<C>
17where
18 C: Clone + Send,
19{
20 fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C;
21}
22
23#[derive(Clone)]
24pub struct GoogleApiClient<B, C>
25where
26 B: GoogleApiClientBuilder<C>,
27 C: Clone + Send,
28{
29 builder: B,
30 service: GoogleAuthMiddlewareService<Channel>,
31 _ph: PhantomData<C>,
32}
33
34impl<B, C> GoogleApiClient<B, C>
35where
36 B: GoogleApiClientBuilder<C>,
37 C: Clone + Send,
38{
39 pub async fn with_token_source<S: AsRef<str>>(
40 builder: B,
41 google_api_url: S,
42 cloud_resource_prefix: Option<String>,
43 token_source_type: TokenSourceType,
44 token_scopes: Vec<String>,
45 ) -> crate::error::Result<Self> {
46 Self::with_token_source_and_headers(
47 builder,
48 google_api_url,
49 cloud_resource_prefix,
50 token_source_type,
51 token_scopes,
52 hyper::HeaderMap::new(),
53 )
54 .await
55 }
56
57 pub async fn with_token_source_and_headers<S: AsRef<str>>(
58 builder: B,
59 google_api_url: S,
60 cloud_resource_prefix: Option<String>,
61 token_source_type: TokenSourceType,
62 token_scopes: Vec<String>,
63 additional_headers: hyper::HeaderMap,
64 ) -> crate::error::Result<Self> {
65 debug!(
66 "Creating a new Google API client for {}. Scopes: {:?}",
67 google_api_url.as_ref(),
68 token_scopes
69 );
70
71 let token_generator =
72 GoogleAuthTokenGenerator::new(token_source_type, token_scopes).await?;
73
74 let mut middleware =
75 GoogleAuthMiddlewareLayer::new(token_generator, cloud_resource_prefix)?;
76 middleware.set_additional_headers(additional_headers);
77
78 Self::with_token_source_and_middleware(builder, google_api_url, middleware).await
79 }
80
81 pub async fn with_token_source_and_middleware<S: AsRef<str>>(
82 builder: B,
83 google_api_url: S,
84 middleware: GoogleAuthMiddlewareLayer,
85 ) -> crate::error::Result<Self> {
86 let channel = GoogleEnvironment::init_google_services_channel(google_api_url).await?;
87
88 let service: GoogleAuthMiddlewareService<Channel> =
89 ServiceBuilder::new().layer(middleware).service(channel);
90
91 Ok(Self {
92 builder,
93 service,
94 _ph: PhantomData,
95 })
96 }
97
98 pub fn get(&self) -> C {
99 self.builder.create_client(self.service.clone())
100 }
101
102 pub fn get_with<C2>(&self, f: impl FnOnce(GoogleAuthMiddlewareService<Channel>) -> C2) -> C2 {
106 f(self.service.clone())
107 }
108
109 pub async fn connect_with_endpoint<C2, S: AsRef<str>>(
119 &self,
120 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C2,
121 google_api_url: S,
122 cloud_resource_prefix: Option<String>,
123 ) -> crate::error::Result<GoogleApi<C2>>
124 where
125 C2: Clone + Send,
126 {
127 let channel = GoogleEnvironment::init_google_services_channel(google_api_url).await?;
128 Ok(GoogleApiClient {
129 builder: GoogleApiClientBuilderFunction { f: builder_fn },
130 service: self.service.with_inner(channel, cloud_resource_prefix)?,
131 _ph: PhantomData,
132 })
133 }
134
135 pub fn amend_user_agent(mut self, user_agent: String) -> crate::error::Result<Self> {
136 self.service.append_user_agent(user_agent)?;
137 Ok(self)
138 }
139
140 pub fn amend_x_goog_api_client(
141 mut self,
142 x_goog_api_client: String,
143 ) -> crate::error::Result<Self> {
144 self.service.append_x_goog_api_client(x_goog_api_client)?;
145 Ok(self)
146 }
147}
148
149#[derive(Clone)]
150pub struct GoogleApiClientBuilderFunction<C>
151where
152 C: Clone + Send,
153{
154 f: fn(GoogleAuthMiddlewareService<Channel>) -> C,
155}
156
157impl<C> GoogleApiClientBuilder<C> for GoogleApiClientBuilderFunction<C>
158where
159 C: Clone + Send,
160{
161 fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C {
162 (self.f)(channel)
163 }
164}
165
166impl<C> GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>
167where
168 C: Clone + Send,
169{
170 pub async fn from_function<S: AsRef<str>>(
171 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
172 google_api_url: S,
173 cloud_resource_prefix_meta: Option<String>,
174 ) -> crate::error::Result<Self> {
175 Self::from_function_with_scopes(
176 builder_fn,
177 google_api_url,
178 cloud_resource_prefix_meta,
179 GCP_DEFAULT_SCOPES.clone(),
180 )
181 .await
182 }
183
184 pub async fn from_function_with_headers<S: AsRef<str>>(
185 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
186 google_api_url: S,
187 cloud_resource_prefix_meta: Option<String>,
188 headers: hyper::HeaderMap,
189 ) -> crate::error::Result<Self> {
190 Self::from_function_with_scopes_and_headers(
191 builder_fn,
192 google_api_url,
193 cloud_resource_prefix_meta,
194 GCP_DEFAULT_SCOPES.clone(),
195 headers,
196 )
197 .await
198 }
199
200 pub async fn from_function_with_scopes<S: AsRef<str>>(
201 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
202 google_api_url: S,
203 cloud_resource_prefix_meta: Option<String>,
204 token_scopes: Vec<String>,
205 ) -> crate::error::Result<Self> {
206 Self::from_function_with_token_source(
207 builder_fn,
208 google_api_url,
209 cloud_resource_prefix_meta,
210 token_scopes,
211 TokenSourceType::Default,
212 )
213 .await
214 }
215
216 pub async fn from_function_with_scopes_and_headers<S: AsRef<str>>(
217 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
218 google_api_url: S,
219 cloud_resource_prefix_meta: Option<String>,
220 token_scopes: Vec<String>,
221 headers: hyper::HeaderMap,
222 ) -> crate::error::Result<Self> {
223 Self::from_function_with_token_source_and_headers(
224 builder_fn,
225 google_api_url,
226 cloud_resource_prefix_meta,
227 token_scopes,
228 TokenSourceType::Default,
229 headers,
230 )
231 .await
232 }
233
234 pub async fn from_function_with_token_source<S: AsRef<str>>(
235 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
236 google_api_url: S,
237 cloud_resource_prefix_meta: Option<String>,
238 token_scopes: Vec<String>,
239 token_source_type: TokenSourceType,
240 ) -> crate::error::Result<Self> {
241 let builder: GoogleApiClientBuilderFunction<C> =
242 GoogleApiClientBuilderFunction { f: builder_fn };
243
244 Self::with_token_source(
245 builder,
246 google_api_url,
247 cloud_resource_prefix_meta,
248 token_source_type,
249 token_scopes,
250 )
251 .await
252 }
253
254 pub async fn from_function_with_token_source_and_headers<S: AsRef<str>>(
255 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
256 google_api_url: S,
257 cloud_resource_prefix_meta: Option<String>,
258 token_scopes: Vec<String>,
259 token_source_type: TokenSourceType,
260 headers: hyper::HeaderMap,
261 ) -> crate::error::Result<Self> {
262 let builder: GoogleApiClientBuilderFunction<C> =
263 GoogleApiClientBuilderFunction { f: builder_fn };
264
265 Self::with_token_source_and_headers(
266 builder,
267 google_api_url,
268 cloud_resource_prefix_meta,
269 token_source_type,
270 token_scopes,
271 headers,
272 )
273 .await
274 }
275
276 pub async fn from_function_with_middleware<S: AsRef<str>>(
277 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
278 google_api_url: S,
279 middleware: GoogleAuthMiddlewareLayer,
280 ) -> crate::error::Result<Self> {
281 let builder: GoogleApiClientBuilderFunction<C> =
282 GoogleApiClientBuilderFunction { f: builder_fn };
283
284 Self::with_token_source_and_middleware(builder, google_api_url, middleware).await
285 }
286}
287
288pub type GoogleAuthMiddleware = GoogleAuthMiddlewareService<Channel>;
289pub type GoogleApi<C> = GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>;
290
291pub struct GoogleEnvironment;
292
293impl GoogleEnvironment {
294 pub async fn detect_google_project_id() -> Option<String> {
295 let for_env = std::env::var("GCP_PROJECT")
296 .ok()
297 .or_else(|| std::env::var("PROJECT_ID").ok())
298 .or_else(|| std::env::var("GCP_PROJECT_ID").ok());
299 if for_env.is_some() {
300 debug!("Detected GCP Project ID using environment variables");
301 for_env
302 } else {
303 let local_creds = match crate::token_source::from_env_var(&GCP_DEFAULT_SCOPES) {
304 Ok(Some(creds)) => Some(creds),
305 Ok(None) | Err(_) => crate::token_source::from_well_known_file(&GCP_DEFAULT_SCOPES)
306 .ok()
307 .flatten(),
308 };
309
310 let local_quota_project_id =
311 local_creds.and_then(|creds| creds.quota_project_id().map(ToString::to_string));
312
313 if local_quota_project_id.is_some() {
314 debug!("Detected default project id from local defined in quota_project_id for the service account file.");
315 local_quota_project_id
316 } else {
317 let mut metadata_server =
318 crate::token_source::metadata::Metadata::new(GCP_DEFAULT_SCOPES.clone());
319 if metadata_server.init().await {
320 let metadata_result = metadata_server.detect_google_project_id().await;
321 if metadata_result.is_some() {
322 debug!("Detected GCP Project ID using GKE metadata server");
323 metadata_result
324 } else {
325 debug!("No GCP Project ID detected in this environment. Please specify it explicitly using environment variables: `PROJECT_ID`,`GCP_PROJECT_ID`, or `GCP_PROJECT`");
326 metadata_result
327 }
328 } else {
329 debug!("No GCP Project ID detected in this environment. Please specify it explicitly using environment variables: `PROJECT_ID`,`GCP_PROJECT_ID`, or `GCP_PROJECT`");
330 None
331 }
332 }
333 }
334 }
335
336 pub async fn find_default_creds(
337 token_scopes: &[String],
338 ) -> crate::error::Result<Option<CredentialsInfo>> {
339 debug!("Finding default credentials for scopes: {:?}", token_scopes);
340
341 if let Some(src) = from_env_var(token_scopes)? {
342 debug!("Creating credentials based on environment variable: GOOGLE_APPLICATION_CREDENTIALS");
343 return Ok(src.to_credentials_info());
344 }
345 if let Some(src) = from_well_known_file(token_scopes)? {
346 debug!("Creating credentials based on standard config files such as application_default_credentials.json");
347 return Ok(src.to_credentials_info());
348 }
349 let mut metadata_server = crate::token_source::metadata::Metadata::new(token_scopes);
350 if metadata_server.init().await {
351 let metadata_result_email = metadata_server.email().await;
352 if let Some(email) = metadata_result_email {
353 debug!("Detected SA email using GKE metadata server");
354 return Ok(Some(CredentialsInfo {
355 client_email: email,
356 project_id: metadata_server.detect_google_project_id().await,
357 }));
358 }
359 }
360 Ok(None)
361 }
362
363 pub async fn init_google_services_channel<S: AsRef<str>>(
364 api_url: S,
365 ) -> Result<Channel, crate::error::Error> {
366 let api_url_string = api_url.as_ref().to_string();
367 let base_config = Channel::from_shared(api_url_string.clone())?
368 .connect_timeout(Duration::from_secs(30))
369 .tcp_keepalive(Some(Duration::from_secs(60)))
370 .keep_alive_timeout(Duration::from_secs(60))
371 .http2_keep_alive_interval(Duration::from_secs(60))
372 .keep_alive_while_idle(true);
373
374 let config = if let Some(domain_name) = tls_server_name(&api_url_string)? {
375 let tls_config = Self::init_tls_config(domain_name);
376 base_config.tls_config(tls_config)?
377 } else {
378 base_config
379 };
380
381 Ok(config.connect().await?)
382 }
383
384 #[cfg(not(any(feature = "tls-roots", feature = "tls-webpki-roots")))]
385 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
386 tonic::transport::ClientTlsConfig::new()
387 .ca_certificate(tonic::transport::Certificate::from_pem(
388 crate::apis::CERTIFICATES,
389 ))
390 .domain_name(domain_name)
391 }
392
393 #[cfg(feature = "tls-roots")]
394 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
395 tonic::transport::ClientTlsConfig::new()
396 .with_native_roots()
397 .domain_name(domain_name)
398 }
399
400 #[cfg(all(feature = "tls-webpki-roots", not(feature = "tls-roots")))]
401 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
402 tonic::transport::ClientTlsConfig::new()
403 .with_webpki_roots()
404 .domain_name(domain_name)
405 }
406}
407
408fn tls_server_name(api_url: &str) -> Result<Option<String>, crate::error::Error> {
414 let uri: hyper::http::Uri = api_url.parse()?;
415 let invalid = |reason: &str| {
416 crate::error::Error::from(crate::error::ErrorKind::InvalidApiUrl(format!(
417 "{reason}: {api_url}"
418 )))
419 };
420 match uri.scheme_str() {
421 Some("https") => {
422 let host = uri
423 .host()
424 .filter(|host| !host.is_empty())
425 .ok_or_else(|| invalid("missing host"))?;
426 let host = host
427 .strip_prefix('[')
428 .and_then(|h| h.strip_suffix(']'))
429 .unwrap_or(host);
430 Ok(Some(host.to_string()))
431 }
432 Some("http") => Ok(None),
433 Some(_) => Err(invalid("unsupported scheme")),
434 None => Err(invalid("missing scheme")),
435 }
436}
437
438pub static GCP_DEFAULT_SCOPES: Lazy<Vec<String>> =
439 Lazy::new(|| vec!["https://www.googleapis.com/auth/cloud-platform".into()]);
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn tls_server_name_is_the_host_of_a_plain_https_url() {
447 assert_eq!(
448 tls_server_name("https://bigquery.googleapis.com").unwrap(),
449 Some("bigquery.googleapis.com".to_string())
450 );
451 }
452
453 #[test]
454 fn tls_server_name_ignores_a_trailing_slash() {
455 assert_eq!(
456 tls_server_name("https://bigquery.googleapis.com/").unwrap(),
457 Some("bigquery.googleapis.com".to_string())
458 );
459 }
460
461 #[test]
462 fn tls_server_name_ignores_the_path() {
463 assert_eq!(
464 tls_server_name("https://bigquery.googleapis.com/bigquery/v2").unwrap(),
465 Some("bigquery.googleapis.com".to_string())
466 );
467 }
468
469 #[test]
470 fn tls_server_name_ignores_an_explicit_port() {
471 assert_eq!(
472 tls_server_name("https://bigquery.googleapis.com:443").unwrap(),
473 Some("bigquery.googleapis.com".to_string())
474 );
475 }
476
477 #[test]
478 fn plain_http_url_uses_no_tls() {
479 assert_eq!(tls_server_name("http://localhost:8080").unwrap(), None);
480 assert_eq!(tls_server_name("http://localhost:8080/").unwrap(), None);
481 }
482
483 #[test]
484 fn url_without_scheme_or_host_is_an_error() {
485 assert!(tls_server_name("bigquery.googleapis.com").is_err());
486 assert!(tls_server_name("/bigquery/v2").is_err());
487 assert!(tls_server_name("https://").is_err());
488 assert!(tls_server_name("not a url").is_err());
489 assert!(tls_server_name("").is_err());
490 }
491 use crate::token_source::{Source, Token, TokenSourceType};
492 use secret_vault_value::SecretValue;
493 use std::sync::atomic::{AtomicUsize, Ordering};
494 use std::sync::Arc;
495
496 struct CountingSource {
497 calls: Arc<AtomicUsize>,
498 }
499
500 #[async_trait]
501 impl Source for CountingSource {
502 async fn token(&self) -> crate::error::Result<Token> {
503 self.calls.fetch_add(1, Ordering::SeqCst);
504 Ok(Token {
505 token_type: "Bearer".to_string(),
506 token: SecretValue::from("counted-token"),
507 expiry: jiff::Timestamp::now() + jiff::SignedDuration::from_hours(1),
508 })
509 }
510 }
511
512 async fn probe(service: &mut GoogleAuthMiddlewareService<Channel>) {
516 let req = hyper::Request::builder()
517 .uri("http://127.0.0.1:1/")
518 .body(tonic::body::Body::empty())
519 .unwrap();
520 let ready = tower::ServiceExt::ready(service).await.unwrap();
521 let _ = tower::Service::call(ready, req).await;
522 }
523
524 #[tokio::test]
525 async fn get_with_shares_the_authenticated_channel() {
526 let calls = Arc::new(AtomicUsize::new(0));
527 let token_generator = GoogleAuthTokenGenerator::new(
528 TokenSourceType::ExternalSource(Box::new(CountingSource {
529 calls: calls.clone(),
530 })),
531 vec![],
532 )
533 .await
534 .unwrap();
535 let middleware = GoogleAuthMiddlewareLayer::new(token_generator, None).unwrap();
536 let channel = Channel::from_static("http://127.0.0.1:1").connect_lazy();
537 let service: GoogleAuthMiddlewareService<Channel> =
538 ServiceBuilder::new().layer(middleware).service(channel);
539
540 let client: GoogleApiClient<
541 GoogleApiClientBuilderFunction<GoogleAuthMiddlewareService<Channel>>,
542 GoogleAuthMiddlewareService<Channel>,
543 > = GoogleApiClient {
544 builder: GoogleApiClientBuilderFunction { f: |svc| svc },
545 service,
546 _ph: PhantomData,
547 };
548
549 let mut from_get = client.get();
550 let mut from_get_with = client.get_with(|svc| svc);
551
552 probe(&mut from_get).await;
553 probe(&mut from_get_with).await;
554
555 assert_eq!(calls.load(Ordering::SeqCst), 1);
559 }
560
561 async fn silent_endpoint() -> (String, Arc<AtomicUsize>) {
565 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
566 let url = format!("http://{}", listener.local_addr().unwrap());
567 let connections = Arc::new(AtomicUsize::new(0));
568 let counter = connections.clone();
569 tokio::spawn(async move {
570 let mut held = Vec::new();
571 while let Ok((socket, _)) = listener.accept().await {
572 counter.fetch_add(1, Ordering::SeqCst);
573 held.push(socket);
574 }
575 });
576 (url, connections)
577 }
578
579 async fn send_stalled(mut service: GoogleAuthMiddlewareService<Channel>) {
580 let _ = tokio::time::timeout(Duration::from_millis(200), probe(&mut service)).await;
581 }
582
583 #[tokio::test]
584 async fn connect_with_endpoint_shares_auth_with_a_client_on_another_host() {
585 let calls = Arc::new(AtomicUsize::new(0));
586 let (first_url, first_connections) = silent_endpoint().await;
587 let (second_url, second_connections) = silent_endpoint().await;
588
589 let first: GoogleApi<GoogleAuthMiddlewareService<Channel>> =
590 GoogleApi::from_function_with_token_source(
591 |svc| svc,
592 first_url,
593 None,
594 vec![],
595 TokenSourceType::ExternalSource(Box::new(CountingSource {
596 calls: calls.clone(),
597 })),
598 )
599 .await
600 .unwrap();
601 let second = first
602 .connect_with_endpoint(|svc| svc, second_url, None)
603 .await
604 .unwrap();
605
606 send_stalled(first.get()).await;
607 send_stalled(second.get()).await;
608
609 assert_eq!(first_connections.load(Ordering::SeqCst), 1);
610 assert_eq!(second_connections.load(Ordering::SeqCst), 1);
611 assert_eq!(calls.load(Ordering::SeqCst), 1);
612 }
613}