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)]
634pub(crate) mod 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    pub(crate) fn find_source_error<'a, T: Error + 'static>(
653        error: &'a (dyn Error + 'static),
654    ) -> Option<&'a T> {
655        let mut source = error.source();
656        while let Some(err) = source {
657            if let Some(target_err) = err.downcast_ref::<T>() {
658                return Some(target_err);
659            }
660            source = err.source();
661        }
662        None
663    }
664
665    mock! {
666        #[derive(Debug)]
667        pub RetryPolicy {}
668        impl RetryPolicy for RetryPolicy {
669            fn on_error(
670                &self,
671                loop_start: std::time::Instant,
672                attempt_count: u32,
673                idempotent: bool,
674                error: gax::error::Error,
675            ) -> RetryResult;
676        }
677    }
678
679    mock! {
680        #[derive(Debug)]
681        pub BackoffPolicy {}
682        impl BackoffPolicy for BackoffPolicy {
683            fn on_failure(
684                &self,
685                loop_start: std::time::Instant,
686                attempt_count: u32,
687            ) -> std::time::Duration;
688        }
689    }
690
691    mockall::mock! {
692        #[derive(Debug)]
693        pub RetryThrottler {}
694        impl RetryThrottler for RetryThrottler {
695            fn throttle_retry_attempt(&self) -> bool;
696            fn on_retry_failure(&mut self, error: &RetryResult);
697            fn on_success(&mut self);
698        }
699    }
700
701    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
702
703    pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
704        let mut retry_policy = MockRetryPolicy::new();
705        retry_policy
706            .expect_on_error()
707            .returning(move |_, attempt_count, _, error| {
708                if attempt_count >= attempts as u32 {
709                    return RetryResult::Exhausted(error);
710                }
711                let is_transient = error
712                    .source()
713                    .and_then(|e| e.downcast_ref::<CredentialsError>())
714                    .is_some_and(|ce| ce.is_transient());
715                if is_transient {
716                    RetryResult::Continue(error)
717                } else {
718                    RetryResult::Permanent(error)
719                }
720            });
721        retry_policy
722    }
723
724    pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
725        let mut backoff_policy = MockBackoffPolicy::new();
726        backoff_policy
727            .expect_on_failure()
728            .return_const(Duration::from_secs(0));
729        backoff_policy
730    }
731
732    pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
733        let mut throttler = MockRetryThrottler::new();
734        throttler.expect_on_retry_failure().return_const(());
735        throttler
736            .expect_throttle_retry_attempt()
737            .return_const(false);
738        throttler.expect_on_success().return_const(());
739        throttler
740    }
741
742    pub(crate) fn get_headers_from_cache(
743        headers: CacheableResource<HeaderMap>,
744    ) -> Result<HeaderMap> {
745        match headers {
746            CacheableResource::New { data, .. } => Ok(data),
747            CacheableResource::NotModified => Err(CredentialsError::from_msg(
748                false,
749                "Expecting headers to be present",
750            )),
751        }
752    }
753
754    pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> 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().nth(1))
760                .map(|s| s.to_string()),
761            CacheableResource::NotModified => None,
762        }
763    }
764
765    pub(crate) fn get_token_type_from_headers(
766        headers: CacheableResource<HeaderMap>,
767    ) -> Option<String> {
768        match headers {
769            CacheableResource::New { data, .. } => data
770                .get(AUTHORIZATION)
771                .and_then(|token_value| token_value.to_str().ok())
772                .and_then(|s| s.split_whitespace().next())
773                .map(|s| s.to_string()),
774            CacheableResource::NotModified => None,
775        }
776    }
777
778    pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
779        let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
780        let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
781        let e_str: &str = "65537";
782
783        let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
784        let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
785        let public_exponent =
786            BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
787
788        RsaPrivateKey::from_primes(vec![p, q], public_exponent)
789            .expect("Failed to create RsaPrivateKey from primes")
790    });
791
792    pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
793        RSA_PRIVATE_KEY
794            .to_pkcs8_pem(LineEnding::LF)
795            .expect("Failed to encode key to PKCS#8 PEM")
796            .to_string()
797    });
798
799    pub fn b64_decode_to_json(s: String) -> serde_json::Value {
800        let decoded = String::from_utf8(
801            base64::engine::general_purpose::URL_SAFE_NO_PAD
802                .decode(s)
803                .unwrap(),
804        )
805        .unwrap();
806        serde_json::from_str(&decoded).unwrap()
807    }
808
809    #[cfg(target_os = "windows")]
810    #[test]
811    #[serial_test::serial]
812    fn adc_well_known_path_windows() {
813        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
814        let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
815        assert_eq!(
816            adc_well_known_path(),
817            Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
818        );
819        assert_eq!(
820            adc_path(),
821            Some(AdcPath::WellKnown(
822                "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
823            ))
824        );
825    }
826
827    #[cfg(target_os = "windows")]
828    #[test]
829    #[serial_test::serial]
830    fn adc_well_known_path_windows_no_appdata() {
831        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
832        let _appdata = ScopedEnv::remove("APPDATA");
833        assert_eq!(adc_well_known_path(), None);
834        assert_eq!(adc_path(), None);
835    }
836
837    #[cfg(not(target_os = "windows"))]
838    #[test]
839    #[serial_test::serial]
840    fn adc_well_known_path_posix() {
841        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
842        let _home = ScopedEnv::set("HOME", "/home/foo");
843        assert_eq!(
844            adc_well_known_path(),
845            Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
846        );
847        assert_eq!(
848            adc_path(),
849            Some(AdcPath::WellKnown(
850                "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
851            ))
852        );
853    }
854
855    #[cfg(not(target_os = "windows"))]
856    #[test]
857    #[serial_test::serial]
858    fn adc_well_known_path_posix_no_home() {
859        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
860        let _appdata = ScopedEnv::remove("HOME");
861        assert_eq!(adc_well_known_path(), None);
862        assert_eq!(adc_path(), None);
863    }
864
865    #[test]
866    #[serial_test::serial]
867    fn adc_path_from_env() {
868        let _creds = ScopedEnv::set(
869            "GOOGLE_APPLICATION_CREDENTIALS",
870            "/usr/bar/application_default_credentials.json",
871        );
872        assert_eq!(
873            adc_path(),
874            Some(AdcPath::FromEnv(
875                "/usr/bar/application_default_credentials.json".to_string()
876            ))
877        );
878    }
879
880    #[test]
881    #[serial_test::serial]
882    fn load_adc_no_well_known_path_fallback_to_mds() {
883        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
884        let _e2 = ScopedEnv::remove("HOME"); // For posix
885        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
886        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
887    }
888
889    #[test]
890    #[serial_test::serial]
891    fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
892        // Create a new temp directory. There is not an ADC file in here.
893        let dir = tempfile::TempDir::new().unwrap();
894        let path = dir.path().to_str().unwrap();
895        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
896        let _e2 = ScopedEnv::set("HOME", path); // For posix
897        let _e3 = ScopedEnv::set("APPDATA", path); // For windows
898        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
899    }
900
901    #[test]
902    #[serial_test::serial]
903    fn load_adc_no_file_at_env_is_error() {
904        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
905        let err = load_adc().unwrap_err();
906        assert!(err.is_loading(), "{err:?}");
907        let msg = format!("{err:?}");
908        assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
909        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
910    }
911
912    #[test]
913    #[serial_test::serial]
914    fn load_adc_success() {
915        let file = tempfile::NamedTempFile::new().unwrap();
916        let path = file.into_temp_path();
917        std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
918        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
919
920        assert_eq!(
921            load_adc().unwrap(),
922            AdcContents::Contents("contents".to_string())
923        );
924    }
925
926    #[test_case(true; "retryable")]
927    #[test_case(false; "non-retryable")]
928    #[tokio::test]
929    async fn error_credentials(retryable: bool) {
930        let credentials = super::testing::error_credentials(retryable);
931        assert!(
932            credentials.universe_domain().await.is_none(),
933            "{credentials:?}"
934        );
935        let err = credentials.headers(Extensions::new()).await.err().unwrap();
936        assert_eq!(err.is_transient(), retryable, "{err:?}");
937        let err = credentials.headers(Extensions::new()).await.err().unwrap();
938        assert_eq!(err.is_transient(), retryable, "{err:?}");
939    }
940
941    #[tokio::test]
942    #[serial_test::serial]
943    async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
944        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
945        let _e2 = ScopedEnv::remove("HOME"); // For posix
946        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
947        let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
948
949        let mds = Builder::default()
950            .with_quota_project_id("test-quota-project")
951            .build()
952            .unwrap();
953        let fmt = format!("{mds:?}");
954        assert!(fmt.contains("MDSCredentials"));
955        assert!(
956            fmt.contains("env-quota-project"),
957            "Expected 'env-quota-project', got: {fmt}"
958        );
959    }
960
961    #[tokio::test]
962    #[serial_test::serial]
963    async fn create_access_token_credentials_with_quota_project_from_builder() {
964        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
965        let _e2 = ScopedEnv::remove("HOME"); // For posix
966        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
967        let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
968
969        let creds = Builder::default()
970            .with_quota_project_id("test-quota-project")
971            .build()
972            .unwrap();
973        let fmt = format!("{creds:?}");
974        assert!(
975            fmt.contains("test-quota-project"),
976            "Expected 'test-quota-project', got: {fmt}"
977        );
978    }
979
980    #[tokio::test]
981    #[serial_test::serial]
982    async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
983        let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
984        let mut service_account_key = serde_json::json!({
985            "type": "service_account",
986            "project_id": "test-project-id",
987            "private_key_id": "test-private-key-id",
988            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
989            "client_email": "test-client-email",
990            "universe_domain": "test-universe-domain"
991        });
992
993        let scopes =
994            ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
995
996        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
997
998        let file = tempfile::NamedTempFile::new().unwrap();
999        let path = file.into_temp_path();
1000        std::fs::write(&path, service_account_key.to_string())
1001            .expect("Unable to write to temporary file.");
1002        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1003
1004        let sac = Builder::default()
1005            .with_quota_project_id("test-quota-project")
1006            .with_scopes(scopes)
1007            .build()
1008            .unwrap();
1009
1010        let headers = sac.headers(Extensions::new()).await?;
1011        let token = get_token_from_headers(headers).unwrap();
1012        let parts: Vec<_> = token.split('.').collect();
1013        assert_eq!(parts.len(), 3);
1014        let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1015
1016        let fmt = format!("{sac:?}");
1017        assert!(fmt.contains("ServiceAccountCredentials"));
1018        assert!(fmt.contains("test-quota-project"));
1019        assert_eq!(claims["scope"], scopes.join(" "));
1020
1021        Ok(())
1022    }
1023}