google_cloud_auth/
credentials.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::build_errors::Error as BuilderError;
16use crate::constants::GOOGLE_CLOUD_QUOTA_PROJECT_VAR;
17use crate::errors::{self, CredentialsError};
18use crate::{BuildResult, Result};
19use http::{Extensions, HeaderMap};
20use serde_json::Value;
21use std::future::Future;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicU64, Ordering};
24
25pub mod api_key_credentials;
26pub mod external_account;
27pub(crate) mod external_account_sources;
28pub mod impersonated;
29pub(crate) mod internal;
30pub mod mds;
31pub mod service_account;
32pub mod subject_token;
33pub mod user_account;
34pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
35pub(crate) const DEFAULT_UNIVERSE_DOMAIN: &str = "googleapis.com";
36
37/// Represents an Entity Tag for a [CacheableResource].
38///
39/// An `EntityTag` is an opaque token that can be used to determine if a
40/// cached resource has changed. The specific format of this tag is an
41/// implementation detail.
42///
43/// As the name indicates, these are inspired by the ETags prevalent in HTTP
44/// caching protocols. Their implementation is very different, and are only
45/// intended for use within a single program.
46#[derive(Clone, Debug, PartialEq, Default)]
47pub struct EntityTag(u64);
48
49static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
50impl EntityTag {
51    pub fn new() -> Self {
52        let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
53        Self(value)
54    }
55}
56
57/// Represents a resource that can be cached, along with its [EntityTag].
58///
59/// This enum is used to provide cacheable data to the consumers of the credential provider.
60/// It allows a data provider to return either new data (with an [EntityTag]) or
61/// indicate that the caller's cached version (identified by a previously provided [EntityTag])
62/// is still valid.
63#[derive(Clone, PartialEq, Debug)]
64pub enum CacheableResource<T> {
65    NotModified,
66    New { entity_tag: EntityTag, data: T },
67}
68
69/// An implementation of [crate::credentials::CredentialsProvider].
70///
71/// Represents a [Credentials] used to obtain the auth request headers.
72///
73/// In general, [Credentials][credentials-link] are "digital object that provide
74/// proof of identity", the archetype may be a username and password
75/// combination, but a private RSA key may be a better example.
76///
77/// Modern authentication protocols do not send the credentials to authenticate
78/// with a service. Even when sent over encrypted transports, the credentials
79/// may be accidentally exposed via logging or may be captured if there are
80/// errors in the transport encryption. Because the credentials are often
81/// long-lived, that risk of exposure is also long-lived.
82///
83/// Instead, modern authentication protocols exchange the credentials for a
84/// time-limited [Token][token-link], a digital object that shows the caller was
85/// in possession of the credentials. Because tokens are time limited, risk of
86/// misuse is also time limited. Tokens may be further restricted to only a
87/// certain subset of the RPCs in the service, or even to specific resources, or
88/// only when used from a given machine (virtual or not). Further limiting the
89/// risks associated with any leaks of these tokens.
90///
91/// This struct also abstracts token sources that are not backed by a specific
92/// digital object. The canonical example is the [Metadata Service]. This
93/// service is available in many Google Cloud environments, including
94/// [Google Compute Engine], and [Google Kubernetes Engine].
95///
96/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
97/// [token-link]: https://cloud.google.com/docs/authentication#token
98/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
99/// [Google Compute Engine]: https://cloud.google.com/products/compute
100/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
101#[derive(Clone, Debug)]
102pub struct Credentials {
103    // We use an `Arc` to hold the inner implementation.
104    //
105    // Credentials may be shared across threads (`Send + Sync`), so an `Rc`
106    // will not do.
107    //
108    // They also need to derive `Clone`, as the
109    // `gax::http_client::ReqwestClient`s which hold them derive `Clone`. So a
110    // `Box` will not do.
111    inner: Arc<dyn dynamic::CredentialsProvider>,
112}
113
114impl<T> std::convert::From<T> for Credentials
115where
116    T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
117{
118    fn from(value: T) -> Self {
119        Self {
120            inner: Arc::new(value),
121        }
122    }
123}
124
125impl Credentials {
126    pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
127        self.inner.headers(extensions).await
128    }
129
130    pub async fn universe_domain(&self) -> Option<String> {
131        self.inner.universe_domain().await
132    }
133}
134
135/// Represents a [Credentials] used to obtain auth request headers.
136///
137/// In general, [Credentials][credentials-link] are "digital object that
138/// provide proof of identity", the archetype may be a username and password
139/// combination, but a private RSA key may be a better example.
140///
141/// Modern authentication protocols do not send the credentials to
142/// authenticate with a service. Even when sent over encrypted transports,
143/// the credentials may be accidentally exposed via logging or may be
144/// captured if there are errors in the transport encryption. Because the
145/// credentials are often long-lived, that risk of exposure is also
146/// long-lived.
147///
148/// Instead, modern authentication protocols exchange the credentials for a
149/// time-limited [Token][token-link], a digital object that shows the caller
150/// was in possession of the credentials. Because tokens are time limited,
151/// risk of misuse is also time limited. Tokens may be further restricted to
152/// only a certain subset of the RPCs in the service, or even to specific
153/// resources, or only when used from a given machine (virtual or not).
154/// Further limiting the risks associated with any leaks of these tokens.
155///
156/// This struct also abstracts token sources that are not backed by a
157/// specific digital object. The canonical example is the
158/// [Metadata Service]. This service is available in many Google Cloud
159/// environments, including [Google Compute Engine], and
160/// [Google Kubernetes Engine].
161///
162/// # Notes
163///
164/// Application developers who directly use the Auth SDK can use this trait,
165/// along with [crate::credentials::Credentials::from()] to mock the credentials.
166/// Application developers who use the Google Cloud Rust SDK directly should not
167/// need this functionality.
168///
169/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
170/// [token-link]: https://cloud.google.com/docs/authentication#token
171/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
172/// [Google Compute Engine]: https://cloud.google.com/products/compute
173/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
174pub trait CredentialsProvider: std::fmt::Debug {
175    /// Asynchronously constructs the auth headers.
176    ///
177    /// Different auth tokens are sent via different headers. The
178    /// [Credentials] constructs the headers (and header values) that should be
179    /// sent with a request.
180    ///
181    /// # Parameters
182    /// * `extensions` - An `http::Extensions` map that can be used to pass additional
183    ///   context to the credential provider. For caching purposes, this can include
184    ///   an [EntityTag] from a previously returned [`CacheableResource<HeaderMap>`].    
185    ///   If a valid `EntityTag` is provided and the underlying authentication data
186    ///   has not changed, this method returns `Ok(CacheableResource::NotModified)`.
187    ///
188    /// # Returns
189    /// A `Future` that resolves to a `Result` containing:
190    /// * `Ok(CacheableResource::New { entity_tag, data })`: If new or updated headers
191    ///   are available.
192    /// * `Ok(CacheableResource::NotModified)`: If the headers have not changed since
193    ///   the ETag provided via `extensions` was issued.
194    /// * `Err(CredentialsError)`: If an error occurs while trying to fetch or
195    ///   generating the headers.
196    fn headers(
197        &self,
198        extensions: Extensions,
199    ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
200
201    /// Retrieves the universe domain associated with the credentials, if any.
202    fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
203}
204
205pub(crate) mod dynamic {
206    use super::Result;
207    use super::{CacheableResource, Extensions, HeaderMap};
208
209    /// A dyn-compatible, crate-private version of `CredentialsProvider`.
210    #[async_trait::async_trait]
211    pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
212        /// Asynchronously constructs the auth headers.
213        ///
214        /// Different auth tokens are sent via different headers. The
215        /// [Credentials] constructs the headers (and header values) that should be
216        /// sent with a request.
217        ///
218        /// # Parameters
219        /// * `extensions` - An `http::Extensions` map that can be used to pass additional
220        ///   context to the credential provider. For caching purposes, this can include
221        ///   an [EntityTag] from a previously returned [CacheableResource<HeaderMap>].    
222        ///   If a valid `EntityTag` is provided and the underlying authentication data
223        ///   has not changed, this method returns `Ok(CacheableResource::NotModified)`.
224        ///
225        /// # Returns
226        /// A `Future` that resolves to a `Result` containing:
227        /// * `Ok(CacheableResource::New { entity_tag, data })`: If new or updated headers
228        ///   are available.
229        /// * `Ok(CacheableResource::NotModified)`: If the headers have not changed since
230        ///   the ETag provided via `extensions` was issued.
231        /// * `Err(CredentialsError)`: If an error occurs while trying to fetch or
232        ///   generating the headers.
233        async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
234
235        /// Retrieves the universe domain associated with the credentials, if any.
236        async fn universe_domain(&self) -> Option<String> {
237            Some("googleapis.com".to_string())
238        }
239    }
240
241    /// The public CredentialsProvider implements the dyn-compatible CredentialsProvider.
242    #[async_trait::async_trait]
243    impl<T> CredentialsProvider for T
244    where
245        T: super::CredentialsProvider + Send + Sync,
246    {
247        async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
248            T::headers(self, extensions).await
249        }
250        async fn universe_domain(&self) -> Option<String> {
251            T::universe_domain(self).await
252        }
253    }
254}
255
256/// A builder for constructing [`Credentials`] instances.
257///
258/// This builder loads credentials according to the standard
259/// [Application Default Credentials (ADC)][ADC-link] strategy.
260/// ADC is the recommended approach for most applications and conforms to
261/// [AIP-4110]. If you need to load credentials from a non-standard location
262/// or source, you can use Builders on the specific credential types.
263///
264/// Common use cases where using ADC would is useful include:
265/// - Your application is deployed to a Google Cloud environment such as
266///   [Google Compute Engine (GCE)][gce-link],
267///   [Google Kubernetes Engine (GKE)][gke-link], or [Cloud Run]. Each of these
268///   deployment environments provides a default service account to the
269///   application, and offers mechanisms to change this default service account
270///   without any code changes to your application.
271/// - You are testing or developing the application on a workstation (physical
272///   or virtual). These credentials will use your preferences as set with
273///   [gcloud auth application-default]. These preferences can be your own
274///   Google Cloud user credentials, or some service account.
275/// - Regardless of where your application is running, you can use the
276///   `GOOGLE_APPLICATION_CREDENTIALS` environment variable to override the
277///   defaults. This environment variable should point to a file containing a
278///   service account key file, or a JSON object describing your user
279///   credentials.
280///
281/// The headers returned by these credentials should be used in the
282/// Authorization HTTP header.
283///
284/// The Google Cloud client libraries for Rust will typically find and use these
285/// credentials automatically if a credentials file exists in the standard ADC
286/// search paths. You might instantiate these credentials if you need to:
287/// - Override the OAuth 2.0 **scopes** being requested for the access token.
288/// - Override the **quota project ID** for billing and quota management.
289///
290/// # Example: fetching headers using ADC
291/// ```
292/// # use google_cloud_auth::credentials::Builder;
293/// # use http::Extensions;
294/// # tokio_test::block_on(async {
295/// let credentials = Builder::default()
296///     .with_quota_project_id("my-project")
297///     .build()?;
298/// let headers = credentials.headers(Extensions::new()).await?;
299/// println!("Headers: {headers:?}");
300/// # Ok::<(), anyhow::Error>(())
301/// # });
302/// ```
303///
304/// [ADC-link]: https://cloud.google.com/docs/authentication/application-default-credentials
305/// [AIP-4110]: https://google.aip.dev/auth/4110
306/// [Cloud Run]: https://cloud.google.com/run
307/// [gce-link]: https://cloud.google.com/products/compute
308/// [gcloud auth application-default]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default
309/// [gke-link]: https://cloud.google.com/kubernetes-engine
310#[derive(Debug)]
311pub struct Builder {
312    quota_project_id: Option<String>,
313    scopes: Option<Vec<String>>,
314}
315
316impl Default for Builder {
317    /// Creates a new builder where credentials will be obtained via [application-default login].
318    ///
319    /// # Example
320    /// ```
321    /// # use google_cloud_auth::credentials::Builder;
322    /// # tokio_test::block_on(async {
323    /// let credentials = Builder::default().build();
324    /// # });
325    /// ```
326    ///
327    /// [application-default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
328    fn default() -> Self {
329        Self {
330            quota_project_id: None,
331            scopes: None,
332        }
333    }
334}
335
336impl Builder {
337    /// Sets the [quota project] for these credentials.
338    ///
339    /// In some services, you can use an account in one project for authentication
340    /// and authorization, and charge the usage to a different project. This requires
341    /// that the user has `serviceusage.services.use` permissions on the quota project.
342    ///
343    /// ## Important: Precedence
344    /// If the `GOOGLE_CLOUD_QUOTA_PROJECT` environment variable is set,
345    /// its value will be used **instead of** the value provided to this method.
346    ///
347    /// # Example
348    /// ```
349    /// # use google_cloud_auth::credentials::Builder;
350    /// # tokio_test::block_on(async {
351    /// let credentials = Builder::default()
352    ///     .with_quota_project_id("my-project")
353    ///     .build();
354    /// # });
355    /// ```
356    ///
357    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
358    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
359        self.quota_project_id = Some(quota_project_id.into());
360        self
361    }
362
363    /// Sets the [scopes] for these credentials.
364    ///
365    /// `scopes` act as an additional restriction in addition to the IAM permissions
366    /// granted to the principal (user or service account) that creates the token.
367    ///
368    /// `scopes` define the *permissions being requested* for this specific access token
369    /// when interacting with a service. For example,
370    /// `https://www.googleapis.com/auth/devstorage.read_write`.
371    ///
372    /// IAM permissions, on the other hand, define the *underlying capabilities*
373    /// the principal possesses within a system. For example, `storage.buckets.delete`.
374    ///
375    /// The credentials certify that a particular token was created by a certain principal.
376    ///
377    /// When a token generated with specific scopes is used, the request must be permitted
378    /// by both the the principals's underlying IAM permissions and the scopes requested
379    /// for the token.
380    ///
381    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
382    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
383    where
384        I: IntoIterator<Item = S>,
385        S: Into<String>,
386    {
387        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
388        self
389    }
390
391    /// Returns a [Credentials] instance with the configured settings.
392    ///
393    /// # Errors
394    ///
395    /// Returns a [CredentialsError] if a unsupported credential type is provided
396    /// or if the JSON value is either malformed
397    /// or missing required fields. For more information, on how to generate
398    /// json, consult the relevant section in the [application-default credentials] guide.
399    ///
400    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
401    pub fn build(self) -> BuildResult<Credentials> {
402        let json_data = match load_adc()? {
403            AdcContents::Contents(contents) => {
404                Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
405            }
406            AdcContents::FallbackToMds => None,
407        };
408        let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
409            .ok()
410            .or(self.quota_project_id);
411        build_credentials(json_data, quota_project_id, self.scopes)
412    }
413}
414
415#[derive(Debug, PartialEq)]
416enum AdcPath {
417    FromEnv(String),
418    WellKnown(String),
419}
420
421#[derive(Debug, PartialEq)]
422enum AdcContents {
423    Contents(String),
424    FallbackToMds,
425}
426
427fn extract_credential_type(json: &Value) -> BuildResult<&str> {
428    json.get("type")
429        .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
430        .as_str()
431        .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
432}
433
434/// Applies common optional configurations (quota project ID, scopes) to a
435/// specific credential builder instance and then builds it.
436///
437/// This macro centralizes the logic for optionally calling `.with_quota_project_id()`
438/// and `.with_scopes()` on different underlying credential builders (like
439/// `mds::Builder`, `service_account::Builder`, etc.) before calling `.build()`.
440/// It helps avoid repetitive code in the `build_credentials` function.
441macro_rules! config_builder {
442    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
443        let builder = $builder_instance;
444        let builder = $quota_project_id_option
445            .into_iter()
446            .fold(builder, |b, qp| b.with_quota_project_id(qp));
447
448        let builder = $scopes_option
449            .into_iter()
450            .fold(builder, |b, s| $apply_scopes_closure(b, s));
451
452        builder.build()
453    }};
454}
455
456fn build_credentials(
457    json: Option<Value>,
458    quota_project_id: Option<String>,
459    scopes: Option<Vec<String>>,
460) -> BuildResult<Credentials> {
461    match json {
462        None => config_builder!(
463            mds::Builder::from_adc(),
464            quota_project_id,
465            scopes,
466            |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
467        ),
468        Some(json) => {
469            let cred_type = extract_credential_type(&json)?;
470            match cred_type {
471                "authorized_user" => {
472                    config_builder!(
473                        user_account::Builder::new(json),
474                        quota_project_id,
475                        scopes,
476                        |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
477                    )
478                }
479                "service_account" => config_builder!(
480                    service_account::Builder::new(json),
481                    quota_project_id,
482                    scopes,
483                    |b: service_account::Builder, s: Vec<String>| b
484                        .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
485                ),
486                "impersonated_service_account" => {
487                    config_builder!(
488                        impersonated::Builder::new(json),
489                        quota_project_id,
490                        scopes,
491                        |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
492                    )
493                }
494                "external_account" => config_builder!(
495                    external_account::Builder::new(json),
496                    quota_project_id,
497                    scopes,
498                    |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
499                ),
500                _ => Err(BuilderError::unknown_type(cred_type)),
501            }
502        }
503    }
504}
505
506fn path_not_found(path: String) -> BuilderError {
507    BuilderError::loading(format!(
508        "{path}. {}",
509        concat!(
510            "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
511            "environment variable. Verify this environment variable points to ",
512            "a valid file."
513        )
514    ))
515}
516
517fn load_adc() -> BuildResult<AdcContents> {
518    match adc_path() {
519        None => Ok(AdcContents::FallbackToMds),
520        Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
521            Ok(contents) => Ok(AdcContents::Contents(contents)),
522            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
523            Err(e) => Err(BuilderError::loading(e)),
524        },
525        Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
526            Ok(contents) => Ok(AdcContents::Contents(contents)),
527            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
528            Err(e) => Err(BuilderError::loading(e)),
529        },
530    }
531}
532
533/// The path to Application Default Credentials (ADC), as specified in [AIP-4110].
534///
535/// [AIP-4110]: https://google.aip.dev/auth/4110
536fn adc_path() -> Option<AdcPath> {
537    if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
538        return Some(AdcPath::FromEnv(path));
539    }
540    Some(AdcPath::WellKnown(adc_well_known_path()?))
541}
542
543/// The well-known path to ADC on Windows, as specified in [AIP-4113].
544///
545/// [AIP-4113]: https://google.aip.dev/auth/4113
546#[cfg(target_os = "windows")]
547fn adc_well_known_path() -> Option<String> {
548    std::env::var("APPDATA")
549        .ok()
550        .map(|root| root + "/gcloud/application_default_credentials.json")
551}
552
553/// The well-known path to ADC on Linux and Mac, as specified in [AIP-4113].
554///
555/// [AIP-4113]: https://google.aip.dev/auth/4113
556#[cfg(not(target_os = "windows"))]
557fn adc_well_known_path() -> Option<String> {
558    std::env::var("HOME")
559        .ok()
560        .map(|root| root + "/.config/gcloud/application_default_credentials.json")
561}
562
563/// A module providing invalid credentials where authentication does not matter.
564///
565/// These credentials are a convenient way to avoid errors from loading
566/// Application Default Credentials in tests.
567///
568/// This module is mainly relevant to other `google-cloud-*` crates, but some
569/// external developers (i.e. consumers, not developers of `google-cloud-rust`)
570/// may find it useful.
571// Skipping mutation testing for this module. As it exclusively provides
572// hardcoded credential stubs for testing purposes.
573#[cfg_attr(test, mutants::skip)]
574#[doc(hidden)]
575pub mod testing {
576    use super::{CacheableResource, EntityTag};
577    use crate::Result;
578    use crate::credentials::Credentials;
579    use crate::credentials::dynamic::CredentialsProvider;
580    use http::{Extensions, HeaderMap};
581    use std::sync::Arc;
582
583    /// A simple credentials implementation to use in tests where authentication does not matter.
584    ///
585    /// Always returns a "Bearer" token, with "test-only-token" as the value.
586    pub fn test_credentials() -> Credentials {
587        Credentials {
588            inner: Arc::from(TestCredentials {}),
589        }
590    }
591
592    #[derive(Debug)]
593    struct TestCredentials;
594
595    #[async_trait::async_trait]
596    impl CredentialsProvider for TestCredentials {
597        async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
598            Ok(CacheableResource::New {
599                entity_tag: EntityTag::default(),
600                data: HeaderMap::new(),
601            })
602        }
603
604        async fn universe_domain(&self) -> Option<String> {
605            None
606        }
607    }
608
609    /// A simple credentials implementation to use in tests.
610    ///
611    /// Always return an error in `headers()`.
612    pub fn error_credentials(retryable: bool) -> Credentials {
613        Credentials {
614            inner: Arc::from(ErrorCredentials(retryable)),
615        }
616    }
617
618    #[derive(Debug, Default)]
619    struct ErrorCredentials(bool);
620
621    #[async_trait::async_trait]
622    impl CredentialsProvider for ErrorCredentials {
623        async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
624            Err(super::CredentialsError::from_msg(self.0, "test-only"))
625        }
626
627        async fn universe_domain(&self) -> Option<String> {
628            None
629        }
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636    use base64::Engine;
637    use gax::backoff_policy::BackoffPolicy;
638    use gax::retry_policy::RetryPolicy;
639    use gax::retry_result::RetryResult;
640    use gax::retry_throttler::RetryThrottler;
641    use mockall::mock;
642    use num_bigint_dig::BigUint;
643    use reqwest::header::AUTHORIZATION;
644    use rsa::RsaPrivateKey;
645    use rsa::pkcs8::{EncodePrivateKey, LineEnding};
646    use scoped_env::ScopedEnv;
647    use std::error::Error;
648    use std::sync::LazyLock;
649    use test_case::test_case;
650    use tokio::time::Duration;
651
652    mock! {
653        #[derive(Debug)]
654        pub RetryPolicy {}
655        impl RetryPolicy for RetryPolicy {
656            fn on_error(
657                &self,
658                loop_start: std::time::Instant,
659                attempt_count: u32,
660                idempotent: bool,
661                error: gax::error::Error,
662            ) -> RetryResult;
663        }
664    }
665
666    mock! {
667        #[derive(Debug)]
668        pub BackoffPolicy {}
669        impl BackoffPolicy for BackoffPolicy {
670            fn on_failure(
671                &self,
672                loop_start: std::time::Instant,
673                attempt_count: u32,
674            ) -> std::time::Duration;
675        }
676    }
677
678    mockall::mock! {
679        #[derive(Debug)]
680        pub RetryThrottler {}
681        impl RetryThrottler for RetryThrottler {
682            fn throttle_retry_attempt(&self) -> bool;
683            fn on_retry_failure(&mut self, error: &RetryResult);
684            fn on_success(&mut self);
685        }
686    }
687
688    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
689
690    pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
691        let mut retry_policy = MockRetryPolicy::new();
692        retry_policy
693            .expect_on_error()
694            .returning(move |_, attempt_count, _, error| {
695                if attempt_count >= attempts as u32 {
696                    return RetryResult::Exhausted(error);
697                }
698                let is_transient = error
699                    .source()
700                    .and_then(|e| e.downcast_ref::<CredentialsError>())
701                    .is_some_and(|ce| ce.is_transient());
702                if is_transient {
703                    RetryResult::Continue(error)
704                } else {
705                    RetryResult::Permanent(error)
706                }
707            });
708        retry_policy
709    }
710
711    pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
712        let mut backoff_policy = MockBackoffPolicy::new();
713        backoff_policy
714            .expect_on_failure()
715            .return_const(Duration::from_secs(0));
716        backoff_policy
717    }
718
719    pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
720        let mut throttler = MockRetryThrottler::new();
721        throttler.expect_on_retry_failure().return_const(());
722        throttler
723            .expect_throttle_retry_attempt()
724            .return_const(false);
725        throttler.expect_on_success().return_const(());
726        throttler
727    }
728
729    pub(crate) fn get_headers_from_cache(
730        headers: CacheableResource<HeaderMap>,
731    ) -> Result<HeaderMap> {
732        match headers {
733            CacheableResource::New { data, .. } => Ok(data),
734            CacheableResource::NotModified => Err(CredentialsError::from_msg(
735                false,
736                "Expecting headers to be present",
737            )),
738        }
739    }
740
741    pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
742        match headers {
743            CacheableResource::New { data, .. } => data
744                .get(AUTHORIZATION)
745                .and_then(|token_value| token_value.to_str().ok())
746                .and_then(|s| s.split_whitespace().nth(1))
747                .map(|s| s.to_string()),
748            CacheableResource::NotModified => None,
749        }
750    }
751
752    pub(crate) fn get_token_type_from_headers(
753        headers: CacheableResource<HeaderMap>,
754    ) -> Option<String> {
755        match headers {
756            CacheableResource::New { data, .. } => data
757                .get(AUTHORIZATION)
758                .and_then(|token_value| token_value.to_str().ok())
759                .and_then(|s| s.split_whitespace().next())
760                .map(|s| s.to_string()),
761            CacheableResource::NotModified => None,
762        }
763    }
764
765    pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
766        let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
767        let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
768        let e_str: &str = "65537";
769
770        let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
771        let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
772        let public_exponent =
773            BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
774
775        RsaPrivateKey::from_primes(vec![p, q], public_exponent)
776            .expect("Failed to create RsaPrivateKey from primes")
777    });
778
779    pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
780        RSA_PRIVATE_KEY
781            .to_pkcs8_pem(LineEnding::LF)
782            .expect("Failed to encode key to PKCS#8 PEM")
783            .to_string()
784    });
785
786    pub fn b64_decode_to_json(s: String) -> serde_json::Value {
787        let decoded = String::from_utf8(
788            base64::engine::general_purpose::URL_SAFE_NO_PAD
789                .decode(s)
790                .unwrap(),
791        )
792        .unwrap();
793        serde_json::from_str(&decoded).unwrap()
794    }
795
796    #[cfg(target_os = "windows")]
797    #[test]
798    #[serial_test::serial]
799    fn adc_well_known_path_windows() {
800        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
801        let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
802        assert_eq!(
803            adc_well_known_path(),
804            Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
805        );
806        assert_eq!(
807            adc_path(),
808            Some(AdcPath::WellKnown(
809                "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
810            ))
811        );
812    }
813
814    #[cfg(target_os = "windows")]
815    #[test]
816    #[serial_test::serial]
817    fn adc_well_known_path_windows_no_appdata() {
818        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
819        let _appdata = ScopedEnv::remove("APPDATA");
820        assert_eq!(adc_well_known_path(), None);
821        assert_eq!(adc_path(), None);
822    }
823
824    #[cfg(not(target_os = "windows"))]
825    #[test]
826    #[serial_test::serial]
827    fn adc_well_known_path_posix() {
828        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
829        let _home = ScopedEnv::set("HOME", "/home/foo");
830        assert_eq!(
831            adc_well_known_path(),
832            Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
833        );
834        assert_eq!(
835            adc_path(),
836            Some(AdcPath::WellKnown(
837                "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
838            ))
839        );
840    }
841
842    #[cfg(not(target_os = "windows"))]
843    #[test]
844    #[serial_test::serial]
845    fn adc_well_known_path_posix_no_home() {
846        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
847        let _appdata = ScopedEnv::remove("HOME");
848        assert_eq!(adc_well_known_path(), None);
849        assert_eq!(adc_path(), None);
850    }
851
852    #[test]
853    #[serial_test::serial]
854    fn adc_path_from_env() {
855        let _creds = ScopedEnv::set(
856            "GOOGLE_APPLICATION_CREDENTIALS",
857            "/usr/bar/application_default_credentials.json",
858        );
859        assert_eq!(
860            adc_path(),
861            Some(AdcPath::FromEnv(
862                "/usr/bar/application_default_credentials.json".to_string()
863            ))
864        );
865    }
866
867    #[test]
868    #[serial_test::serial]
869    fn load_adc_no_well_known_path_fallback_to_mds() {
870        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
871        let _e2 = ScopedEnv::remove("HOME"); // For posix
872        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
873        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
874    }
875
876    #[test]
877    #[serial_test::serial]
878    fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
879        // Create a new temp directory. There is not an ADC file in here.
880        let dir = tempfile::TempDir::new().unwrap();
881        let path = dir.path().to_str().unwrap();
882        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
883        let _e2 = ScopedEnv::set("HOME", path); // For posix
884        let _e3 = ScopedEnv::set("APPDATA", path); // For windows
885        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
886    }
887
888    #[test]
889    #[serial_test::serial]
890    fn load_adc_no_file_at_env_is_error() {
891        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
892        let err = load_adc().unwrap_err();
893        assert!(err.is_loading(), "{err:?}");
894        let msg = format!("{err:?}");
895        assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
896        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
897    }
898
899    #[test]
900    #[serial_test::serial]
901    fn load_adc_success() {
902        let file = tempfile::NamedTempFile::new().unwrap();
903        let path = file.into_temp_path();
904        std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
905        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
906
907        assert_eq!(
908            load_adc().unwrap(),
909            AdcContents::Contents("contents".to_string())
910        );
911    }
912
913    #[test_case(true; "retryable")]
914    #[test_case(false; "non-retryable")]
915    #[tokio::test]
916    async fn error_credentials(retryable: bool) {
917        let credentials = super::testing::error_credentials(retryable);
918        assert!(
919            credentials.universe_domain().await.is_none(),
920            "{credentials:?}"
921        );
922        let err = credentials.headers(Extensions::new()).await.err().unwrap();
923        assert_eq!(err.is_transient(), retryable, "{err:?}");
924        let err = credentials.headers(Extensions::new()).await.err().unwrap();
925        assert_eq!(err.is_transient(), retryable, "{err:?}");
926    }
927
928    #[tokio::test]
929    #[serial_test::serial]
930    async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
931        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
932        let _e2 = ScopedEnv::remove("HOME"); // For posix
933        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
934        let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
935
936        let mds = Builder::default()
937            .with_quota_project_id("test-quota-project")
938            .build()
939            .unwrap();
940        let fmt = format!("{mds:?}");
941        assert!(fmt.contains("MDSCredentials"));
942        assert!(
943            fmt.contains("env-quota-project"),
944            "Expected 'env-quota-project', got: {fmt}"
945        );
946    }
947
948    #[tokio::test]
949    #[serial_test::serial]
950    async fn create_access_token_credentials_with_quota_project_from_builder() {
951        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
952        let _e2 = ScopedEnv::remove("HOME"); // For posix
953        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
954        let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
955
956        let creds = Builder::default()
957            .with_quota_project_id("test-quota-project")
958            .build()
959            .unwrap();
960        let fmt = format!("{creds:?}");
961        assert!(
962            fmt.contains("test-quota-project"),
963            "Expected 'test-quota-project', got: {fmt}"
964        );
965    }
966
967    #[tokio::test]
968    #[serial_test::serial]
969    async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
970        let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
971        let mut service_account_key = serde_json::json!({
972            "type": "service_account",
973            "project_id": "test-project-id",
974            "private_key_id": "test-private-key-id",
975            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
976            "client_email": "test-client-email",
977            "universe_domain": "test-universe-domain"
978        });
979
980        let scopes =
981            ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
982
983        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
984
985        let file = tempfile::NamedTempFile::new().unwrap();
986        let path = file.into_temp_path();
987        std::fs::write(&path, service_account_key.to_string())
988            .expect("Unable to write to temporary file.");
989        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
990
991        let sac = Builder::default()
992            .with_quota_project_id("test-quota-project")
993            .with_scopes(scopes)
994            .build()
995            .unwrap();
996
997        let headers = sac.headers(Extensions::new()).await?;
998        let token = get_token_from_headers(headers).unwrap();
999        let parts: Vec<_> = token.split('.').collect();
1000        assert_eq!(parts.len(), 3);
1001        let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1002
1003        let fmt = format!("{sac:?}");
1004        assert!(fmt.contains("ServiceAccountCredentials"));
1005        assert!(fmt.contains("test-quota-project"));
1006        assert_eq!(claims["scope"], scopes.join(" "));
1007
1008        Ok(())
1009    }
1010}