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