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 !&api_url_string.contains("http://") {
375 let domain_name = api_url_string.replace("https://", "");
376
377 let tls_config = Self::init_tls_config(domain_name);
378 base_config.tls_config(tls_config)?
379 } else {
380 base_config
381 };
382
383 Ok(config.connect().await?)
384 }
385
386 #[cfg(not(any(feature = "tls-roots", feature = "tls-webpki-roots")))]
387 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
388 tonic::transport::ClientTlsConfig::new()
389 .ca_certificate(tonic::transport::Certificate::from_pem(
390 crate::apis::CERTIFICATES,
391 ))
392 .domain_name(domain_name)
393 }
394
395 #[cfg(feature = "tls-roots")]
396 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
397 tonic::transport::ClientTlsConfig::new()
398 .with_native_roots()
399 .domain_name(domain_name)
400 }
401
402 #[cfg(all(feature = "tls-webpki-roots", not(feature = "tls-roots")))]
403 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
404 tonic::transport::ClientTlsConfig::new()
405 .with_webpki_roots()
406 .domain_name(domain_name)
407 }
408}
409
410pub static GCP_DEFAULT_SCOPES: Lazy<Vec<String>> =
411 Lazy::new(|| vec!["https://www.googleapis.com/auth/cloud-platform".into()]);
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::token_source::{Source, Token, TokenSourceType};
417 use secret_vault_value::SecretValue;
418 use std::sync::atomic::{AtomicUsize, Ordering};
419 use std::sync::Arc;
420
421 struct CountingSource {
422 calls: Arc<AtomicUsize>,
423 }
424
425 #[async_trait]
426 impl Source for CountingSource {
427 async fn token(&self) -> crate::error::Result<Token> {
428 self.calls.fetch_add(1, Ordering::SeqCst);
429 Ok(Token {
430 token_type: "Bearer".to_string(),
431 token: SecretValue::from("counted-token"),
432 expiry: jiff::Timestamp::now() + jiff::SignedDuration::from_hours(1),
433 })
434 }
435 }
436
437 async fn probe(service: &mut GoogleAuthMiddlewareService<Channel>) {
441 let req = hyper::Request::builder()
442 .uri("http://127.0.0.1:1/")
443 .body(tonic::body::Body::empty())
444 .unwrap();
445 let ready = tower::ServiceExt::ready(service).await.unwrap();
446 let _ = tower::Service::call(ready, req).await;
447 }
448
449 #[tokio::test]
450 async fn get_with_shares_the_authenticated_channel() {
451 let calls = Arc::new(AtomicUsize::new(0));
452 let token_generator = GoogleAuthTokenGenerator::new(
453 TokenSourceType::ExternalSource(Box::new(CountingSource {
454 calls: calls.clone(),
455 })),
456 vec![],
457 )
458 .await
459 .unwrap();
460 let middleware = GoogleAuthMiddlewareLayer::new(token_generator, None).unwrap();
461 let channel = Channel::from_static("http://127.0.0.1:1").connect_lazy();
462 let service: GoogleAuthMiddlewareService<Channel> =
463 ServiceBuilder::new().layer(middleware).service(channel);
464
465 let client: GoogleApiClient<
466 GoogleApiClientBuilderFunction<GoogleAuthMiddlewareService<Channel>>,
467 GoogleAuthMiddlewareService<Channel>,
468 > = GoogleApiClient {
469 builder: GoogleApiClientBuilderFunction { f: |svc| svc },
470 service,
471 _ph: PhantomData,
472 };
473
474 let mut from_get = client.get();
475 let mut from_get_with = client.get_with(|svc| svc);
476
477 probe(&mut from_get).await;
478 probe(&mut from_get_with).await;
479
480 assert_eq!(calls.load(Ordering::SeqCst), 1);
484 }
485
486 async fn silent_endpoint() -> (String, Arc<AtomicUsize>) {
490 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
491 let url = format!("http://{}", listener.local_addr().unwrap());
492 let connections = Arc::new(AtomicUsize::new(0));
493 let counter = connections.clone();
494 tokio::spawn(async move {
495 let mut held = Vec::new();
496 while let Ok((socket, _)) = listener.accept().await {
497 counter.fetch_add(1, Ordering::SeqCst);
498 held.push(socket);
499 }
500 });
501 (url, connections)
502 }
503
504 async fn send_stalled(mut service: GoogleAuthMiddlewareService<Channel>) {
505 let _ = tokio::time::timeout(Duration::from_millis(200), probe(&mut service)).await;
506 }
507
508 #[tokio::test]
509 async fn connect_with_endpoint_shares_auth_with_a_client_on_another_host() {
510 let calls = Arc::new(AtomicUsize::new(0));
511 let (first_url, first_connections) = silent_endpoint().await;
512 let (second_url, second_connections) = silent_endpoint().await;
513
514 let first: GoogleApi<GoogleAuthMiddlewareService<Channel>> =
515 GoogleApi::from_function_with_token_source(
516 |svc| svc,
517 first_url,
518 None,
519 vec![],
520 TokenSourceType::ExternalSource(Box::new(CountingSource {
521 calls: calls.clone(),
522 })),
523 )
524 .await
525 .unwrap();
526 let second = first
527 .connect_with_endpoint(|svc| svc, second_url, None)
528 .await
529 .unwrap();
530
531 send_stalled(first.get()).await;
532 send_stalled(second.get()).await;
533
534 assert_eq!(first_connections.load(Ordering::SeqCst), 1);
535 assert_eq!(second_connections.load(Ordering::SeqCst), 1);
536 assert_eq!(calls.load(Ordering::SeqCst), 1);
537 }
538}