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}