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    /// Builds a client of an API served from another endpoint that reuses this client's
110    /// authentication: one token generator, so the token is fetched and refreshed once for
111    /// both clients (BigQuery and Cloud Storage, for example).
112    ///
113    /// Unlike [`get_with`](Self::get_with), which shares the channel and so reaches only
114    /// the same host, this opens a new channel to `google_api_url`. The user agent,
115    /// `x-goog-api-client` and additional headers are carried over;
116    /// `google-cloud-resource-prefix` is not, since another API usually needs another
117    /// prefix or none, and is set from `cloud_resource_prefix` instead.
118    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    // Neither address is dialed: the middleware fetches the token before handing the
438    // request to the channel, and a closed loopback port fails the connection quickly
439    // without ever reaching the network.
440    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        // A client built with `get_with` reuses the same `Arc<GoogleAuthTokenGenerator>`
481        // as one built with `get`, so its token cache is shared: the source is asked for
482        // a token once, not once per client.
483        assert_eq!(calls.load(Ordering::SeqCst), 1);
484    }
485
486    // A gRPC endpoint that accepts connections and never answers, counting the
487    // connections it received: the channel's eager connect succeeds against it, and a
488    // request sent to it reaches the middleware's token fetch before it stalls.
489    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}