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 gax::backoff_policy::BackoffPolicy;
860 use gax::retry_policy::RetryPolicy;
861 use gax::retry_result::RetryResult;
862 use gax::retry_state::RetryState;
863 use 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: 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 pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1011 let secret_key_bytes = [
1012 0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1013 0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1014 0x26, 0x6c, 0x75, 0xdf,
1015 ];
1016 p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1017 });
1018
1019 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1020 RSA_PRIVATE_KEY
1021 .to_pkcs8_pem(LineEnding::LF)
1022 .expect("Failed to encode key to PKCS#8 PEM")
1023 .to_string()
1024 });
1025
1026 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1027 let decoded = String::from_utf8(
1028 base64::engine::general_purpose::URL_SAFE_NO_PAD
1029 .decode(s)
1030 .unwrap(),
1031 )
1032 .unwrap();
1033 serde_json::from_str(&decoded).unwrap()
1034 }
1035
1036 #[cfg(target_os = "windows")]
1037 #[test]
1038 #[serial_test::serial]
1039 fn adc_well_known_path_windows() {
1040 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1041 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1042 assert_eq!(
1043 adc_well_known_path(),
1044 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1045 );
1046 assert_eq!(
1047 adc_path(),
1048 Some(AdcPath::WellKnown(
1049 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1050 ))
1051 );
1052 }
1053
1054 #[cfg(target_os = "windows")]
1055 #[test]
1056 #[serial_test::serial]
1057 fn adc_well_known_path_windows_no_appdata() {
1058 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1059 let _appdata = ScopedEnv::remove("APPDATA");
1060 assert_eq!(adc_well_known_path(), None);
1061 assert_eq!(adc_path(), None);
1062 }
1063
1064 #[cfg(not(target_os = "windows"))]
1065 #[test]
1066 #[serial_test::serial]
1067 fn adc_well_known_path_posix() {
1068 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1069 let _home = ScopedEnv::set("HOME", "/home/foo");
1070 assert_eq!(
1071 adc_well_known_path(),
1072 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1073 );
1074 assert_eq!(
1075 adc_path(),
1076 Some(AdcPath::WellKnown(
1077 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1078 ))
1079 );
1080 }
1081
1082 #[cfg(not(target_os = "windows"))]
1083 #[test]
1084 #[serial_test::serial]
1085 fn adc_well_known_path_posix_no_home() {
1086 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1087 let _appdata = ScopedEnv::remove("HOME");
1088 assert_eq!(adc_well_known_path(), None);
1089 assert_eq!(adc_path(), None);
1090 }
1091
1092 #[test]
1093 #[serial_test::serial]
1094 fn adc_path_from_env() {
1095 let _creds = ScopedEnv::set(
1096 "GOOGLE_APPLICATION_CREDENTIALS",
1097 "/usr/bar/application_default_credentials.json",
1098 );
1099 assert_eq!(
1100 adc_path(),
1101 Some(AdcPath::FromEnv(
1102 "/usr/bar/application_default_credentials.json".to_string()
1103 ))
1104 );
1105 }
1106
1107 #[test]
1108 #[serial_test::serial]
1109 fn load_adc_no_well_known_path_fallback_to_mds() {
1110 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1111 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1114 }
1115
1116 #[test]
1117 #[serial_test::serial]
1118 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1119 let dir = tempfile::TempDir::new().unwrap();
1121 let path = dir.path().to_str().unwrap();
1122 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1123 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1126 }
1127
1128 #[test]
1129 #[serial_test::serial]
1130 fn load_adc_no_file_at_env_is_error() {
1131 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1132 let err = load_adc().unwrap_err();
1133 assert!(err.is_loading(), "{err:?}");
1134 let msg = format!("{err:?}");
1135 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1136 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1137 }
1138
1139 #[test]
1140 #[serial_test::serial]
1141 fn load_adc_success() {
1142 let file = tempfile::NamedTempFile::new().unwrap();
1143 let path = file.into_temp_path();
1144 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1145 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1146
1147 assert_eq!(
1148 load_adc().unwrap(),
1149 AdcContents::Contents("contents".to_string())
1150 );
1151 }
1152
1153 #[test_case(true; "retryable")]
1154 #[test_case(false; "non-retryable")]
1155 #[tokio::test]
1156 async fn error_credentials(retryable: bool) {
1157 let credentials = super::testing::error_credentials(retryable);
1158 assert!(
1159 credentials.universe_domain().await.is_none(),
1160 "{credentials:?}"
1161 );
1162 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1163 assert_eq!(err.is_transient(), retryable, "{err:?}");
1164 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1165 assert_eq!(err.is_transient(), retryable, "{err:?}");
1166 }
1167
1168 #[tokio::test]
1169 #[serial_test::serial]
1170 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1171 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1172 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1175
1176 let mds = Builder::default()
1177 .with_quota_project_id("test-quota-project")
1178 .build()
1179 .unwrap();
1180 let fmt = format!("{mds:?}");
1181 assert!(fmt.contains("MDSCredentials"));
1182 assert!(
1183 fmt.contains("env-quota-project"),
1184 "Expected 'env-quota-project', got: {fmt}"
1185 );
1186 }
1187
1188 #[tokio::test]
1189 #[serial_test::serial]
1190 async fn create_access_token_credentials_with_quota_project_from_builder() {
1191 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1192 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1195
1196 let creds = Builder::default()
1197 .with_quota_project_id("test-quota-project")
1198 .build()
1199 .unwrap();
1200 let fmt = format!("{creds:?}");
1201 assert!(
1202 fmt.contains("test-quota-project"),
1203 "Expected 'test-quota-project', got: {fmt}"
1204 );
1205 }
1206
1207 #[tokio::test]
1208 #[serial_test::serial]
1209 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1210 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1211 let mut service_account_key = serde_json::json!({
1212 "type": "service_account",
1213 "project_id": "test-project-id",
1214 "private_key_id": "test-private-key-id",
1215 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1216 "client_email": "test-client-email",
1217 "universe_domain": "test-universe-domain"
1218 });
1219
1220 let scopes =
1221 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1222
1223 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1224
1225 let file = tempfile::NamedTempFile::new().unwrap();
1226 let path = file.into_temp_path();
1227 std::fs::write(&path, service_account_key.to_string())
1228 .expect("Unable to write to temporary file.");
1229 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1230
1231 let sac = Builder::default()
1232 .with_quota_project_id("test-quota-project")
1233 .with_scopes(scopes)
1234 .build()
1235 .unwrap();
1236
1237 let headers = sac.headers(Extensions::new()).await?;
1238 let token = get_token_from_headers(headers).unwrap();
1239 let parts: Vec<_> = token.split('.').collect();
1240 assert_eq!(parts.len(), 3);
1241 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1242
1243 let fmt = format!("{sac:?}");
1244 assert!(fmt.contains("ServiceAccountCredentials"));
1245 assert!(fmt.contains("test-quota-project"));
1246 assert_eq!(claims["scope"], scopes.join(" "));
1247
1248 Ok(())
1249 }
1250
1251 #[test]
1252 fn debug_access_token() {
1253 let expires_at = Instant::now() + Duration::from_secs(3600);
1254 let token = Token {
1255 token: "token-test-only".into(),
1256 token_type: "Bearer".into(),
1257 expires_at: Some(expires_at),
1258 metadata: None,
1259 };
1260 let access_token: AccessToken = token.into();
1261 let got = format!("{access_token:?}");
1262 assert!(!got.contains("token-test-only"), "{got}");
1263 assert!(got.contains("token: \"[censored]\""), "{got}");
1264 }
1265}