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