1use crate::build_errors::Error as BuilderError;
20use crate::constants::GOOGLE_CLOUD_QUOTA_PROJECT_VAR;
21use crate::errors::{self, CredentialsError};
22use crate::token::Token;
23use crate::{BuildResult, Result};
24use http::{Extensions, HeaderMap};
25use serde_json::Value;
26use std::future::Future;
27use std::sync::Arc;
28use std::sync::atomic::{AtomicU64, Ordering};
29pub mod anonymous;
30pub mod api_key_credentials;
31pub mod external_account;
32pub(crate) mod external_account_sources;
33#[cfg(feature = "idtoken")]
34pub mod idtoken;
35pub mod impersonated;
36pub(crate) mod internal;
37pub mod mds;
38pub mod service_account;
39pub mod subject_token;
40pub mod user_account;
41pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
42
43#[cfg(test)]
44pub(crate) const DEFAULT_UNIVERSE_DOMAIN: &str = "googleapis.com";
45
46#[derive(Clone, Debug, PartialEq, Default)]
56pub struct EntityTag(u64);
57
58static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
59impl EntityTag {
60 pub fn new() -> Self {
61 let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
62 Self(value)
63 }
64}
65
66#[derive(Clone, PartialEq, Debug)]
73pub enum CacheableResource<T> {
74 NotModified,
75 New { entity_tag: EntityTag, data: T },
76}
77
78#[derive(Clone, Debug)]
111pub struct Credentials {
112 inner: Arc<dyn dynamic::CredentialsProvider>,
121}
122
123impl<T> std::convert::From<T> for Credentials
124where
125 T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
126{
127 fn from(value: T) -> Self {
128 Self {
129 inner: Arc::new(value),
130 }
131 }
132}
133
134impl Credentials {
135 pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
136 self.inner.headers(extensions).await
137 }
138
139 pub async fn universe_domain(&self) -> Option<String> {
140 self.inner.universe_domain().await
141 }
142}
143
144#[derive(Clone, Debug)]
152pub struct AccessTokenCredentials {
153 inner: Arc<dyn dynamic::AccessTokenCredentialsProvider>,
162}
163
164impl<T> std::convert::From<T> for AccessTokenCredentials
165where
166 T: crate::credentials::AccessTokenCredentialsProvider + Send + Sync + 'static,
167{
168 fn from(value: T) -> Self {
169 Self {
170 inner: Arc::new(value),
171 }
172 }
173}
174
175impl AccessTokenCredentials {
176 pub async fn access_token(&self) -> Result<AccessToken> {
177 self.inner.access_token().await
178 }
179}
180
181impl CredentialsProvider for AccessTokenCredentials {
184 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
185 self.inner.headers(extensions).await
186 }
187
188 async fn universe_domain(&self) -> Option<String> {
189 self.inner.universe_domain().await
190 }
191}
192
193#[derive(Clone)]
195pub struct AccessToken {
196 pub token: String,
198}
199
200impl std::fmt::Debug for AccessToken {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("AccessToken")
203 .field("token", &"[censored]")
204 .finish()
205 }
206}
207
208impl std::convert::From<CacheableResource<Token>> for Result<AccessToken> {
209 fn from(token: CacheableResource<Token>) -> Self {
210 match token {
211 CacheableResource::New { data, .. } => Ok(data.into()),
212 CacheableResource::NotModified => Err(errors::CredentialsError::from_msg(
213 false,
214 "Expecting token to be present",
215 )),
216 }
217 }
218}
219
220impl std::convert::From<Token> for AccessToken {
221 fn from(token: Token) -> Self {
222 Self { token: token.token }
223 }
224}
225
226pub trait AccessTokenCredentialsProvider: CredentialsProvider + std::fmt::Debug {
232 fn access_token(&self) -> impl Future<Output = Result<AccessToken>> + Send;
234}
235
236pub trait CredentialsProvider: std::fmt::Debug {
276 fn headers(
298 &self,
299 extensions: Extensions,
300 ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
301
302 fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
304}
305
306pub(crate) mod dynamic {
307 use super::Result;
308 use super::{CacheableResource, Extensions, HeaderMap};
309
310 #[async_trait::async_trait]
312 pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
313 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
335
336 async fn universe_domain(&self) -> Option<String> {
338 Some("googleapis.com".to_string())
339 }
340 }
341
342 #[async_trait::async_trait]
344 impl<T> CredentialsProvider for T
345 where
346 T: super::CredentialsProvider + Send + Sync,
347 {
348 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
349 T::headers(self, extensions).await
350 }
351 async fn universe_domain(&self) -> Option<String> {
352 T::universe_domain(self).await
353 }
354 }
355
356 #[async_trait::async_trait]
358 pub trait AccessTokenCredentialsProvider:
359 CredentialsProvider + Send + Sync + std::fmt::Debug
360 {
361 async fn access_token(&self) -> Result<super::AccessToken>;
362 }
363
364 #[async_trait::async_trait]
365 impl<T> AccessTokenCredentialsProvider for T
366 where
367 T: super::AccessTokenCredentialsProvider + Send + Sync,
368 {
369 async fn access_token(&self) -> Result<super::AccessToken> {
370 T::access_token(self).await
371 }
372 }
373}
374
375#[derive(Debug)]
430pub struct Builder {
431 quota_project_id: Option<String>,
432 scopes: Option<Vec<String>>,
433}
434
435impl Default for Builder {
436 fn default() -> Self {
448 Self {
449 quota_project_id: None,
450 scopes: None,
451 }
452 }
453}
454
455impl Builder {
456 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
478 self.quota_project_id = Some(quota_project_id.into());
479 self
480 }
481
482 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
502 where
503 I: IntoIterator<Item = S>,
504 S: Into<String>,
505 {
506 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
507 self
508 }
509
510 pub fn build(self) -> BuildResult<Credentials> {
522 Ok(self.build_access_token_credentials()?.into())
523 }
524
525 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
551 let json_data = match load_adc()? {
552 AdcContents::Contents(contents) => {
553 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
554 }
555 AdcContents::FallbackToMds => None,
556 };
557 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
558 .ok()
559 .or(self.quota_project_id);
560 build_credentials(json_data, quota_project_id, self.scopes)
561 }
562
563 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
582 let json_data = match load_adc()? {
583 AdcContents::Contents(contents) => {
584 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
585 }
586 AdcContents::FallbackToMds => None,
587 };
588 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
589 .ok()
590 .or(self.quota_project_id);
591 build_signer(json_data, quota_project_id, self.scopes)
592 }
593}
594
595#[derive(Debug, PartialEq)]
596enum AdcPath {
597 FromEnv(String),
598 WellKnown(String),
599}
600
601#[derive(Debug, PartialEq)]
602enum AdcContents {
603 Contents(String),
604 FallbackToMds,
605}
606
607fn extract_credential_type(json: &Value) -> BuildResult<&str> {
608 json.get("type")
609 .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
610 .as_str()
611 .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
612}
613
614macro_rules! config_builder {
622 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
623 let builder = config_common_builder!(
624 $builder_instance,
625 $quota_project_id_option,
626 $scopes_option,
627 $apply_scopes_closure
628 );
629 builder.build_access_token_credentials()
630 }};
631}
632
633macro_rules! config_signer {
636 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
637 let builder = config_common_builder!(
638 $builder_instance,
639 $quota_project_id_option,
640 $scopes_option,
641 $apply_scopes_closure
642 );
643 builder.build_signer()
644 }};
645}
646
647macro_rules! config_common_builder {
648 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
649 let builder = $builder_instance;
650 let builder = $quota_project_id_option
651 .into_iter()
652 .fold(builder, |b, qp| b.with_quota_project_id(qp));
653
654 let builder = $scopes_option
655 .into_iter()
656 .fold(builder, |b, s| $apply_scopes_closure(b, s));
657
658 builder
659 }};
660}
661
662fn build_credentials(
663 json: Option<Value>,
664 quota_project_id: Option<String>,
665 scopes: Option<Vec<String>>,
666) -> BuildResult<AccessTokenCredentials> {
667 match json {
668 None => config_builder!(
669 mds::Builder::from_adc(),
670 quota_project_id,
671 scopes,
672 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
673 ),
674 Some(json) => {
675 let cred_type = extract_credential_type(&json)?;
676 match cred_type {
677 "authorized_user" => {
678 config_builder!(
679 user_account::Builder::new(json),
680 quota_project_id,
681 scopes,
682 |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
683 )
684 }
685 "service_account" => config_builder!(
686 service_account::Builder::new(json),
687 quota_project_id,
688 scopes,
689 |b: service_account::Builder, s: Vec<String>| b
690 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
691 ),
692 "impersonated_service_account" => {
693 config_builder!(
694 impersonated::Builder::new(json),
695 quota_project_id,
696 scopes,
697 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
698 )
699 }
700 "external_account" => config_builder!(
701 external_account::Builder::new(json),
702 quota_project_id,
703 scopes,
704 |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
705 ),
706 _ => Err(BuilderError::unknown_type(cred_type)),
707 }
708 }
709 }
710}
711
712fn build_signer(
713 json: Option<Value>,
714 quota_project_id: Option<String>,
715 scopes: Option<Vec<String>>,
716) -> BuildResult<crate::signer::Signer> {
717 match json {
718 None => config_signer!(
719 mds::Builder::from_adc(),
720 quota_project_id,
721 scopes,
722 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
723 ),
724 Some(json) => {
725 let cred_type = extract_credential_type(&json)?;
726 match cred_type {
727 "authorized_user" => Err(BuilderError::not_supported(
728 "authorized_user signer is not supported",
729 )),
730 "service_account" => config_signer!(
731 service_account::Builder::new(json),
732 quota_project_id,
733 scopes,
734 |b: service_account::Builder, s: Vec<String>| b
735 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
736 ),
737 "impersonated_service_account" => {
738 config_signer!(
739 impersonated::Builder::new(json),
740 quota_project_id,
741 scopes,
742 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
743 )
744 }
745 "external_account" => Err(BuilderError::not_supported(
746 "external_account signer is not supported",
747 )),
748 _ => Err(BuilderError::unknown_type(cred_type)),
749 }
750 }
751 }
752}
753
754fn path_not_found(path: String) -> BuilderError {
755 BuilderError::loading(format!(
756 "{path}. {}",
757 concat!(
758 "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
759 "environment variable. Verify this environment variable points to ",
760 "a valid file."
761 )
762 ))
763}
764
765fn load_adc() -> BuildResult<AdcContents> {
766 match adc_path() {
767 None => Ok(AdcContents::FallbackToMds),
768 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
769 Ok(contents) => Ok(AdcContents::Contents(contents)),
770 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
771 Err(e) => Err(BuilderError::loading(e)),
772 },
773 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
774 Ok(contents) => Ok(AdcContents::Contents(contents)),
775 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
776 Err(e) => Err(BuilderError::loading(e)),
777 },
778 }
779}
780
781fn adc_path() -> Option<AdcPath> {
785 if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
786 return Some(AdcPath::FromEnv(path));
787 }
788 Some(AdcPath::WellKnown(adc_well_known_path()?))
789}
790
791#[cfg(target_os = "windows")]
795fn adc_well_known_path() -> Option<String> {
796 std::env::var("APPDATA")
797 .ok()
798 .map(|root| root + "/gcloud/application_default_credentials.json")
799}
800
801#[cfg(not(target_os = "windows"))]
805fn adc_well_known_path() -> Option<String> {
806 std::env::var("HOME")
807 .ok()
808 .map(|root| root + "/.config/gcloud/application_default_credentials.json")
809}
810
811#[cfg_attr(test, mutants::skip)]
822#[doc(hidden)]
823pub mod testing {
824 use super::CacheableResource;
825 use crate::Result;
826 use crate::credentials::Credentials;
827 use crate::credentials::dynamic::CredentialsProvider;
828 use http::{Extensions, HeaderMap};
829 use std::sync::Arc;
830
831 pub fn error_credentials(retryable: bool) -> Credentials {
835 Credentials {
836 inner: Arc::from(ErrorCredentials(retryable)),
837 }
838 }
839
840 #[derive(Debug, Default)]
841 struct ErrorCredentials(bool);
842
843 #[async_trait::async_trait]
844 impl CredentialsProvider for ErrorCredentials {
845 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
846 Err(super::CredentialsError::from_msg(self.0, "test-only"))
847 }
848
849 async fn universe_domain(&self) -> Option<String> {
850 None
851 }
852 }
853}
854
855#[cfg(test)]
856pub(crate) mod tests {
857 use super::*;
858 use base64::Engine;
859 use google_cloud_gax::backoff_policy::BackoffPolicy;
860 use google_cloud_gax::retry_policy::RetryPolicy;
861 use google_cloud_gax::retry_result::RetryResult;
862 use google_cloud_gax::retry_state::RetryState;
863 use google_cloud_gax::retry_throttler::RetryThrottler;
864 use mockall::mock;
865 use reqwest::header::AUTHORIZATION;
866 use rsa::BigUint;
867 use rsa::RsaPrivateKey;
868 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
869 use scoped_env::ScopedEnv;
870 use std::error::Error;
871 use std::sync::LazyLock;
872 use test_case::test_case;
873 use tokio::time::Duration;
874 use tokio::time::Instant;
875
876 pub(crate) fn find_source_error<'a, T: Error + 'static>(
877 error: &'a (dyn Error + 'static),
878 ) -> Option<&'a T> {
879 let mut source = error.source();
880 while let Some(err) = source {
881 if let Some(target_err) = err.downcast_ref::<T>() {
882 return Some(target_err);
883 }
884 source = err.source();
885 }
886 None
887 }
888
889 mock! {
890 #[derive(Debug)]
891 pub RetryPolicy {}
892 impl RetryPolicy for RetryPolicy {
893 fn on_error(
894 &self,
895 state: &RetryState,
896 error: google_cloud_gax::error::Error,
897 ) -> RetryResult;
898 }
899 }
900
901 mock! {
902 #[derive(Debug)]
903 pub BackoffPolicy {}
904 impl BackoffPolicy for BackoffPolicy {
905 fn on_failure(&self, state: &RetryState) -> std::time::Duration;
906 }
907 }
908
909 mockall::mock! {
910 #[derive(Debug)]
911 pub RetryThrottler {}
912 impl RetryThrottler for RetryThrottler {
913 fn throttle_retry_attempt(&self) -> bool;
914 fn on_retry_failure(&mut self, error: &RetryResult);
915 fn on_success(&mut self);
916 }
917 }
918
919 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
920
921 pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
922 let mut retry_policy = MockRetryPolicy::new();
923 retry_policy
924 .expect_on_error()
925 .returning(move |state, error| {
926 if state.attempt_count >= attempts as u32 {
927 return RetryResult::Exhausted(error);
928 }
929 let is_transient = error
930 .source()
931 .and_then(|e| e.downcast_ref::<CredentialsError>())
932 .is_some_and(|ce| ce.is_transient());
933 if is_transient {
934 RetryResult::Continue(error)
935 } else {
936 RetryResult::Permanent(error)
937 }
938 });
939 retry_policy
940 }
941
942 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
943 let mut backoff_policy = MockBackoffPolicy::new();
944 backoff_policy
945 .expect_on_failure()
946 .return_const(Duration::from_secs(0));
947 backoff_policy
948 }
949
950 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
951 let mut throttler = MockRetryThrottler::new();
952 throttler.expect_on_retry_failure().return_const(());
953 throttler
954 .expect_throttle_retry_attempt()
955 .return_const(false);
956 throttler.expect_on_success().return_const(());
957 throttler
958 }
959
960 pub(crate) fn get_headers_from_cache(
961 headers: CacheableResource<HeaderMap>,
962 ) -> Result<HeaderMap> {
963 match headers {
964 CacheableResource::New { data, .. } => Ok(data),
965 CacheableResource::NotModified => Err(CredentialsError::from_msg(
966 false,
967 "Expecting headers to be present",
968 )),
969 }
970 }
971
972 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
973 match headers {
974 CacheableResource::New { data, .. } => data
975 .get(AUTHORIZATION)
976 .and_then(|token_value| token_value.to_str().ok())
977 .and_then(|s| s.split_whitespace().nth(1))
978 .map(|s| s.to_string()),
979 CacheableResource::NotModified => None,
980 }
981 }
982
983 pub(crate) fn get_token_type_from_headers(
984 headers: CacheableResource<HeaderMap>,
985 ) -> Option<String> {
986 match headers {
987 CacheableResource::New { data, .. } => data
988 .get(AUTHORIZATION)
989 .and_then(|token_value| token_value.to_str().ok())
990 .and_then(|s| s.split_whitespace().next())
991 .map(|s| s.to_string()),
992 CacheableResource::NotModified => None,
993 }
994 }
995
996 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
997 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
998 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
999 let e_str: &str = "65537";
1000
1001 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1002 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1003 let public_exponent =
1004 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1005
1006 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1007 .expect("Failed to create RsaPrivateKey from primes")
1008 });
1009
1010 #[cfg(feature = "idtoken")]
1011 pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1012 let secret_key_bytes = [
1013 0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1014 0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1015 0x26, 0x6c, 0x75, 0xdf,
1016 ];
1017 p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1018 });
1019
1020 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1021 RSA_PRIVATE_KEY
1022 .to_pkcs8_pem(LineEnding::LF)
1023 .expect("Failed to encode key to PKCS#8 PEM")
1024 .to_string()
1025 });
1026
1027 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1028 let decoded = String::from_utf8(
1029 base64::engine::general_purpose::URL_SAFE_NO_PAD
1030 .decode(s)
1031 .unwrap(),
1032 )
1033 .unwrap();
1034 serde_json::from_str(&decoded).unwrap()
1035 }
1036
1037 #[cfg(target_os = "windows")]
1038 #[test]
1039 #[serial_test::serial]
1040 fn adc_well_known_path_windows() {
1041 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1042 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1043 assert_eq!(
1044 adc_well_known_path(),
1045 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1046 );
1047 assert_eq!(
1048 adc_path(),
1049 Some(AdcPath::WellKnown(
1050 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1051 ))
1052 );
1053 }
1054
1055 #[cfg(target_os = "windows")]
1056 #[test]
1057 #[serial_test::serial]
1058 fn adc_well_known_path_windows_no_appdata() {
1059 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1060 let _appdata = ScopedEnv::remove("APPDATA");
1061 assert_eq!(adc_well_known_path(), None);
1062 assert_eq!(adc_path(), None);
1063 }
1064
1065 #[cfg(not(target_os = "windows"))]
1066 #[test]
1067 #[serial_test::serial]
1068 fn adc_well_known_path_posix() {
1069 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1070 let _home = ScopedEnv::set("HOME", "/home/foo");
1071 assert_eq!(
1072 adc_well_known_path(),
1073 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1074 );
1075 assert_eq!(
1076 adc_path(),
1077 Some(AdcPath::WellKnown(
1078 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1079 ))
1080 );
1081 }
1082
1083 #[cfg(not(target_os = "windows"))]
1084 #[test]
1085 #[serial_test::serial]
1086 fn adc_well_known_path_posix_no_home() {
1087 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1088 let _appdata = ScopedEnv::remove("HOME");
1089 assert_eq!(adc_well_known_path(), None);
1090 assert_eq!(adc_path(), None);
1091 }
1092
1093 #[test]
1094 #[serial_test::serial]
1095 fn adc_path_from_env() {
1096 let _creds = ScopedEnv::set(
1097 "GOOGLE_APPLICATION_CREDENTIALS",
1098 "/usr/bar/application_default_credentials.json",
1099 );
1100 assert_eq!(
1101 adc_path(),
1102 Some(AdcPath::FromEnv(
1103 "/usr/bar/application_default_credentials.json".to_string()
1104 ))
1105 );
1106 }
1107
1108 #[test]
1109 #[serial_test::serial]
1110 fn load_adc_no_well_known_path_fallback_to_mds() {
1111 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1112 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1115 }
1116
1117 #[test]
1118 #[serial_test::serial]
1119 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1120 let dir = tempfile::TempDir::new().unwrap();
1122 let path = dir.path().to_str().unwrap();
1123 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1124 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1127 }
1128
1129 #[test]
1130 #[serial_test::serial]
1131 fn load_adc_no_file_at_env_is_error() {
1132 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1133 let err = load_adc().unwrap_err();
1134 assert!(err.is_loading(), "{err:?}");
1135 let msg = format!("{err:?}");
1136 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1137 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1138 }
1139
1140 #[test]
1141 #[serial_test::serial]
1142 fn load_adc_success() {
1143 let file = tempfile::NamedTempFile::new().unwrap();
1144 let path = file.into_temp_path();
1145 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1146 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1147
1148 assert_eq!(
1149 load_adc().unwrap(),
1150 AdcContents::Contents("contents".to_string())
1151 );
1152 }
1153
1154 #[test_case(true; "retryable")]
1155 #[test_case(false; "non-retryable")]
1156 #[tokio::test]
1157 async fn error_credentials(retryable: bool) {
1158 let credentials = super::testing::error_credentials(retryable);
1159 assert!(
1160 credentials.universe_domain().await.is_none(),
1161 "{credentials:?}"
1162 );
1163 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1164 assert_eq!(err.is_transient(), retryable, "{err:?}");
1165 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1166 assert_eq!(err.is_transient(), retryable, "{err:?}");
1167 }
1168
1169 #[tokio::test]
1170 #[serial_test::serial]
1171 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1172 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1173 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1176
1177 let mds = Builder::default()
1178 .with_quota_project_id("test-quota-project")
1179 .build()
1180 .unwrap();
1181 let fmt = format!("{mds:?}");
1182 assert!(fmt.contains("MDSCredentials"));
1183 assert!(
1184 fmt.contains("env-quota-project"),
1185 "Expected 'env-quota-project', got: {fmt}"
1186 );
1187 }
1188
1189 #[tokio::test]
1190 #[serial_test::serial]
1191 async fn create_access_token_credentials_with_quota_project_from_builder() {
1192 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1193 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1196
1197 let creds = Builder::default()
1198 .with_quota_project_id("test-quota-project")
1199 .build()
1200 .unwrap();
1201 let fmt = format!("{creds:?}");
1202 assert!(
1203 fmt.contains("test-quota-project"),
1204 "Expected 'test-quota-project', got: {fmt}"
1205 );
1206 }
1207
1208 #[tokio::test]
1209 #[serial_test::serial]
1210 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1211 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1212 let mut service_account_key = serde_json::json!({
1213 "type": "service_account",
1214 "project_id": "test-project-id",
1215 "private_key_id": "test-private-key-id",
1216 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1217 "client_email": "test-client-email",
1218 "universe_domain": "test-universe-domain"
1219 });
1220
1221 let scopes =
1222 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1223
1224 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1225
1226 let file = tempfile::NamedTempFile::new().unwrap();
1227 let path = file.into_temp_path();
1228 std::fs::write(&path, service_account_key.to_string())
1229 .expect("Unable to write to temporary file.");
1230 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1231
1232 let sac = Builder::default()
1233 .with_quota_project_id("test-quota-project")
1234 .with_scopes(scopes)
1235 .build()
1236 .unwrap();
1237
1238 let headers = sac.headers(Extensions::new()).await?;
1239 let token = get_token_from_headers(headers).unwrap();
1240 let parts: Vec<_> = token.split('.').collect();
1241 assert_eq!(parts.len(), 3);
1242 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1243
1244 let fmt = format!("{sac:?}");
1245 assert!(fmt.contains("ServiceAccountCredentials"));
1246 assert!(fmt.contains("test-quota-project"));
1247 assert_eq!(claims["scope"], scopes.join(" "));
1248
1249 Ok(())
1250 }
1251
1252 #[test]
1253 fn debug_access_token() {
1254 let expires_at = Instant::now() + Duration::from_secs(3600);
1255 let token = Token {
1256 token: "token-test-only".into(),
1257 token_type: "Bearer".into(),
1258 expires_at: Some(expires_at),
1259 metadata: None,
1260 };
1261 let access_token: AccessToken = token.into();
1262 let got = format!("{access_token:?}");
1263 assert!(!got.contains("token-test-only"), "{got}");
1264 assert!(got.contains("token: \"[censored]\""), "{got}");
1265 }
1266}