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 crate::constants::TRUST_BOUNDARY_HEADER;
859 use base64::Engine;
860 use google_cloud_gax::backoff_policy::BackoffPolicy;
861 use google_cloud_gax::retry_policy::RetryPolicy;
862 use google_cloud_gax::retry_result::RetryResult;
863 use google_cloud_gax::retry_state::RetryState;
864 use google_cloud_gax::retry_throttler::RetryThrottler;
865 use mockall::mock;
866 use reqwest::header::AUTHORIZATION;
867 use rsa::BigUint;
868 use rsa::RsaPrivateKey;
869 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
870 use scoped_env::ScopedEnv;
871 use std::error::Error;
872 use std::sync::LazyLock;
873 use test_case::test_case;
874 use tokio::time::Duration;
875 use tokio::time::Instant;
876
877 pub(crate) fn find_source_error<'a, T: Error + 'static>(
878 error: &'a (dyn Error + 'static),
879 ) -> Option<&'a T> {
880 let mut source = error.source();
881 while let Some(err) = source {
882 if let Some(target_err) = err.downcast_ref::<T>() {
883 return Some(target_err);
884 }
885 source = err.source();
886 }
887 None
888 }
889
890 mock! {
891 #[derive(Debug)]
892 pub RetryPolicy {}
893 impl RetryPolicy for RetryPolicy {
894 fn on_error(
895 &self,
896 state: &RetryState,
897 error: google_cloud_gax::error::Error,
898 ) -> RetryResult;
899 }
900 }
901
902 mock! {
903 #[derive(Debug)]
904 pub BackoffPolicy {}
905 impl BackoffPolicy for BackoffPolicy {
906 fn on_failure(&self, state: &RetryState) -> std::time::Duration;
907 }
908 }
909
910 mockall::mock! {
911 #[derive(Debug)]
912 pub RetryThrottler {}
913 impl RetryThrottler for RetryThrottler {
914 fn throttle_retry_attempt(&self) -> bool;
915 fn on_retry_failure(&mut self, error: &RetryResult);
916 fn on_success(&mut self);
917 }
918 }
919
920 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
921
922 pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
923 let mut retry_policy = MockRetryPolicy::new();
924 retry_policy
925 .expect_on_error()
926 .returning(move |state, error| {
927 if state.attempt_count >= attempts as u32 {
928 return RetryResult::Exhausted(error);
929 }
930 let is_transient = error
931 .source()
932 .and_then(|e| e.downcast_ref::<CredentialsError>())
933 .is_some_and(|ce| ce.is_transient());
934 if is_transient {
935 RetryResult::Continue(error)
936 } else {
937 RetryResult::Permanent(error)
938 }
939 });
940 retry_policy
941 }
942
943 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
944 let mut backoff_policy = MockBackoffPolicy::new();
945 backoff_policy
946 .expect_on_failure()
947 .return_const(Duration::from_secs(0));
948 backoff_policy
949 }
950
951 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
952 let mut throttler = MockRetryThrottler::new();
953 throttler.expect_on_retry_failure().return_const(());
954 throttler
955 .expect_throttle_retry_attempt()
956 .return_const(false);
957 throttler.expect_on_success().return_const(());
958 throttler
959 }
960
961 pub(crate) fn get_headers_from_cache(
962 headers: CacheableResource<HeaderMap>,
963 ) -> Result<HeaderMap> {
964 match headers {
965 CacheableResource::New { data, .. } => Ok(data),
966 CacheableResource::NotModified => Err(CredentialsError::from_msg(
967 false,
968 "Expecting headers to be present",
969 )),
970 }
971 }
972
973 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
974 match headers {
975 CacheableResource::New { data, .. } => data
976 .get(AUTHORIZATION)
977 .and_then(|token_value| token_value.to_str().ok())
978 .and_then(|s| s.split_whitespace().nth(1))
979 .map(|s| s.to_string()),
980 CacheableResource::NotModified => None,
981 }
982 }
983
984 pub(crate) fn get_access_boundary_from_headers(
985 headers: CacheableResource<HeaderMap>,
986 ) -> Option<String> {
987 match headers {
988 CacheableResource::New { data, .. } => data
989 .get(TRUST_BOUNDARY_HEADER)
990 .and_then(|token_value| token_value.to_str().ok())
991 .map(|s| s.to_string()),
992 CacheableResource::NotModified => None,
993 }
994 }
995
996 pub(crate) fn get_token_type_from_headers(
997 headers: CacheableResource<HeaderMap>,
998 ) -> Option<String> {
999 match headers {
1000 CacheableResource::New { data, .. } => data
1001 .get(AUTHORIZATION)
1002 .and_then(|token_value| token_value.to_str().ok())
1003 .and_then(|s| s.split_whitespace().next())
1004 .map(|s| s.to_string()),
1005 CacheableResource::NotModified => None,
1006 }
1007 }
1008
1009 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
1010 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
1011 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
1012 let e_str: &str = "65537";
1013
1014 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1015 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1016 let public_exponent =
1017 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1018
1019 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1020 .expect("Failed to create RsaPrivateKey from primes")
1021 });
1022
1023 #[cfg(feature = "idtoken")]
1024 pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1025 let secret_key_bytes = [
1026 0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1027 0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1028 0x26, 0x6c, 0x75, 0xdf,
1029 ];
1030 p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1031 });
1032
1033 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1034 RSA_PRIVATE_KEY
1035 .to_pkcs8_pem(LineEnding::LF)
1036 .expect("Failed to encode key to PKCS#8 PEM")
1037 .to_string()
1038 });
1039
1040 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1041 let decoded = String::from_utf8(
1042 base64::engine::general_purpose::URL_SAFE_NO_PAD
1043 .decode(s)
1044 .unwrap(),
1045 )
1046 .unwrap();
1047 serde_json::from_str(&decoded).unwrap()
1048 }
1049
1050 #[cfg(target_os = "windows")]
1051 #[test]
1052 #[serial_test::serial]
1053 fn adc_well_known_path_windows() {
1054 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1055 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1056 assert_eq!(
1057 adc_well_known_path(),
1058 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1059 );
1060 assert_eq!(
1061 adc_path(),
1062 Some(AdcPath::WellKnown(
1063 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1064 ))
1065 );
1066 }
1067
1068 #[cfg(target_os = "windows")]
1069 #[test]
1070 #[serial_test::serial]
1071 fn adc_well_known_path_windows_no_appdata() {
1072 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1073 let _appdata = ScopedEnv::remove("APPDATA");
1074 assert_eq!(adc_well_known_path(), None);
1075 assert_eq!(adc_path(), None);
1076 }
1077
1078 #[cfg(not(target_os = "windows"))]
1079 #[test]
1080 #[serial_test::serial]
1081 fn adc_well_known_path_posix() {
1082 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1083 let _home = ScopedEnv::set("HOME", "/home/foo");
1084 assert_eq!(
1085 adc_well_known_path(),
1086 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1087 );
1088 assert_eq!(
1089 adc_path(),
1090 Some(AdcPath::WellKnown(
1091 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1092 ))
1093 );
1094 }
1095
1096 #[cfg(not(target_os = "windows"))]
1097 #[test]
1098 #[serial_test::serial]
1099 fn adc_well_known_path_posix_no_home() {
1100 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1101 let _appdata = ScopedEnv::remove("HOME");
1102 assert_eq!(adc_well_known_path(), None);
1103 assert_eq!(adc_path(), None);
1104 }
1105
1106 #[test]
1107 #[serial_test::serial]
1108 fn adc_path_from_env() {
1109 let _creds = ScopedEnv::set(
1110 "GOOGLE_APPLICATION_CREDENTIALS",
1111 "/usr/bar/application_default_credentials.json",
1112 );
1113 assert_eq!(
1114 adc_path(),
1115 Some(AdcPath::FromEnv(
1116 "/usr/bar/application_default_credentials.json".to_string()
1117 ))
1118 );
1119 }
1120
1121 #[test]
1122 #[serial_test::serial]
1123 fn load_adc_no_well_known_path_fallback_to_mds() {
1124 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1125 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1128 }
1129
1130 #[test]
1131 #[serial_test::serial]
1132 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1133 let dir = tempfile::TempDir::new().unwrap();
1135 let path = dir.path().to_str().unwrap();
1136 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1137 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1140 }
1141
1142 #[test]
1143 #[serial_test::serial]
1144 fn load_adc_no_file_at_env_is_error() {
1145 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1146 let err = load_adc().unwrap_err();
1147 assert!(err.is_loading(), "{err:?}");
1148 let msg = format!("{err:?}");
1149 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1150 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1151 }
1152
1153 #[test]
1154 #[serial_test::serial]
1155 fn load_adc_success() {
1156 let file = tempfile::NamedTempFile::new().unwrap();
1157 let path = file.into_temp_path();
1158 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1159 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1160
1161 assert_eq!(
1162 load_adc().unwrap(),
1163 AdcContents::Contents("contents".to_string())
1164 );
1165 }
1166
1167 #[test_case(true; "retryable")]
1168 #[test_case(false; "non-retryable")]
1169 #[tokio::test]
1170 async fn error_credentials(retryable: bool) {
1171 let credentials = super::testing::error_credentials(retryable);
1172 assert!(
1173 credentials.universe_domain().await.is_none(),
1174 "{credentials:?}"
1175 );
1176 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1177 assert_eq!(err.is_transient(), retryable, "{err:?}");
1178 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1179 assert_eq!(err.is_transient(), retryable, "{err:?}");
1180 }
1181
1182 #[tokio::test]
1183 #[serial_test::serial]
1184 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1185 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1186 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1189
1190 let mds = Builder::default()
1191 .with_quota_project_id("test-quota-project")
1192 .build()
1193 .unwrap();
1194 let fmt = format!("{mds:?}");
1195 assert!(fmt.contains("MDSCredentials"));
1196 assert!(
1197 fmt.contains("env-quota-project"),
1198 "Expected 'env-quota-project', got: {fmt}"
1199 );
1200 }
1201
1202 #[tokio::test]
1203 #[serial_test::serial]
1204 async fn create_access_token_credentials_with_quota_project_from_builder() {
1205 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1206 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1209
1210 let creds = Builder::default()
1211 .with_quota_project_id("test-quota-project")
1212 .build()
1213 .unwrap();
1214 let fmt = format!("{creds:?}");
1215 assert!(
1216 fmt.contains("test-quota-project"),
1217 "Expected 'test-quota-project', got: {fmt}"
1218 );
1219 }
1220
1221 #[tokio::test]
1222 #[serial_test::serial]
1223 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1224 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1225 let mut service_account_key = serde_json::json!({
1226 "type": "service_account",
1227 "project_id": "test-project-id",
1228 "private_key_id": "test-private-key-id",
1229 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1230 "client_email": "test-client-email",
1231 "universe_domain": "test-universe-domain"
1232 });
1233
1234 let scopes =
1235 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1236
1237 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1238
1239 let file = tempfile::NamedTempFile::new().unwrap();
1240 let path = file.into_temp_path();
1241 std::fs::write(&path, service_account_key.to_string())
1242 .expect("Unable to write to temporary file.");
1243 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1244
1245 let sac = Builder::default()
1246 .with_quota_project_id("test-quota-project")
1247 .with_scopes(scopes)
1248 .build()
1249 .unwrap();
1250
1251 let headers = sac.headers(Extensions::new()).await?;
1252 let token = get_token_from_headers(headers).unwrap();
1253 let parts: Vec<_> = token.split('.').collect();
1254 assert_eq!(parts.len(), 3);
1255 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1256
1257 let fmt = format!("{sac:?}");
1258 assert!(fmt.contains("ServiceAccountCredentials"));
1259 assert!(fmt.contains("test-quota-project"));
1260 assert_eq!(claims["scope"], scopes.join(" "));
1261
1262 Ok(())
1263 }
1264
1265 #[test]
1266 fn debug_access_token() {
1267 let expires_at = Instant::now() + Duration::from_secs(3600);
1268 let token = Token {
1269 token: "token-test-only".into(),
1270 token_type: "Bearer".into(),
1271 expires_at: Some(expires_at),
1272 metadata: None,
1273 };
1274 let access_token: AccessToken = token.into();
1275 let got = format!("{access_token:?}");
1276 assert!(!got.contains("token-test-only"), "{got}");
1277 assert!(got.contains("token: \"[censored]\""), "{got}");
1278 }
1279}