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}