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