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)]
429pub struct Builder {
430 quota_project_id: Option<String>,
431 scopes: Option<Vec<String>>,
432}
433
434impl Default for Builder {
435 fn default() -> Self {
447 Self {
448 quota_project_id: None,
449 scopes: None,
450 }
451 }
452}
453
454impl Builder {
455 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
477 self.quota_project_id = Some(quota_project_id.into());
478 self
479 }
480
481 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
501 where
502 I: IntoIterator<Item = S>,
503 S: Into<String>,
504 {
505 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
506 self
507 }
508
509 pub fn build(self) -> BuildResult<Credentials> {
521 Ok(self.build_access_token_credentials()?.into())
522 }
523
524 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
549 let json_data = match load_adc()? {
550 AdcContents::Contents(contents) => {
551 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
552 }
553 AdcContents::FallbackToMds => None,
554 };
555 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
556 .ok()
557 .or(self.quota_project_id);
558 build_credentials(json_data, quota_project_id, self.scopes)
559 }
560
561 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
579 let json_data = match load_adc()? {
580 AdcContents::Contents(contents) => {
581 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
582 }
583 AdcContents::FallbackToMds => None,
584 };
585 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
586 .ok()
587 .or(self.quota_project_id);
588 build_signer(json_data, quota_project_id, self.scopes)
589 }
590}
591
592#[derive(Debug, PartialEq)]
593enum AdcPath {
594 FromEnv(String),
595 WellKnown(String),
596}
597
598#[derive(Debug, PartialEq)]
599enum AdcContents {
600 Contents(String),
601 FallbackToMds,
602}
603
604fn extract_credential_type(json: &Value) -> BuildResult<&str> {
605 json.get("type")
606 .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
607 .as_str()
608 .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
609}
610
611macro_rules! config_builder {
619 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
620 let builder = config_common_builder!(
621 $builder_instance,
622 $quota_project_id_option,
623 $scopes_option,
624 $apply_scopes_closure
625 );
626 builder.build_access_token_credentials()
627 }};
628}
629
630macro_rules! config_signer {
633 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
634 let builder = config_common_builder!(
635 $builder_instance,
636 $quota_project_id_option,
637 $scopes_option,
638 $apply_scopes_closure
639 );
640 builder.build_signer()
641 }};
642}
643
644macro_rules! config_common_builder {
645 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
646 let builder = $builder_instance;
647 let builder = $quota_project_id_option
648 .into_iter()
649 .fold(builder, |b, qp| b.with_quota_project_id(qp));
650
651 let builder = $scopes_option
652 .into_iter()
653 .fold(builder, |b, s| $apply_scopes_closure(b, s));
654
655 builder
656 }};
657}
658
659fn build_credentials(
660 json: Option<Value>,
661 quota_project_id: Option<String>,
662 scopes: Option<Vec<String>>,
663) -> BuildResult<AccessTokenCredentials> {
664 match json {
665 None => config_builder!(
666 mds::Builder::from_adc(),
667 quota_project_id,
668 scopes,
669 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
670 ),
671 Some(json) => {
672 let cred_type = extract_credential_type(&json)?;
673 match cred_type {
674 "authorized_user" => {
675 config_builder!(
676 user_account::Builder::new(json),
677 quota_project_id,
678 scopes,
679 |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
680 )
681 }
682 "service_account" => config_builder!(
683 service_account::Builder::new(json),
684 quota_project_id,
685 scopes,
686 |b: service_account::Builder, s: Vec<String>| b
687 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
688 ),
689 "impersonated_service_account" => {
690 config_builder!(
691 impersonated::Builder::new(json),
692 quota_project_id,
693 scopes,
694 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
695 )
696 }
697 "external_account" => config_builder!(
698 external_account::Builder::new(json),
699 quota_project_id,
700 scopes,
701 |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
702 ),
703 _ => Err(BuilderError::unknown_type(cred_type)),
704 }
705 }
706 }
707}
708
709fn build_signer(
710 json: Option<Value>,
711 quota_project_id: Option<String>,
712 scopes: Option<Vec<String>>,
713) -> BuildResult<crate::signer::Signer> {
714 match json {
715 None => config_signer!(
716 mds::Builder::from_adc(),
717 quota_project_id,
718 scopes,
719 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
720 ),
721 Some(json) => {
722 let cred_type = extract_credential_type(&json)?;
723 match cred_type {
724 "authorized_user" => Err(BuilderError::not_supported(
725 "authorized_user signer is not supported",
726 )),
727 "service_account" => config_signer!(
728 service_account::Builder::new(json),
729 quota_project_id,
730 scopes,
731 |b: service_account::Builder, s: Vec<String>| b
732 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
733 ),
734 "impersonated_service_account" => {
735 config_signer!(
736 impersonated::Builder::new(json),
737 quota_project_id,
738 scopes,
739 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
740 )
741 }
742 "external_account" => Err(BuilderError::not_supported(
743 "external_account signer is not supported",
744 )),
745 _ => Err(BuilderError::unknown_type(cred_type)),
746 }
747 }
748 }
749}
750
751fn path_not_found(path: String) -> BuilderError {
752 BuilderError::loading(format!(
753 "{path}. {}",
754 concat!(
755 "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
756 "environment variable. Verify this environment variable points to ",
757 "a valid file."
758 )
759 ))
760}
761
762fn load_adc() -> BuildResult<AdcContents> {
763 match adc_path() {
764 None => Ok(AdcContents::FallbackToMds),
765 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
766 Ok(contents) => Ok(AdcContents::Contents(contents)),
767 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
768 Err(e) => Err(BuilderError::loading(e)),
769 },
770 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
771 Ok(contents) => Ok(AdcContents::Contents(contents)),
772 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
773 Err(e) => Err(BuilderError::loading(e)),
774 },
775 }
776}
777
778fn adc_path() -> Option<AdcPath> {
782 if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
783 return Some(AdcPath::FromEnv(path));
784 }
785 Some(AdcPath::WellKnown(adc_well_known_path()?))
786}
787
788#[cfg(target_os = "windows")]
792fn adc_well_known_path() -> Option<String> {
793 std::env::var("APPDATA")
794 .ok()
795 .map(|root| root + "/gcloud/application_default_credentials.json")
796}
797
798#[cfg(not(target_os = "windows"))]
802fn adc_well_known_path() -> Option<String> {
803 std::env::var("HOME")
804 .ok()
805 .map(|root| root + "/.config/gcloud/application_default_credentials.json")
806}
807
808#[cfg_attr(test, mutants::skip)]
819#[doc(hidden)]
820pub mod testing {
821 use super::CacheableResource;
822 use crate::Result;
823 use crate::credentials::Credentials;
824 use crate::credentials::dynamic::CredentialsProvider;
825 use http::{Extensions, HeaderMap};
826 use std::sync::Arc;
827
828 pub fn error_credentials(retryable: bool) -> Credentials {
832 Credentials {
833 inner: Arc::from(ErrorCredentials(retryable)),
834 }
835 }
836
837 #[derive(Debug, Default)]
838 struct ErrorCredentials(bool);
839
840 #[async_trait::async_trait]
841 impl CredentialsProvider for ErrorCredentials {
842 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
843 Err(super::CredentialsError::from_msg(self.0, "test-only"))
844 }
845
846 async fn universe_domain(&self) -> Option<String> {
847 None
848 }
849 }
850}
851
852#[cfg(test)]
853pub(crate) mod tests {
854 use super::*;
855 use crate::constants::TRUST_BOUNDARY_HEADER;
856 use base64::Engine;
857 use google_cloud_gax::backoff_policy::BackoffPolicy;
858 use google_cloud_gax::retry_policy::RetryPolicy;
859 use google_cloud_gax::retry_result::RetryResult;
860 use google_cloud_gax::retry_state::RetryState;
861 use google_cloud_gax::retry_throttler::RetryThrottler;
862 use mockall::mock;
863 use reqwest::header::AUTHORIZATION;
864 use rsa::BigUint;
865 use rsa::RsaPrivateKey;
866 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
867 use scoped_env::ScopedEnv;
868 use std::error::Error;
869 use std::sync::LazyLock;
870 use test_case::test_case;
871 use tokio::time::Duration;
872 use tokio::time::Instant;
873
874 pub(crate) fn find_source_error<'a, T: Error + 'static>(
875 error: &'a (dyn Error + 'static),
876 ) -> Option<&'a T> {
877 let mut source = error.source();
878 while let Some(err) = source {
879 if let Some(target_err) = err.downcast_ref::<T>() {
880 return Some(target_err);
881 }
882 source = err.source();
883 }
884 None
885 }
886
887 mock! {
888 #[derive(Debug)]
889 pub RetryPolicy {}
890 impl RetryPolicy for RetryPolicy {
891 fn on_error(
892 &self,
893 state: &RetryState,
894 error: google_cloud_gax::error::Error,
895 ) -> RetryResult;
896 }
897 }
898
899 mock! {
900 #[derive(Debug)]
901 pub BackoffPolicy {}
902 impl BackoffPolicy for BackoffPolicy {
903 fn on_failure(&self, state: &RetryState) -> std::time::Duration;
904 }
905 }
906
907 mockall::mock! {
908 #[derive(Debug)]
909 pub RetryThrottler {}
910 impl RetryThrottler for RetryThrottler {
911 fn throttle_retry_attempt(&self) -> bool;
912 fn on_retry_failure(&mut self, error: &RetryResult);
913 fn on_success(&mut self);
914 }
915 }
916
917 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
918
919 pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
920 let mut retry_policy = MockRetryPolicy::new();
921 retry_policy
922 .expect_on_error()
923 .returning(move |state, error| {
924 if state.attempt_count >= attempts as u32 {
925 return RetryResult::Exhausted(error);
926 }
927 let is_transient = error
928 .source()
929 .and_then(|e| e.downcast_ref::<CredentialsError>())
930 .is_some_and(|ce| ce.is_transient());
931 if is_transient {
932 RetryResult::Continue(error)
933 } else {
934 RetryResult::Permanent(error)
935 }
936 });
937 retry_policy
938 }
939
940 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
941 let mut backoff_policy = MockBackoffPolicy::new();
942 backoff_policy
943 .expect_on_failure()
944 .return_const(Duration::from_secs(0));
945 backoff_policy
946 }
947
948 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
949 let mut throttler = MockRetryThrottler::new();
950 throttler.expect_on_retry_failure().return_const(());
951 throttler
952 .expect_throttle_retry_attempt()
953 .return_const(false);
954 throttler.expect_on_success().return_const(());
955 throttler
956 }
957
958 pub(crate) fn get_headers_from_cache(
959 headers: CacheableResource<HeaderMap>,
960 ) -> Result<HeaderMap> {
961 match headers {
962 CacheableResource::New { data, .. } => Ok(data),
963 CacheableResource::NotModified => Err(CredentialsError::from_msg(
964 false,
965 "Expecting headers to be present",
966 )),
967 }
968 }
969
970 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
971 match headers {
972 CacheableResource::New { data, .. } => data
973 .get(AUTHORIZATION)
974 .and_then(|token_value| token_value.to_str().ok())
975 .and_then(|s| s.split_whitespace().nth(1))
976 .map(|s| s.to_string()),
977 CacheableResource::NotModified => None,
978 }
979 }
980
981 pub(crate) fn get_access_boundary_from_headers(
982 headers: CacheableResource<HeaderMap>,
983 ) -> Option<String> {
984 match headers {
985 CacheableResource::New { data, .. } => data
986 .get(TRUST_BOUNDARY_HEADER)
987 .and_then(|token_value| token_value.to_str().ok())
988 .map(|s| s.to_string()),
989 CacheableResource::NotModified => None,
990 }
991 }
992
993 pub(crate) fn get_token_type_from_headers(
994 headers: CacheableResource<HeaderMap>,
995 ) -> Option<String> {
996 match headers {
997 CacheableResource::New { data, .. } => data
998 .get(AUTHORIZATION)
999 .and_then(|token_value| token_value.to_str().ok())
1000 .and_then(|s| s.split_whitespace().next())
1001 .map(|s| s.to_string()),
1002 CacheableResource::NotModified => None,
1003 }
1004 }
1005
1006 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
1007 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
1008 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
1009 let e_str: &str = "65537";
1010
1011 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1012 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1013 let public_exponent =
1014 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1015
1016 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1017 .expect("Failed to create RsaPrivateKey from primes")
1018 });
1019
1020 #[cfg(feature = "idtoken")]
1021 pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1022 let secret_key_bytes = [
1023 0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1024 0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1025 0x26, 0x6c, 0x75, 0xdf,
1026 ];
1027 p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1028 });
1029
1030 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1031 RSA_PRIVATE_KEY
1032 .to_pkcs8_pem(LineEnding::LF)
1033 .expect("Failed to encode key to PKCS#8 PEM")
1034 .to_string()
1035 });
1036
1037 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1038 let decoded = String::from_utf8(
1039 base64::engine::general_purpose::URL_SAFE_NO_PAD
1040 .decode(s)
1041 .unwrap(),
1042 )
1043 .unwrap();
1044 serde_json::from_str(&decoded).unwrap()
1045 }
1046
1047 #[cfg(target_os = "windows")]
1048 #[test]
1049 #[serial_test::serial]
1050 fn adc_well_known_path_windows() {
1051 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1052 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1053 assert_eq!(
1054 adc_well_known_path(),
1055 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1056 );
1057 assert_eq!(
1058 adc_path(),
1059 Some(AdcPath::WellKnown(
1060 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1061 ))
1062 );
1063 }
1064
1065 #[cfg(target_os = "windows")]
1066 #[test]
1067 #[serial_test::serial]
1068 fn adc_well_known_path_windows_no_appdata() {
1069 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1070 let _appdata = ScopedEnv::remove("APPDATA");
1071 assert_eq!(adc_well_known_path(), None);
1072 assert_eq!(adc_path(), None);
1073 }
1074
1075 #[cfg(not(target_os = "windows"))]
1076 #[test]
1077 #[serial_test::serial]
1078 fn adc_well_known_path_posix() {
1079 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1080 let _home = ScopedEnv::set("HOME", "/home/foo");
1081 assert_eq!(
1082 adc_well_known_path(),
1083 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1084 );
1085 assert_eq!(
1086 adc_path(),
1087 Some(AdcPath::WellKnown(
1088 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1089 ))
1090 );
1091 }
1092
1093 #[cfg(not(target_os = "windows"))]
1094 #[test]
1095 #[serial_test::serial]
1096 fn adc_well_known_path_posix_no_home() {
1097 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1098 let _appdata = ScopedEnv::remove("HOME");
1099 assert_eq!(adc_well_known_path(), None);
1100 assert_eq!(adc_path(), None);
1101 }
1102
1103 #[test]
1104 #[serial_test::serial]
1105 fn adc_path_from_env() {
1106 let _creds = ScopedEnv::set(
1107 "GOOGLE_APPLICATION_CREDENTIALS",
1108 "/usr/bar/application_default_credentials.json",
1109 );
1110 assert_eq!(
1111 adc_path(),
1112 Some(AdcPath::FromEnv(
1113 "/usr/bar/application_default_credentials.json".to_string()
1114 ))
1115 );
1116 }
1117
1118 #[test]
1119 #[serial_test::serial]
1120 fn load_adc_no_well_known_path_fallback_to_mds() {
1121 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1122 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1125 }
1126
1127 #[test]
1128 #[serial_test::serial]
1129 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1130 let dir = tempfile::TempDir::new().unwrap();
1132 let path = dir.path().to_str().unwrap();
1133 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1134 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1137 }
1138
1139 #[test]
1140 #[serial_test::serial]
1141 fn load_adc_no_file_at_env_is_error() {
1142 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1143 let err = load_adc().unwrap_err();
1144 assert!(err.is_loading(), "{err:?}");
1145 let msg = format!("{err:?}");
1146 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1147 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1148 }
1149
1150 #[test]
1151 #[serial_test::serial]
1152 fn load_adc_success() {
1153 let file = tempfile::NamedTempFile::new().unwrap();
1154 let path = file.into_temp_path();
1155 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1156 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1157
1158 assert_eq!(
1159 load_adc().unwrap(),
1160 AdcContents::Contents("contents".to_string())
1161 );
1162 }
1163
1164 #[test_case(true; "retryable")]
1165 #[test_case(false; "non-retryable")]
1166 #[tokio::test]
1167 async fn error_credentials(retryable: bool) {
1168 let credentials = super::testing::error_credentials(retryable);
1169 assert!(
1170 credentials.universe_domain().await.is_none(),
1171 "{credentials:?}"
1172 );
1173 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1174 assert_eq!(err.is_transient(), retryable, "{err:?}");
1175 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1176 assert_eq!(err.is_transient(), retryable, "{err:?}");
1177 }
1178
1179 #[tokio::test]
1180 #[serial_test::serial]
1181 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1182 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1183 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1186
1187 let mds = Builder::default()
1188 .with_quota_project_id("test-quota-project")
1189 .build()
1190 .unwrap();
1191 let fmt = format!("{mds:?}");
1192 assert!(fmt.contains("MDSCredentials"));
1193 assert!(
1194 fmt.contains("env-quota-project"),
1195 "Expected 'env-quota-project', got: {fmt}"
1196 );
1197 }
1198
1199 #[tokio::test]
1200 #[serial_test::serial]
1201 async fn create_access_token_credentials_with_quota_project_from_builder() {
1202 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1203 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1206
1207 let creds = Builder::default()
1208 .with_quota_project_id("test-quota-project")
1209 .build()
1210 .unwrap();
1211 let fmt = format!("{creds:?}");
1212 assert!(
1213 fmt.contains("test-quota-project"),
1214 "Expected 'test-quota-project', got: {fmt}"
1215 );
1216 }
1217
1218 #[tokio::test]
1219 #[serial_test::serial]
1220 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1221 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1222 let mut service_account_key = serde_json::json!({
1223 "type": "service_account",
1224 "project_id": "test-project-id",
1225 "private_key_id": "test-private-key-id",
1226 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1227 "client_email": "test-client-email",
1228 "universe_domain": "test-universe-domain"
1229 });
1230
1231 let scopes =
1232 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1233
1234 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1235
1236 let file = tempfile::NamedTempFile::new().unwrap();
1237 let path = file.into_temp_path();
1238 std::fs::write(&path, service_account_key.to_string())
1239 .expect("Unable to write to temporary file.");
1240 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1241
1242 let sac = Builder::default()
1243 .with_quota_project_id("test-quota-project")
1244 .with_scopes(scopes)
1245 .build()
1246 .unwrap();
1247
1248 let headers = sac.headers(Extensions::new()).await?;
1249 let token = get_token_from_headers(headers).unwrap();
1250 let parts: Vec<_> = token.split('.').collect();
1251 assert_eq!(parts.len(), 3);
1252 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1253
1254 let fmt = format!("{sac:?}");
1255 assert!(fmt.contains("ServiceAccountCredentials"));
1256 assert!(fmt.contains("test-quota-project"));
1257 assert_eq!(claims["scope"], scopes.join(" "));
1258
1259 Ok(())
1260 }
1261
1262 #[test]
1263 fn debug_access_token() {
1264 let expires_at = Instant::now() + Duration::from_secs(3600);
1265 let token = Token {
1266 token: "token-test-only".into(),
1267 token_type: "Bearer".into(),
1268 expires_at: Some(expires_at),
1269 metadata: None,
1270 };
1271 let access_token: AccessToken = token.into();
1272 let got = format!("{access_token:?}");
1273 assert!(!got.contains("token-test-only"), "{got}");
1274 assert!(got.contains("token: \"[censored]\""), "{got}");
1275 }
1276}