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 fn amend_user_agent(mut self, user_agent: String) -> crate::error::Result<Self> {
110 self.service.append_user_agent(user_agent)?;
111 Ok(self)
112 }
113
114 pub fn amend_x_goog_api_client(
115 mut self,
116 x_goog_api_client: String,
117 ) -> crate::error::Result<Self> {
118 self.service.append_x_goog_api_client(x_goog_api_client)?;
119 Ok(self)
120 }
121}
122
123#[derive(Clone)]
124pub struct GoogleApiClientBuilderFunction<C>
125where
126 C: Clone + Send,
127{
128 f: fn(GoogleAuthMiddlewareService<Channel>) -> C,
129}
130
131impl<C> GoogleApiClientBuilder<C> for GoogleApiClientBuilderFunction<C>
132where
133 C: Clone + Send,
134{
135 fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C {
136 (self.f)(channel)
137 }
138}
139
140impl<C> GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>
141where
142 C: Clone + Send,
143{
144 pub async fn from_function<S: AsRef<str>>(
145 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
146 google_api_url: S,
147 cloud_resource_prefix_meta: Option<String>,
148 ) -> crate::error::Result<Self> {
149 Self::from_function_with_scopes(
150 builder_fn,
151 google_api_url,
152 cloud_resource_prefix_meta,
153 GCP_DEFAULT_SCOPES.clone(),
154 )
155 .await
156 }
157
158 pub async fn from_function_with_headers<S: AsRef<str>>(
159 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
160 google_api_url: S,
161 cloud_resource_prefix_meta: Option<String>,
162 headers: hyper::HeaderMap,
163 ) -> crate::error::Result<Self> {
164 Self::from_function_with_scopes_and_headers(
165 builder_fn,
166 google_api_url,
167 cloud_resource_prefix_meta,
168 GCP_DEFAULT_SCOPES.clone(),
169 headers,
170 )
171 .await
172 }
173
174 pub async fn from_function_with_scopes<S: AsRef<str>>(
175 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
176 google_api_url: S,
177 cloud_resource_prefix_meta: Option<String>,
178 token_scopes: Vec<String>,
179 ) -> crate::error::Result<Self> {
180 Self::from_function_with_token_source(
181 builder_fn,
182 google_api_url,
183 cloud_resource_prefix_meta,
184 token_scopes,
185 TokenSourceType::Default,
186 )
187 .await
188 }
189
190 pub async fn from_function_with_scopes_and_headers<S: AsRef<str>>(
191 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
192 google_api_url: S,
193 cloud_resource_prefix_meta: Option<String>,
194 token_scopes: Vec<String>,
195 headers: hyper::HeaderMap,
196 ) -> crate::error::Result<Self> {
197 Self::from_function_with_token_source_and_headers(
198 builder_fn,
199 google_api_url,
200 cloud_resource_prefix_meta,
201 token_scopes,
202 TokenSourceType::Default,
203 headers,
204 )
205 .await
206 }
207
208 pub async fn from_function_with_token_source<S: AsRef<str>>(
209 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
210 google_api_url: S,
211 cloud_resource_prefix_meta: Option<String>,
212 token_scopes: Vec<String>,
213 token_source_type: TokenSourceType,
214 ) -> crate::error::Result<Self> {
215 let builder: GoogleApiClientBuilderFunction<C> =
216 GoogleApiClientBuilderFunction { f: builder_fn };
217
218 Self::with_token_source(
219 builder,
220 google_api_url,
221 cloud_resource_prefix_meta,
222 token_source_type,
223 token_scopes,
224 )
225 .await
226 }
227
228 pub async fn from_function_with_token_source_and_headers<S: AsRef<str>>(
229 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
230 google_api_url: S,
231 cloud_resource_prefix_meta: Option<String>,
232 token_scopes: Vec<String>,
233 token_source_type: TokenSourceType,
234 headers: hyper::HeaderMap,
235 ) -> crate::error::Result<Self> {
236 let builder: GoogleApiClientBuilderFunction<C> =
237 GoogleApiClientBuilderFunction { f: builder_fn };
238
239 Self::with_token_source_and_headers(
240 builder,
241 google_api_url,
242 cloud_resource_prefix_meta,
243 token_source_type,
244 token_scopes,
245 headers,
246 )
247 .await
248 }
249
250 pub async fn from_function_with_middleware<S: AsRef<str>>(
251 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
252 google_api_url: S,
253 middleware: GoogleAuthMiddlewareLayer,
254 ) -> crate::error::Result<Self> {
255 let builder: GoogleApiClientBuilderFunction<C> =
256 GoogleApiClientBuilderFunction { f: builder_fn };
257
258 Self::with_token_source_and_middleware(builder, google_api_url, middleware).await
259 }
260}
261
262pub type GoogleAuthMiddleware = GoogleAuthMiddlewareService<Channel>;
263pub type GoogleApi<C> = GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>;
264
265pub struct GoogleEnvironment;
266
267impl GoogleEnvironment {
268 pub async fn detect_google_project_id() -> Option<String> {
269 let for_env = std::env::var("GCP_PROJECT")
270 .ok()
271 .or_else(|| std::env::var("PROJECT_ID").ok())
272 .or_else(|| std::env::var("GCP_PROJECT_ID").ok());
273 if for_env.is_some() {
274 debug!("Detected GCP Project ID using environment variables");
275 for_env
276 } else {
277 let local_creds = match crate::token_source::from_env_var(&GCP_DEFAULT_SCOPES) {
278 Ok(Some(creds)) => Some(creds),
279 Ok(None) | Err(_) => crate::token_source::from_well_known_file(&GCP_DEFAULT_SCOPES)
280 .ok()
281 .flatten(),
282 };
283
284 let local_quota_project_id =
285 local_creds.and_then(|creds| creds.quota_project_id().map(ToString::to_string));
286
287 if local_quota_project_id.is_some() {
288 debug!("Detected default project id from local defined in quota_project_id for the service account file.");
289 local_quota_project_id
290 } else {
291 let mut metadata_server =
292 crate::token_source::metadata::Metadata::new(GCP_DEFAULT_SCOPES.clone());
293 if metadata_server.init().await {
294 let metadata_result = metadata_server.detect_google_project_id().await;
295 if metadata_result.is_some() {
296 debug!("Detected GCP Project ID using GKE metadata server");
297 metadata_result
298 } else {
299 debug!("No GCP Project ID detected in this environment. Please specify it explicitly using environment variables: `PROJECT_ID`,`GCP_PROJECT_ID`, or `GCP_PROJECT`");
300 metadata_result
301 }
302 } else {
303 debug!("No GCP Project ID detected in this environment. Please specify it explicitly using environment variables: `PROJECT_ID`,`GCP_PROJECT_ID`, or `GCP_PROJECT`");
304 None
305 }
306 }
307 }
308 }
309
310 pub async fn find_default_creds(
311 token_scopes: &[String],
312 ) -> crate::error::Result<Option<CredentialsInfo>> {
313 debug!("Finding default credentials for scopes: {:?}", token_scopes);
314
315 if let Some(src) = from_env_var(token_scopes)? {
316 debug!("Creating credentials based on environment variable: GOOGLE_APPLICATION_CREDENTIALS");
317 return Ok(src.to_credentials_info());
318 }
319 if let Some(src) = from_well_known_file(token_scopes)? {
320 debug!("Creating credentials based on standard config files such as application_default_credentials.json");
321 return Ok(src.to_credentials_info());
322 }
323 let mut metadata_server = crate::token_source::metadata::Metadata::new(token_scopes);
324 if metadata_server.init().await {
325 let metadata_result_email = metadata_server.email().await;
326 if let Some(email) = metadata_result_email {
327 debug!("Detected SA email using GKE metadata server");
328 return Ok(Some(CredentialsInfo {
329 client_email: email,
330 project_id: metadata_server.detect_google_project_id().await,
331 }));
332 }
333 }
334 Ok(None)
335 }
336
337 pub async fn init_google_services_channel<S: AsRef<str>>(
338 api_url: S,
339 ) -> Result<Channel, crate::error::Error> {
340 let api_url_string = api_url.as_ref().to_string();
341 let base_config = Channel::from_shared(api_url_string.clone())?
342 .connect_timeout(Duration::from_secs(30))
343 .tcp_keepalive(Some(Duration::from_secs(60)))
344 .keep_alive_timeout(Duration::from_secs(60))
345 .http2_keep_alive_interval(Duration::from_secs(60))
346 .keep_alive_while_idle(true);
347
348 let config = if !&api_url_string.contains("http://") {
349 let domain_name = api_url_string.replace("https://", "");
350
351 let tls_config = Self::init_tls_config(domain_name);
352 base_config.tls_config(tls_config)?
353 } else {
354 base_config
355 };
356
357 Ok(config.connect().await?)
358 }
359
360 #[cfg(not(any(feature = "tls-roots", feature = "tls-webpki-roots")))]
361 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
362 tonic::transport::ClientTlsConfig::new()
363 .ca_certificate(tonic::transport::Certificate::from_pem(
364 crate::apis::CERTIFICATES,
365 ))
366 .domain_name(domain_name)
367 }
368
369 #[cfg(feature = "tls-roots")]
370 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
371 tonic::transport::ClientTlsConfig::new()
372 .with_native_roots()
373 .domain_name(domain_name)
374 }
375
376 #[cfg(all(feature = "tls-webpki-roots", not(feature = "tls-roots")))]
377 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
378 tonic::transport::ClientTlsConfig::new()
379 .with_webpki_roots()
380 .domain_name(domain_name)
381 }
382}
383
384pub static GCP_DEFAULT_SCOPES: Lazy<Vec<String>> =
385 Lazy::new(|| vec!["https://www.googleapis.com/auth/cloud-platform".into()]);
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::token_source::{Source, Token, TokenSourceType};
391 use secret_vault_value::SecretValue;
392 use std::sync::atomic::{AtomicUsize, Ordering};
393 use std::sync::Arc;
394
395 struct CountingSource {
396 calls: Arc<AtomicUsize>,
397 }
398
399 #[async_trait]
400 impl Source for CountingSource {
401 async fn token(&self) -> crate::error::Result<Token> {
402 self.calls.fetch_add(1, Ordering::SeqCst);
403 Ok(Token {
404 token_type: "Bearer".to_string(),
405 token: SecretValue::from("counted-token"),
406 expiry: jiff::Timestamp::now() + jiff::SignedDuration::from_hours(1),
407 })
408 }
409 }
410
411 async fn probe(service: &mut GoogleAuthMiddlewareService<Channel>) {
415 let req = hyper::Request::builder()
416 .uri("http://127.0.0.1:1/")
417 .body(tonic::body::Body::empty())
418 .unwrap();
419 let ready = tower::ServiceExt::ready(service).await.unwrap();
420 let _ = tower::Service::call(ready, req).await;
421 }
422
423 #[tokio::test]
424 async fn get_with_shares_the_authenticated_channel() {
425 let calls = Arc::new(AtomicUsize::new(0));
426 let token_generator = GoogleAuthTokenGenerator::new(
427 TokenSourceType::ExternalSource(Box::new(CountingSource {
428 calls: calls.clone(),
429 })),
430 vec![],
431 )
432 .await
433 .unwrap();
434 let middleware = GoogleAuthMiddlewareLayer::new(token_generator, None).unwrap();
435 let channel = Channel::from_static("http://127.0.0.1:1").connect_lazy();
436 let service: GoogleAuthMiddlewareService<Channel> =
437 ServiceBuilder::new().layer(middleware).service(channel);
438
439 let client: GoogleApiClient<
440 GoogleApiClientBuilderFunction<GoogleAuthMiddlewareService<Channel>>,
441 GoogleAuthMiddlewareService<Channel>,
442 > = GoogleApiClient {
443 builder: GoogleApiClientBuilderFunction { f: |svc| svc },
444 service,
445 _ph: PhantomData,
446 };
447
448 let mut from_get = client.get();
449 let mut from_get_with = client.get_with(|svc| svc);
450
451 probe(&mut from_get).await;
452 probe(&mut from_get_with).await;
453
454 assert_eq!(calls.load(Ordering::SeqCst), 1);
458 }
459}