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
15pub mod api_key_credentials;
16pub mod mds;
17pub mod service_account;
18pub mod user_account;
19
20use crate::Result;
21use crate::errors::{self, CredentialsError};
22use http::{Extensions, HeaderMap};
23use serde_json::Value;
24use std::future::Future;
25use std::sync::Arc;
26
27pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
28pub(crate) const DEFAULT_UNIVERSE_DOMAIN: &str = "googleapis.com";
29
30/// An implementation of [crate::credentials::CredentialsProvider].
31///
32/// Represents a [Credentials] used to obtain the auth request headers.
33///
34/// In general, [Credentials][credentials-link] are "digital object that provide
35/// proof of identity", the archetype may be a username and password
36/// combination, but a private RSA key may be a better example.
37///
38/// Modern authentication protocols do not send the credentials to authenticate
39/// with a service. Even when sent over encrypted transports, the credentials
40/// may be accidentally exposed via logging or may be captured if there are
41/// errors in the transport encryption. Because the credentials are often
42/// long-lived, that risk of exposure is also long-lived.
43///
44/// Instead, modern authentication protocols exchange the credentials for a
45/// time-limited [Token][token-link], a digital object that shows the caller was
46/// in possession of the credentials. Because tokens are time limited, risk of
47/// misuse is also time limited. Tokens may be further restricted to only a
48/// certain subset of the RPCs in the service, or even to specific resources, or
49/// only when used from a given machine (virtual or not). Further limiting the
50/// risks associated with any leaks of these tokens.
51///
52/// This struct also abstracts token sources that are not backed by a specific
53/// digital object. The canonical example is the [Metadata Service]. This
54/// service is available in many Google Cloud environments, including
55/// [Google Compute Engine], and [Google Kubernetes Engine].
56///
57/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
58/// [token-link]: https://cloud.google.com/docs/authentication#token
59/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
60/// [Google Compute Engine]: https://cloud.google.com/products/compute
61/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
62#[derive(Clone, Debug)]
63pub struct Credentials {
64    // We use an `Arc` to hold the inner implementation.
65    //
66    // Credentials may be shared across threads (`Send + Sync`), so an `Rc`
67    // will not do.
68    //
69    // They also need to derive `Clone`, as the
70    // `gax::http_client::ReqwestClient`s which hold them derive `Clone`. So a
71    // `Box` will not do.
72    inner: Arc<dyn dynamic::CredentialsProvider>,
73}
74
75impl<T> std::convert::From<T> for Credentials
76where
77    T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
78{
79    fn from(value: T) -> Self {
80        Self {
81            inner: Arc::new(value),
82        }
83    }
84}
85
86impl Credentials {
87    pub async fn headers(&self, extensions: Extensions) -> Result<HeaderMap> {
88        self.inner.headers(extensions).await
89    }
90
91    pub async fn universe_domain(&self) -> Option<String> {
92        self.inner.universe_domain().await
93    }
94}
95
96/// Represents a [Credentials] used to obtain auth request headers.
97///
98/// In general, [Credentials][credentials-link] are "digital object that
99/// provide proof of identity", the archetype may be a username and password
100/// combination, but a private RSA key may be a better example.
101///
102/// Modern authentication protocols do not send the credentials to
103/// authenticate with a service. Even when sent over encrypted transports,
104/// the credentials may be accidentally exposed via logging or may be
105/// captured if there are errors in the transport encryption. Because the
106/// credentials are often long-lived, that risk of exposure is also
107/// long-lived.
108///
109/// Instead, modern authentication protocols exchange the credentials for a
110/// time-limited [Token][token-link], a digital object that shows the caller
111/// was in possession of the credentials. Because tokens are time limited,
112/// risk of misuse is also time limited. Tokens may be further restricted to
113/// only a certain subset of the RPCs in the service, or even to specific
114/// resources, or only when used from a given machine (virtual or not).
115/// Further limiting the risks associated with any leaks of these tokens.
116///
117/// This struct also abstracts token sources that are not backed by a
118/// specific digital object. The canonical example is the
119/// [Metadata Service]. This service is available in many Google Cloud
120/// environments, including [Google Compute Engine], and
121/// [Google Kubernetes Engine].
122///
123/// # Notes
124///
125/// Application developers who directly use the Auth SDK can use this trait,
126/// along with [crate::credentials::Credentials::from()] to mock the credentials.
127/// Application developers who use the Google Cloud Rust SDK directly should not
128/// need this functionality.
129///
130/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
131/// [token-link]: https://cloud.google.com/docs/authentication#token
132/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
133/// [Google Compute Engine]: https://cloud.google.com/products/compute
134/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
135pub trait CredentialsProvider: std::fmt::Debug {
136    /// Asynchronously constructs the auth headers.
137    ///
138    /// Different auth tokens are sent via different headers. The
139    /// [Credentials] constructs the headers (and header values) that should be
140    /// sent with a request.
141    ///
142    /// The underlying implementation refreshes the token as needed.
143    // TODO(#2036): After the return type is updated to return a cacheable resource
144    // update Rustdoc to fully explain the `extensions: http::Extensions` parameter.
145    fn headers(&self, extensions: Extensions) -> impl Future<Output = Result<HeaderMap>> + Send;
146
147    /// Retrieves the universe domain associated with the credentials, if any.
148    fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
149}
150
151pub(crate) mod dynamic {
152    use super::Result;
153    use super::{Extensions, HeaderMap};
154
155    /// A dyn-compatible, crate-private version of `CredentialsProvider`.
156    #[async_trait::async_trait]
157    pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
158        /// Asynchronously constructs the auth headers.
159        ///
160        /// Different auth tokens are sent via different headers. The
161        /// [Credentials] constructs the headers (and header values) that should be
162        /// sent with a request.
163        ///
164        /// The underlying implementation refreshes the token as needed.
165        // TODO(#2036): After the return type is updated to return cacheable resource
166        // update Rustdoc to fully explain the `extensions: Option<http::Extensions>` parameter.
167        async fn headers(&self, extensions: Extensions) -> Result<HeaderMap>;
168
169        /// Retrieves the universe domain associated with the credentials, if any.
170        async fn universe_domain(&self) -> Option<String> {
171            Some("googleapis.com".to_string())
172        }
173    }
174
175    /// The public CredentialsProvider implements the dyn-compatible CredentialsProvider.
176    #[async_trait::async_trait]
177    impl<T> CredentialsProvider for T
178    where
179        T: super::CredentialsProvider + Send + Sync,
180    {
181        async fn headers(&self, extensions: Extensions) -> Result<HeaderMap> {
182            T::headers(self, extensions).await
183        }
184        async fn universe_domain(&self) -> Option<String> {
185            T::universe_domain(self).await
186        }
187    }
188}
189
190#[derive(Debug, Clone)]
191enum CredentialsSource {
192    CredentialsJson(Value),
193    DefaultCredentials,
194}
195
196/// A builder for constructing [`Credentials`] instances.
197///
198/// By default (using [`Builder::default`]), the builder is configured to load
199/// credentials according to the standard [Application Default Credentials (ADC)][ADC-link]
200/// strategy. ADC is the recommended approach for most applications and conforms to
201/// [AIP-4110]. If you need to load credentials from a non-standard location or source,
202/// you can provide specific credential JSON directly using [`Builder::new`].
203///
204/// Common use cases where using ADC would is useful include:
205/// - Your application is deployed to a Google Cloud environment such as
206///   [Google Compute Engine (GCE)][gce-link],
207///   [Google Kubernetes Engine (GKE)][gke-link], or [Cloud Run]. Each of these
208///   deployment environments provides a default service account to the
209///   application, and offers mechanisms to change this default service account
210///   without any code changes to your application.
211/// - You are testing or developing the application on a workstation (physical or
212///   virtual). These credentials will use your preferences as set with
213///   [gcloud auth application-default]. These preferences can be your own Gooogle
214///   Cloud user credentials, or some service account.
215/// - Regardless of where your application is running, you can use the
216///   `GOOGLE_APPLICATION_CREDENTIALS` environment variable to override the
217///   defaults. This environment variable should point to a file containing a
218///   service account key file, or a JSON object describing your user
219///   credentials.
220///
221/// The headers returned by these credentials should be used in the
222/// Authorization HTTP header.
223///
224/// The Google Cloud client libraries for Rust will typically find and use these
225/// credentials automatically if a credentials file exists in the
226/// standard ADC search paths. You might instantiate these credentials either
227/// via ADC or a specific JSON file, if you need to:
228/// * Override the OAuth 2.0 **scopes** being requested for the access token.
229/// * Override the **quota project ID** for billing and quota management.
230///
231/// Example usage:
232///
233/// Fetching headers using ADC
234/// ```
235/// # use google_cloud_auth::credentials::Builder;
236/// # use google_cloud_auth::errors::CredentialsError;
237/// # use http::Extensions;
238/// # tokio_test::block_on(async {
239/// let credentials = Builder::default()
240///     .with_quota_project_id("my-project")
241///     .build()?;
242/// let headers = credentials.headers(Extensions::new()).await?;
243/// println!("Headers: {headers:?}");
244/// # Ok::<(), CredentialsError>(())
245/// # });
246/// ```
247///
248/// Fetching headers using custom JSON
249/// ```
250/// # use google_cloud_auth::credentials::Builder;
251/// # use google_cloud_auth::errors::CredentialsError;
252/// # use http::Extensions;
253/// # tokio_test::block_on(async {
254/// # use google_cloud_auth::credentials::Builder;
255/// let authorized_user = serde_json::json!({
256///     "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com", // Replace with your actual Client ID
257///     "client_secret": "YOUR_CLIENT_SECRET", // Replace with your actual Client Secret - LOAD SECURELY!
258///     "refresh_token": "YOUR_REFRESH_TOKEN", // Replace with the user's refresh token - LOAD SECURELY!
259///     "type": "authorized_user",
260///     // "quota_project_id": "your-billing-project-id", // Optional: Set if needed
261///     // "token_uri" : "test-token-uri", // Optional: Set if needed
262/// });
263///
264/// let creds = Builder::new(authorized_user)
265///     .with_quota_project_id("my-project")
266///     .build()?;
267/// let headers = creds.headers(Extensions::new()).await?;
268/// println!("Headers: {headers:?}");
269/// # Ok::<(), CredentialsError>(())
270/// # });
271/// ```
272///
273/// [ADC-link]: https://cloud.google.com/docs/authentication/application-default-credentials
274/// [AIP-4110]: https://google.aip.dev/auth/4110
275/// [Cloud Run]: https://cloud.google.com/run
276/// [gce-link]: https://cloud.google.com/products/compute
277/// [gcloud auth application-default]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default
278/// [gke-link]: https://cloud.google.com/kubernetes-engine
279#[derive(Debug)]
280pub struct Builder {
281    credentials_source: CredentialsSource,
282    quota_project_id: Option<String>,
283    scopes: Option<Vec<String>>,
284}
285
286impl Default for Builder {
287    /// Creates a new builder where credentials will be obtained via [application-default login].
288    ///
289    /// # Example
290    /// ```
291    /// # use google_cloud_auth::credentials::Builder;
292    /// # tokio_test::block_on(async {
293    /// let credentials = Builder::default().build();
294    /// # });
295    /// ```
296    ///
297    /// [application-default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
298    fn default() -> Self {
299        Self {
300            credentials_source: CredentialsSource::DefaultCredentials,
301            quota_project_id: None,
302            scopes: None,
303        }
304    }
305}
306
307impl Builder {
308    /// Creates a new builder with given credentials json.
309    ///
310    /// # Example
311    /// ```
312    /// # use google_cloud_auth::credentials::Builder;
313    /// let authorized_user = serde_json::json!({ /* add details here */ });
314    /// let credentials = Builder::new(authorized_user).build();
315    ///```
316    ///
317    pub fn new(json: serde_json::Value) -> Self {
318        Self {
319            credentials_source: CredentialsSource::CredentialsJson(json),
320            quota_project_id: None,
321            scopes: None,
322        }
323    }
324
325    /// Sets the [quota project] for these credentials.
326    ///
327    /// In some services, you can use an account in one project for authentication
328    /// and authorization, and charge the usage to a different project. This requires
329    /// that the user has `serviceusage.services.use` permissions on the quota project.
330    ///
331    /// # Example
332    /// ```
333    /// # use google_cloud_auth::credentials::Builder;
334    /// # tokio_test::block_on(async {
335    /// let credentials = Builder::default()
336    ///     .with_quota_project_id("my-project")
337    ///     .build();
338    /// # });
339    /// ```
340    ///
341    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
342    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
343        self.quota_project_id = Some(quota_project_id.into());
344        self
345    }
346
347    /// Sets the [scopes] for these credentials.
348    ///
349    /// `scopes` act as an additional restriction in addition to the IAM permissions
350    /// granted to the principal (user or service account) that creates the token.
351    ///
352    /// `scopes` define the *permissions being requested* for this specific access token
353    /// when interacting with a service. For example,
354    /// `https://www.googleapis.com/auth/devstorage.read_write`.
355    ///
356    /// IAM permissions, on the other hand, define the *underlying capabilities*
357    /// the principal possesses within a system. For example, `storage.buckets.delete`.
358    ///
359    /// The credentials certify that a particular token was created by a certain principal.
360    ///
361    /// When a token generated with specific scopes is used, the request must be permitted
362    /// by both the the principals's underlying IAM permissions and the scopes requested
363    /// for the token.
364    ///
365    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
366    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
367    where
368        I: IntoIterator<Item = S>,
369        S: Into<String>,
370    {
371        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
372        self
373    }
374
375    /// Returns a [Credentials] instance with the configured settings.
376    ///
377    /// # Errors
378    ///
379    /// Returns a [CredentialsError] if a unsupported credential type is provided
380    /// or if the `json` provided to [Builder::new] cannot be successfully deserialized
381    /// into the expected format. This typically happens if the JSON value is malformed
382    /// or missing required fields. For more information, on how to generate
383    /// json, consult the relevant section in the [application-default credentials] guide.
384    ///
385    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
386    pub fn build(self) -> Result<Credentials> {
387        let json_data = match self.credentials_source {
388            CredentialsSource::CredentialsJson(json) => Some(json),
389            CredentialsSource::DefaultCredentials => match load_adc()? {
390                AdcContents::Contents(contents) => {
391                    Some(serde_json::from_str(&contents).map_err(errors::non_retryable)?)
392                }
393                AdcContents::FallbackToMds => None,
394            },
395        };
396        build_credentials(json_data, self.quota_project_id, self.scopes)
397    }
398}
399
400#[derive(Debug, PartialEq)]
401enum AdcPath {
402    FromEnv(String),
403    WellKnown(String),
404}
405
406#[derive(Debug, PartialEq)]
407enum AdcContents {
408    Contents(String),
409    FallbackToMds,
410}
411
412fn extract_credential_type(json: &Value) -> Result<&str> {
413    json.get("type")
414        .ok_or_else(|| {
415            errors::non_retryable_from_str(
416                "Failed to parse Credentials JSON. No `type` field found.",
417            )
418        })?
419        .as_str()
420        .ok_or_else(|| {
421            errors::non_retryable_from_str(
422                "Failed to parse Credentials JSON. `type` field is not a string.",
423            )
424        })
425}
426
427/// Applies common optional configurations (quota project ID, scopes) to a
428/// specific credential builder instance and then builds it.
429///
430/// This macro centralizes the logic for optionally calling `.with_quota_project_id()`
431/// and `.with_scopes()` on different underlying credential builders (like
432/// `mds::Builder`, `service_account::Builder`, etc.) before calling `.build()`.
433/// It helps avoid repetitive code in the `build_credentials` function.
434macro_rules! config_builder {
435    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
436        let builder = $builder_instance;
437        let builder = $quota_project_id_option
438            .into_iter()
439            .fold(builder, |b, qp| b.with_quota_project_id(qp));
440
441        let builder = $scopes_option
442            .into_iter()
443            .fold(builder, |b, s| $apply_scopes_closure(b, s));
444
445        builder.build()
446    }};
447}
448
449fn build_credentials(
450    json: Option<Value>,
451    quota_project_id: Option<String>,
452    scopes: Option<Vec<String>>,
453) -> Result<Credentials> {
454    match json {
455        None => config_builder!(
456            mds::Builder::default(),
457            quota_project_id,
458            scopes,
459            |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
460        ),
461        Some(json) => {
462            let cred_type = extract_credential_type(&json)?;
463            match cred_type {
464                "authorized_user" => {
465                    config_builder!(
466                        user_account::Builder::new(json),
467                        quota_project_id,
468                        scopes,
469                        |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
470                    )
471                }
472                "service_account" => config_builder!(
473                    service_account::Builder::new(json),
474                    quota_project_id,
475                    scopes,
476                    |b: service_account::Builder, s: Vec<String>| b
477                        .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
478                ),
479                _ => Err(errors::non_retryable_from_str(format!(
480                    "Invalid or unsupported credentials type found in JSON: {cred_type}"
481                ))),
482            }
483        }
484    }
485}
486
487fn path_not_found(path: String) -> CredentialsError {
488    errors::non_retryable_from_str(format!(
489        "Failed to load Application Default Credentials (ADC) from {path}. Check that the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid file."
490    ))
491}
492
493fn load_adc() -> Result<AdcContents> {
494    match adc_path() {
495        None => Ok(AdcContents::FallbackToMds),
496        Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
497            Ok(contents) => Ok(AdcContents::Contents(contents)),
498            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
499            Err(e) => Err(errors::non_retryable(e)),
500        },
501        Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
502            Ok(contents) => Ok(AdcContents::Contents(contents)),
503            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
504            Err(e) => Err(errors::non_retryable(e)),
505        },
506    }
507}
508
509/// The path to Application Default Credentials (ADC), as specified in [AIP-4110].
510///
511/// [AIP-4110]: https://google.aip.dev/auth/4110
512fn adc_path() -> Option<AdcPath> {
513    if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
514        return Some(AdcPath::FromEnv(path));
515    }
516    Some(AdcPath::WellKnown(adc_well_known_path()?))
517}
518
519/// The well-known path to ADC on Windows, as specified in [AIP-4113].
520///
521/// [AIP-4113]: https://google.aip.dev/auth/4113
522#[cfg(target_os = "windows")]
523fn adc_well_known_path() -> Option<String> {
524    std::env::var("APPDATA")
525        .ok()
526        .map(|root| root + "/gcloud/application_default_credentials.json")
527}
528
529/// The well-known path to ADC on Linux and Mac, as specified in [AIP-4113].
530///
531/// [AIP-4113]: https://google.aip.dev/auth/4113
532#[cfg(not(target_os = "windows"))]
533fn adc_well_known_path() -> Option<String> {
534    std::env::var("HOME")
535        .ok()
536        .map(|root| root + "/.config/gcloud/application_default_credentials.json")
537}
538
539/// A module providing invalid credentials where authentication does not matter.
540///
541/// These credentials are a convenient way to avoid errors from loading
542/// Application Default Credentials in tests.
543///
544/// This module is mainly relevant to other `google-cloud-*` crates, but some
545/// external developers (i.e. consumers, not developers of `google-cloud-rust`)
546/// may find it useful.
547// Skipping mutation testing for this module. As it exclusively provides
548// hardcoded credential stubs for testing purposes.
549#[cfg_attr(test, mutants::skip)]
550#[doc(hidden)]
551pub mod testing {
552    use crate::Result;
553    use crate::credentials::Credentials;
554    use crate::credentials::dynamic::CredentialsProvider;
555    use http::{Extensions, HeaderMap};
556    use std::sync::Arc;
557
558    /// A simple credentials implementation to use in tests where authentication does not matter.
559    ///
560    /// Always returns a "Bearer" token, with "test-only-token" as the value.
561    pub fn test_credentials() -> Credentials {
562        Credentials {
563            inner: Arc::from(TestCredentials {}),
564        }
565    }
566
567    #[derive(Debug)]
568    struct TestCredentials;
569
570    #[async_trait::async_trait]
571    impl CredentialsProvider for TestCredentials {
572        async fn headers(&self, _extensions: Extensions) -> Result<HeaderMap> {
573            Ok(HeaderMap::new())
574        }
575
576        async fn universe_domain(&self) -> Option<String> {
577            None
578        }
579    }
580
581    /// A simple credentials implementation to use in tests.
582    ///
583    /// Always return an error in `headers()`.
584    pub fn error_credentials(retryable: bool) -> Credentials {
585        Credentials {
586            inner: Arc::from(ErrorCredentials(retryable)),
587        }
588    }
589
590    #[derive(Debug, Default)]
591    struct ErrorCredentials(bool);
592
593    #[async_trait::async_trait]
594    impl CredentialsProvider for ErrorCredentials {
595        async fn headers(&self, _extensions: Extensions) -> Result<HeaderMap> {
596            Err(super::CredentialsError::from_str(self.0, "test-only"))
597        }
598
599        async fn universe_domain(&self) -> Option<String> {
600            None
601        }
602    }
603}
604
605#[cfg(test)]
606mod test {
607    use super::*;
608    use base64::Engine;
609    use reqwest::header::AUTHORIZATION;
610    use rsa::RsaPrivateKey;
611    use rsa::pkcs8::{EncodePrivateKey, LineEnding};
612    use scoped_env::ScopedEnv;
613    use std::error::Error;
614    use std::sync::LazyLock;
615    use test_case::test_case;
616
617    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
618
619    pub(crate) fn get_token_from_headers(headers: &HeaderMap) -> Option<String> {
620        headers
621            .get(AUTHORIZATION)
622            .and_then(|token_value| token_value.to_str().ok())
623            .and_then(|s| s.split_whitespace().nth(1))
624            .map(|s| s.to_string())
625    }
626
627    pub(crate) fn get_token_type_from_headers(headers: &HeaderMap) -> Option<String> {
628        headers
629            .get(AUTHORIZATION)
630            .and_then(|token_value| token_value.to_str().ok())
631            .and_then(|s| s.split_whitespace().next())
632            .map(|s| s.to_string())
633    }
634
635    pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
636        let mut rng = rand::thread_rng();
637        let bits = 2048;
638        let priv_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key");
639        priv_key
640            .to_pkcs8_pem(LineEnding::LF)
641            .expect("Failed to encode key to PKCS#8 PEM")
642            .to_string()
643    });
644
645    pub fn b64_decode_to_json(s: String) -> serde_json::Value {
646        let decoded = String::from_utf8(
647            base64::engine::general_purpose::URL_SAFE_NO_PAD
648                .decode(s)
649                .unwrap(),
650        )
651        .unwrap();
652        serde_json::from_str(&decoded).unwrap()
653    }
654
655    #[cfg(target_os = "windows")]
656    #[test]
657    #[serial_test::serial]
658    fn adc_well_known_path_windows() {
659        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
660        let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
661        assert_eq!(
662            adc_well_known_path(),
663            Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
664        );
665        assert_eq!(
666            adc_path(),
667            Some(AdcPath::WellKnown(
668                "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
669            ))
670        );
671    }
672
673    #[cfg(target_os = "windows")]
674    #[test]
675    #[serial_test::serial]
676    fn adc_well_known_path_windows_no_appdata() {
677        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
678        let _appdata = ScopedEnv::remove("APPDATA");
679        assert_eq!(adc_well_known_path(), None);
680        assert_eq!(adc_path(), None);
681    }
682
683    #[cfg(not(target_os = "windows"))]
684    #[test]
685    #[serial_test::serial]
686    fn adc_well_known_path_posix() {
687        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
688        let _home = ScopedEnv::set("HOME", "/home/foo");
689        assert_eq!(
690            adc_well_known_path(),
691            Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
692        );
693        assert_eq!(
694            adc_path(),
695            Some(AdcPath::WellKnown(
696                "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
697            ))
698        );
699    }
700
701    #[cfg(not(target_os = "windows"))]
702    #[test]
703    #[serial_test::serial]
704    fn adc_well_known_path_posix_no_home() {
705        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
706        let _appdata = ScopedEnv::remove("HOME");
707        assert_eq!(adc_well_known_path(), None);
708        assert_eq!(adc_path(), None);
709    }
710
711    #[test]
712    #[serial_test::serial]
713    fn adc_path_from_env() {
714        let _creds = ScopedEnv::set(
715            "GOOGLE_APPLICATION_CREDENTIALS",
716            "/usr/bar/application_default_credentials.json",
717        );
718        assert_eq!(
719            adc_path(),
720            Some(AdcPath::FromEnv(
721                "/usr/bar/application_default_credentials.json".to_string()
722            ))
723        );
724    }
725
726    #[test]
727    #[serial_test::serial]
728    fn load_adc_no_well_known_path_fallback_to_mds() {
729        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
730        let _e2 = ScopedEnv::remove("HOME"); // For posix
731        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
732        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
733    }
734
735    #[test]
736    #[serial_test::serial]
737    fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
738        // Create a new temp directory. There is not an ADC file in here.
739        let dir = tempfile::TempDir::new().unwrap();
740        let path = dir.path().to_str().unwrap();
741        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
742        let _e2 = ScopedEnv::set("HOME", path); // For posix
743        let _e3 = ScopedEnv::set("APPDATA", path); // For windows
744        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
745    }
746
747    #[test]
748    #[serial_test::serial]
749    fn load_adc_no_file_at_env_is_error() {
750        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
751        let err = load_adc().err().unwrap();
752        let msg = err.source().unwrap().to_string();
753        assert!(msg.contains("Failed to load Application Default Credentials"));
754        assert!(msg.contains("file-does-not-exist.json"));
755        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"));
756    }
757
758    #[test]
759    #[serial_test::serial]
760    fn load_adc_success() {
761        let file = tempfile::NamedTempFile::new().unwrap();
762        let path = file.into_temp_path();
763        std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
764        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
765
766        assert_eq!(
767            load_adc().unwrap(),
768            AdcContents::Contents("contents".to_string())
769        );
770    }
771
772    #[test_case(true; "retryable")]
773    #[test_case(false; "non-retryable")]
774    #[tokio::test]
775    async fn error_credentials(retryable: bool) {
776        let credentials = super::testing::error_credentials(retryable);
777        assert!(
778            credentials.universe_domain().await.is_none(),
779            "{credentials:?}"
780        );
781        let err = credentials.headers(Extensions::new()).await.err().unwrap();
782        assert_eq!(err.is_retryable(), retryable, "{err:?}");
783        let err = credentials.headers(Extensions::new()).await.err().unwrap();
784        assert_eq!(err.is_retryable(), retryable, "{err:?}");
785    }
786
787    #[tokio::test]
788    #[serial_test::serial]
789    async fn create_access_token_credentials_fallback_to_mds_with_quota_project() {
790        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
791        let _e2 = ScopedEnv::remove("HOME"); // For posix
792        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
793
794        let mds = Builder::default()
795            .with_quota_project_id("test-quota-project")
796            .build()
797            .unwrap();
798        let fmt = format!("{:?}", mds);
799        assert!(fmt.contains("MDSCredentials"));
800        assert!(fmt.contains("test-quota-project"));
801    }
802
803    #[tokio::test]
804    async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
805        let mut service_account_key = serde_json::json!({
806            "type": "service_account",
807            "project_id": "test-project-id",
808            "private_key_id": "test-private-key-id",
809            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
810            "client_email": "test-client-email",
811            "universe_domain": "test-universe-domain"
812        });
813
814        let scopes =
815            ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
816
817        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
818
819        let sac = Builder::new(service_account_key)
820            .with_quota_project_id("test-quota-project")
821            .with_scopes(scopes)
822            .build()
823            .unwrap();
824
825        let headers = sac.headers(Extensions::new()).await?;
826        let token = get_token_from_headers(&headers).unwrap();
827        let parts: Vec<_> = token.split('.').collect();
828        assert_eq!(parts.len(), 3);
829        let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
830
831        let fmt = format!("{:?}", sac);
832        assert!(fmt.contains("ServiceAccountCredentials"));
833        assert!(fmt.contains("test-quota-project"));
834        assert_eq!(claims["scope"], scopes.join(" "));
835
836        Ok(())
837    }
838}