Skip to main content

gcloud_sdk/
api_client.rs

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    /// Builds a client of another API on this client's authenticated channel: same
103    /// connection, token source and headers, for an API served from the same endpoint
104    /// (Firestore's admin and long-running operations services, for example).
105    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    // Neither address is dialed: the middleware fetches the token before handing the
412    // request to the channel, and a closed loopback port fails the connection quickly
413    // without ever reaching the network.
414    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        // A client built with `get_with` reuses the same `Arc<GoogleAuthTokenGenerator>`
455        // as one built with `get`, so its token cache is shared: the source is asked for
456        // a token once, not once per client.
457        assert_eq!(calls.load(Ordering::SeqCst), 1);
458    }
459}