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 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
408/// The TLS server name to verify for a gRPC API URL.
409///
410/// Returns the URL's host, without port or path, for an `https` URL, and `None`
411/// for a plain `http` URL, which connects without TLS. Any other scheme, a URL
412/// without a scheme or host, or an unparseable URL is an error.
413fn 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    // Neither address is dialed: the middleware fetches the token before handing the
513    // request to the channel, and a closed loopback port fails the connection quickly
514    // without ever reaching the network.
515    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        // A client built with `get_with` reuses the same `Arc<GoogleAuthTokenGenerator>`
556        // as one built with `get`, so its token cache is shared: the source is asked for
557        // a token once, not once per client.
558        assert_eq!(calls.load(Ordering::SeqCst), 1);
559    }
560
561    // A gRPC endpoint that accepts connections and never answers, counting the
562    // connections it received: the channel's eager connect succeeds against it, and a
563    // request sent to it reaches the middleware's token fetch before it stalls.
564    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}