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;
26#[doc(hidden)]
27pub mod external_account;
28pub(crate) mod external_account_sources;
29pub mod impersonated;
30pub(crate) mod internal;
31pub mod mds;
32pub mod service_account;
33pub mod 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#[derive(Debug, Clone)]
257enum CredentialsSource {
258    CredentialsJson(Value),
259    DefaultCredentials,
260}
261
262/// A builder for constructing [`Credentials`] instances.
263///
264/// By default (using [`Builder::default`]), the builder is configured to load
265/// credentials according to the standard [Application Default Credentials (ADC)][ADC-link]
266/// strategy. ADC is the recommended approach for most applications and conforms to
267/// [AIP-4110]. If you need to load credentials from a non-standard location or source,
268/// you can provide specific credential JSON directly using [`Builder::new`].
269///
270/// Common use cases where using ADC would is useful include:
271/// - Your application is deployed to a Google Cloud environment such as
272///   [Google Compute Engine (GCE)][gce-link],
273///   [Google Kubernetes Engine (GKE)][gke-link], or [Cloud Run]. Each of these
274///   deployment environments provides a default service account to the
275///   application, and offers mechanisms to change this default service account
276///   without any code changes to your application.
277/// - You are testing or developing the application on a workstation (physical or
278///   virtual). These credentials will use your preferences as set with
279///   [gcloud auth application-default]. These preferences can be your own Gooogle
280///   Cloud user credentials, or some service account.
281/// - Regardless of where your application is running, you can use the
282///   `GOOGLE_APPLICATION_CREDENTIALS` environment variable to override the
283///   defaults. This environment variable should point to a file containing a
284///   service account key file, or a JSON object describing your user
285///   credentials.
286///
287/// The headers returned by these credentials should be used in the
288/// Authorization HTTP header.
289///
290/// The Google Cloud client libraries for Rust will typically find and use these
291/// credentials automatically if a credentials file exists in the
292/// standard ADC search paths. You might instantiate these credentials either
293/// via ADC or a specific JSON file, if you need to:
294/// * Override the OAuth 2.0 **scopes** being requested for the access token.
295/// * Override the **quota project ID** for billing and quota management.
296///
297/// # Example: fetching headers using ADC
298/// ```
299/// # use google_cloud_auth::credentials::Builder;
300/// # use http::Extensions;
301/// # tokio_test::block_on(async {
302/// let credentials = Builder::default()
303///     .with_quota_project_id("my-project")
304///     .build()?;
305/// let headers = credentials.headers(Extensions::new()).await?;
306/// println!("Headers: {headers:?}");
307/// # Ok::<(), anyhow::Error>(())
308/// # });
309/// ```
310///
311/// # Example: fetching headers using custom JSON
312/// ```
313/// # use google_cloud_auth::credentials::Builder;
314/// # use http::Extensions;
315/// # tokio_test::block_on(async {
316/// # use google_cloud_auth::credentials::Builder;
317/// let authorized_user = serde_json::json!({
318///     "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com", // Replace with your actual Client ID
319///     "client_secret": "YOUR_CLIENT_SECRET", // Replace with your actual Client Secret - LOAD SECURELY!
320///     "refresh_token": "YOUR_REFRESH_TOKEN", // Replace with the user's refresh token - LOAD SECURELY!
321///     "type": "authorized_user",
322///     // "quota_project_id": "your-billing-project-id", // Optional: Set if needed
323///     // "token_uri" : "test-token-uri", // Optional: Set if needed
324/// });
325///
326/// let creds = Builder::new(authorized_user)
327///     .with_quota_project_id("my-project")
328///     .build()?;
329/// let headers = creds.headers(Extensions::new()).await?;
330/// println!("Headers: {headers:?}");
331/// # Ok::<(), anyhow::Error>(())
332/// # });
333/// ```
334///
335/// [ADC-link]: https://cloud.google.com/docs/authentication/application-default-credentials
336/// [AIP-4110]: https://google.aip.dev/auth/4110
337/// [Cloud Run]: https://cloud.google.com/run
338/// [gce-link]: https://cloud.google.com/products/compute
339/// [gcloud auth application-default]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default
340/// [gke-link]: https://cloud.google.com/kubernetes-engine
341#[derive(Debug)]
342pub struct Builder {
343    credentials_source: CredentialsSource,
344    quota_project_id: Option<String>,
345    scopes: Option<Vec<String>>,
346}
347
348impl Default for Builder {
349    /// Creates a new builder where credentials will be obtained via [application-default login].
350    ///
351    /// # Example
352    /// ```
353    /// # use google_cloud_auth::credentials::Builder;
354    /// # tokio_test::block_on(async {
355    /// let credentials = Builder::default().build();
356    /// # });
357    /// ```
358    ///
359    /// [application-default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
360    fn default() -> Self {
361        Self {
362            credentials_source: CredentialsSource::DefaultCredentials,
363            quota_project_id: None,
364            scopes: None,
365        }
366    }
367}
368
369impl Builder {
370    /// Creates a new builder with given credentials json.
371    ///
372    /// # Example
373    /// ```
374    /// # use google_cloud_auth::credentials::Builder;
375    /// let authorized_user = serde_json::json!({ /* add details here */ });
376    /// let credentials = Builder::new(authorized_user).build();
377    ///```
378    ///
379    pub fn new(json: serde_json::Value) -> Self {
380        Self {
381            credentials_source: CredentialsSource::CredentialsJson(json),
382            quota_project_id: None,
383            scopes: None,
384        }
385    }
386
387    /// Sets the [quota project] for these credentials.
388    ///
389    /// In some services, you can use an account in one project for authentication
390    /// and authorization, and charge the usage to a different project. This requires
391    /// that the user has `serviceusage.services.use` permissions on the quota project.
392    ///
393    /// ## Important: Precedence
394    /// If the `GOOGLE_CLOUD_QUOTA_PROJECT` environment variable is set,
395    /// its value will be used **instead of** the value provided to this method.
396    ///
397    /// # Example
398    /// ```
399    /// # use google_cloud_auth::credentials::Builder;
400    /// # tokio_test::block_on(async {
401    /// let credentials = Builder::default()
402    ///     .with_quota_project_id("my-project")
403    ///     .build();
404    /// # });
405    /// ```
406    ///
407    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
408    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
409        self.quota_project_id = Some(quota_project_id.into());
410        self
411    }
412
413    /// Sets the [scopes] for these credentials.
414    ///
415    /// `scopes` act as an additional restriction in addition to the IAM permissions
416    /// granted to the principal (user or service account) that creates the token.
417    ///
418    /// `scopes` define the *permissions being requested* for this specific access token
419    /// when interacting with a service. For example,
420    /// `https://www.googleapis.com/auth/devstorage.read_write`.
421    ///
422    /// IAM permissions, on the other hand, define the *underlying capabilities*
423    /// the principal possesses within a system. For example, `storage.buckets.delete`.
424    ///
425    /// The credentials certify that a particular token was created by a certain principal.
426    ///
427    /// When a token generated with specific scopes is used, the request must be permitted
428    /// by both the the principals's underlying IAM permissions and the scopes requested
429    /// for the token.
430    ///
431    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
432    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
433    where
434        I: IntoIterator<Item = S>,
435        S: Into<String>,
436    {
437        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
438        self
439    }
440
441    /// Returns a [Credentials] instance with the configured settings.
442    ///
443    /// # Errors
444    ///
445    /// Returns a [CredentialsError] if a unsupported credential type is provided
446    /// or if the `json` provided to [Builder::new] cannot be successfully deserialized
447    /// into the expected format. This typically happens if the JSON value is malformed
448    /// or missing required fields. For more information, on how to generate
449    /// json, consult the relevant section in the [application-default credentials] guide.
450    ///
451    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
452    pub fn build(self) -> BuildResult<Credentials> {
453        let json_data = match self.credentials_source {
454            CredentialsSource::CredentialsJson(json) => Some(json),
455            CredentialsSource::DefaultCredentials => match load_adc()? {
456                AdcContents::Contents(contents) => {
457                    Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
458                }
459                AdcContents::FallbackToMds => None,
460            },
461        };
462        let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
463            .ok()
464            .or(self.quota_project_id);
465        build_credentials(json_data, quota_project_id, self.scopes)
466    }
467}
468
469#[derive(Debug, PartialEq)]
470enum AdcPath {
471    FromEnv(String),
472    WellKnown(String),
473}
474
475#[derive(Debug, PartialEq)]
476enum AdcContents {
477    Contents(String),
478    FallbackToMds,
479}
480
481fn extract_credential_type(json: &Value) -> BuildResult<&str> {
482    json.get("type")
483        .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
484        .as_str()
485        .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
486}
487
488/// Applies common optional configurations (quota project ID, scopes) to a
489/// specific credential builder instance and then builds it.
490///
491/// This macro centralizes the logic for optionally calling `.with_quota_project_id()`
492/// and `.with_scopes()` on different underlying credential builders (like
493/// `mds::Builder`, `service_account::Builder`, etc.) before calling `.build()`.
494/// It helps avoid repetitive code in the `build_credentials` function.
495macro_rules! config_builder {
496    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
497        let builder = $builder_instance;
498        let builder = $quota_project_id_option
499            .into_iter()
500            .fold(builder, |b, qp| b.with_quota_project_id(qp));
501
502        let builder = $scopes_option
503            .into_iter()
504            .fold(builder, |b, s| $apply_scopes_closure(b, s));
505
506        builder.build()
507    }};
508}
509
510fn build_credentials(
511    json: Option<Value>,
512    quota_project_id: Option<String>,
513    scopes: Option<Vec<String>>,
514) -> BuildResult<Credentials> {
515    match json {
516        None => config_builder!(
517            mds::Builder::from_adc(),
518            quota_project_id,
519            scopes,
520            |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
521        ),
522        Some(json) => {
523            let cred_type = extract_credential_type(&json)?;
524            match cred_type {
525                "authorized_user" => {
526                    config_builder!(
527                        user_account::Builder::new(json),
528                        quota_project_id,
529                        scopes,
530                        |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
531                    )
532                }
533                "service_account" => config_builder!(
534                    service_account::Builder::new(json),
535                    quota_project_id,
536                    scopes,
537                    |b: service_account::Builder, s: Vec<String>| b
538                        .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
539                ),
540                "impersonated_service_account" => {
541                    config_builder!(
542                        impersonated::Builder::new(json),
543                        quota_project_id,
544                        scopes,
545                        |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
546                    )
547                }
548                "external_account" => config_builder!(
549                    external_account::Builder::new(json),
550                    quota_project_id,
551                    scopes,
552                    |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
553                ),
554                _ => Err(BuilderError::unknown_type(cred_type)),
555            }
556        }
557    }
558}
559
560fn path_not_found(path: String) -> BuilderError {
561    BuilderError::loading(format!(
562        "{path}. {}",
563        concat!(
564            "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
565            "environment variable. Verify this environment variable points to ",
566            "a valid file."
567        )
568    ))
569}
570
571fn load_adc() -> BuildResult<AdcContents> {
572    match adc_path() {
573        None => Ok(AdcContents::FallbackToMds),
574        Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
575            Ok(contents) => Ok(AdcContents::Contents(contents)),
576            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
577            Err(e) => Err(BuilderError::loading(e)),
578        },
579        Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
580            Ok(contents) => Ok(AdcContents::Contents(contents)),
581            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
582            Err(e) => Err(BuilderError::loading(e)),
583        },
584    }
585}
586
587/// The path to Application Default Credentials (ADC), as specified in [AIP-4110].
588///
589/// [AIP-4110]: https://google.aip.dev/auth/4110
590fn adc_path() -> Option<AdcPath> {
591    if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
592        return Some(AdcPath::FromEnv(path));
593    }
594    Some(AdcPath::WellKnown(adc_well_known_path()?))
595}
596
597/// The well-known path to ADC on Windows, as specified in [AIP-4113].
598///
599/// [AIP-4113]: https://google.aip.dev/auth/4113
600#[cfg(target_os = "windows")]
601fn adc_well_known_path() -> Option<String> {
602    std::env::var("APPDATA")
603        .ok()
604        .map(|root| root + "/gcloud/application_default_credentials.json")
605}
606
607/// The well-known path to ADC on Linux and Mac, as specified in [AIP-4113].
608///
609/// [AIP-4113]: https://google.aip.dev/auth/4113
610#[cfg(not(target_os = "windows"))]
611fn adc_well_known_path() -> Option<String> {
612    std::env::var("HOME")
613        .ok()
614        .map(|root| root + "/.config/gcloud/application_default_credentials.json")
615}
616
617/// A module providing invalid credentials where authentication does not matter.
618///
619/// These credentials are a convenient way to avoid errors from loading
620/// Application Default Credentials in tests.
621///
622/// This module is mainly relevant to other `google-cloud-*` crates, but some
623/// external developers (i.e. consumers, not developers of `google-cloud-rust`)
624/// may find it useful.
625// Skipping mutation testing for this module. As it exclusively provides
626// hardcoded credential stubs for testing purposes.
627#[cfg_attr(test, mutants::skip)]
628#[doc(hidden)]
629pub mod testing {
630    use super::{CacheableResource, EntityTag};
631    use crate::Result;
632    use crate::credentials::Credentials;
633    use crate::credentials::dynamic::CredentialsProvider;
634    use http::{Extensions, HeaderMap};
635    use std::sync::Arc;
636
637    /// A simple credentials implementation to use in tests where authentication does not matter.
638    ///
639    /// Always returns a "Bearer" token, with "test-only-token" as the value.
640    pub fn test_credentials() -> Credentials {
641        Credentials {
642            inner: Arc::from(TestCredentials {}),
643        }
644    }
645
646    #[derive(Debug)]
647    struct TestCredentials;
648
649    #[async_trait::async_trait]
650    impl CredentialsProvider for TestCredentials {
651        async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
652            Ok(CacheableResource::New {
653                entity_tag: EntityTag::default(),
654                data: HeaderMap::new(),
655            })
656        }
657
658        async fn universe_domain(&self) -> Option<String> {
659            None
660        }
661    }
662
663    /// A simple credentials implementation to use in tests.
664    ///
665    /// Always return an error in `headers()`.
666    pub fn error_credentials(retryable: bool) -> Credentials {
667        Credentials {
668            inner: Arc::from(ErrorCredentials(retryable)),
669        }
670    }
671
672    #[derive(Debug, Default)]
673    struct ErrorCredentials(bool);
674
675    #[async_trait::async_trait]
676    impl CredentialsProvider for ErrorCredentials {
677        async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
678            Err(super::CredentialsError::from_msg(self.0, "test-only"))
679        }
680
681        async fn universe_domain(&self) -> Option<String> {
682            None
683        }
684    }
685}
686
687#[cfg(test)]
688mod test {
689    use super::*;
690    use base64::Engine;
691    use num_bigint_dig::BigUint;
692    use reqwest::header::AUTHORIZATION;
693    use rsa::RsaPrivateKey;
694    use rsa::pkcs8::{EncodePrivateKey, LineEnding};
695    use scoped_env::ScopedEnv;
696    use std::sync::LazyLock;
697    use test_case::test_case;
698
699    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
700
701    pub(crate) fn get_headers_from_cache(
702        headers: CacheableResource<HeaderMap>,
703    ) -> Result<HeaderMap> {
704        match headers {
705            CacheableResource::New { data, .. } => Ok(data),
706            CacheableResource::NotModified => Err(CredentialsError::from_msg(
707                false,
708                "Expecting headers to be present",
709            )),
710        }
711    }
712
713    pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
714        match headers {
715            CacheableResource::New { data, .. } => data
716                .get(AUTHORIZATION)
717                .and_then(|token_value| token_value.to_str().ok())
718                .and_then(|s| s.split_whitespace().nth(1))
719                .map(|s| s.to_string()),
720            CacheableResource::NotModified => None,
721        }
722    }
723
724    pub(crate) fn get_token_type_from_headers(
725        headers: CacheableResource<HeaderMap>,
726    ) -> Option<String> {
727        match headers {
728            CacheableResource::New { data, .. } => data
729                .get(AUTHORIZATION)
730                .and_then(|token_value| token_value.to_str().ok())
731                .and_then(|s| s.split_whitespace().next())
732                .map(|s| s.to_string()),
733            CacheableResource::NotModified => None,
734        }
735    }
736
737    pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
738        let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
739        let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
740        let e_str: &str = "65537";
741
742        let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
743        let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
744        let public_exponent =
745            BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
746
747        RsaPrivateKey::from_primes(vec![p, q], public_exponent)
748            .expect("Failed to create RsaPrivateKey from primes")
749    });
750
751    pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
752        RSA_PRIVATE_KEY
753            .to_pkcs8_pem(LineEnding::LF)
754            .expect("Failed to encode key to PKCS#8 PEM")
755            .to_string()
756    });
757
758    pub fn b64_decode_to_json(s: String) -> serde_json::Value {
759        let decoded = String::from_utf8(
760            base64::engine::general_purpose::URL_SAFE_NO_PAD
761                .decode(s)
762                .unwrap(),
763        )
764        .unwrap();
765        serde_json::from_str(&decoded).unwrap()
766    }
767
768    #[cfg(target_os = "windows")]
769    #[test]
770    #[serial_test::serial]
771    fn adc_well_known_path_windows() {
772        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
773        let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
774        assert_eq!(
775            adc_well_known_path(),
776            Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
777        );
778        assert_eq!(
779            adc_path(),
780            Some(AdcPath::WellKnown(
781                "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
782            ))
783        );
784    }
785
786    #[cfg(target_os = "windows")]
787    #[test]
788    #[serial_test::serial]
789    fn adc_well_known_path_windows_no_appdata() {
790        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
791        let _appdata = ScopedEnv::remove("APPDATA");
792        assert_eq!(adc_well_known_path(), None);
793        assert_eq!(adc_path(), None);
794    }
795
796    #[cfg(not(target_os = "windows"))]
797    #[test]
798    #[serial_test::serial]
799    fn adc_well_known_path_posix() {
800        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
801        let _home = ScopedEnv::set("HOME", "/home/foo");
802        assert_eq!(
803            adc_well_known_path(),
804            Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
805        );
806        assert_eq!(
807            adc_path(),
808            Some(AdcPath::WellKnown(
809                "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
810            ))
811        );
812    }
813
814    #[cfg(not(target_os = "windows"))]
815    #[test]
816    #[serial_test::serial]
817    fn adc_well_known_path_posix_no_home() {
818        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
819        let _appdata = ScopedEnv::remove("HOME");
820        assert_eq!(adc_well_known_path(), None);
821        assert_eq!(adc_path(), None);
822    }
823
824    #[test]
825    #[serial_test::serial]
826    fn adc_path_from_env() {
827        let _creds = ScopedEnv::set(
828            "GOOGLE_APPLICATION_CREDENTIALS",
829            "/usr/bar/application_default_credentials.json",
830        );
831        assert_eq!(
832            adc_path(),
833            Some(AdcPath::FromEnv(
834                "/usr/bar/application_default_credentials.json".to_string()
835            ))
836        );
837    }
838
839    #[test]
840    #[serial_test::serial]
841    fn load_adc_no_well_known_path_fallback_to_mds() {
842        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
843        let _e2 = ScopedEnv::remove("HOME"); // For posix
844        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
845        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
846    }
847
848    #[test]
849    #[serial_test::serial]
850    fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
851        // Create a new temp directory. There is not an ADC file in here.
852        let dir = tempfile::TempDir::new().unwrap();
853        let path = dir.path().to_str().unwrap();
854        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
855        let _e2 = ScopedEnv::set("HOME", path); // For posix
856        let _e3 = ScopedEnv::set("APPDATA", path); // For windows
857        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
858    }
859
860    #[test]
861    #[serial_test::serial]
862    fn load_adc_no_file_at_env_is_error() {
863        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
864        let err = load_adc().unwrap_err();
865        assert!(err.is_loading(), "{err:?}");
866        let msg = format!("{err:?}");
867        assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
868        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
869    }
870
871    #[test]
872    #[serial_test::serial]
873    fn load_adc_success() {
874        let file = tempfile::NamedTempFile::new().unwrap();
875        let path = file.into_temp_path();
876        std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
877        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
878
879        assert_eq!(
880            load_adc().unwrap(),
881            AdcContents::Contents("contents".to_string())
882        );
883    }
884
885    #[test_case(true; "retryable")]
886    #[test_case(false; "non-retryable")]
887    #[tokio::test]
888    async fn error_credentials(retryable: bool) {
889        let credentials = super::testing::error_credentials(retryable);
890        assert!(
891            credentials.universe_domain().await.is_none(),
892            "{credentials:?}"
893        );
894        let err = credentials.headers(Extensions::new()).await.err().unwrap();
895        assert_eq!(err.is_transient(), retryable, "{err:?}");
896        let err = credentials.headers(Extensions::new()).await.err().unwrap();
897        assert_eq!(err.is_transient(), retryable, "{err:?}");
898    }
899
900    #[tokio::test]
901    #[serial_test::serial]
902    async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
903        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
904        let _e2 = ScopedEnv::remove("HOME"); // For posix
905        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
906        let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
907
908        let mds = Builder::default()
909            .with_quota_project_id("test-quota-project")
910            .build()
911            .unwrap();
912        let fmt = format!("{:?}", mds);
913        assert!(fmt.contains("MDSCredentials"));
914        assert!(
915            fmt.contains("env-quota-project"),
916            "Expected 'env-quota-project', got: {}",
917            fmt
918        );
919    }
920
921    #[tokio::test]
922    #[serial_test::serial]
923    async fn create_access_token_credentials_with_quota_project_from_builder() {
924        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
925        let _e2 = ScopedEnv::remove("HOME"); // For posix
926        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
927        let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
928
929        let creds = Builder::default()
930            .with_quota_project_id("test-quota-project")
931            .build()
932            .unwrap();
933        let fmt = format!("{:?}", creds);
934        assert!(
935            fmt.contains("test-quota-project"),
936            "Expected 'test-quota-project', got: {}",
937            fmt
938        );
939    }
940
941    #[tokio::test]
942    #[serial_test::serial]
943    async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
944        let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
945        let mut service_account_key = serde_json::json!({
946            "type": "service_account",
947            "project_id": "test-project-id",
948            "private_key_id": "test-private-key-id",
949            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
950            "client_email": "test-client-email",
951            "universe_domain": "test-universe-domain"
952        });
953
954        let scopes =
955            ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
956
957        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
958
959        let sac = Builder::new(service_account_key)
960            .with_quota_project_id("test-quota-project")
961            .with_scopes(scopes)
962            .build()
963            .unwrap();
964
965        let headers = sac.headers(Extensions::new()).await?;
966        let token = get_token_from_headers(headers).unwrap();
967        let parts: Vec<_> = token.split('.').collect();
968        assert_eq!(parts.len(), 3);
969        let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
970
971        let fmt = format!("{:?}", sac);
972        assert!(fmt.contains("ServiceAccountCredentials"));
973        assert!(fmt.contains("test-quota-project"));
974        assert_eq!(claims["scope"], scopes.join(" "));
975
976        Ok(())
977    }
978}