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#[derive(Clone, Debug, PartialEq, Default)]
53pub struct EntityTag(u64);
54
55static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
56impl EntityTag {
57 pub fn new() -> Self {
58 let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
59 Self(value)
60 }
61}
62
63#[derive(Clone, PartialEq, Debug)]
70pub enum CacheableResource<T> {
71 NotModified,
72 New { entity_tag: EntityTag, data: T },
73}
74
75#[derive(Clone, Debug)]
108pub struct Credentials {
109 inner: Arc<dyn dynamic::CredentialsProvider>,
118}
119
120impl<T> std::convert::From<T> for Credentials
121where
122 T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
123{
124 fn from(value: T) -> Self {
125 Self {
126 inner: Arc::new(value),
127 }
128 }
129}
130
131impl Credentials {
132 pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
133 self.inner.headers(extensions).await
134 }
135
136 pub async fn universe_domain(&self) -> Option<String> {
137 self.inner.universe_domain().await
138 }
139}
140
141#[derive(Clone, Debug)]
149pub struct AccessTokenCredentials {
150 inner: Arc<dyn dynamic::AccessTokenCredentialsProvider>,
159}
160
161impl<T> std::convert::From<T> for AccessTokenCredentials
162where
163 T: crate::credentials::AccessTokenCredentialsProvider + Send + Sync + 'static,
164{
165 fn from(value: T) -> Self {
166 Self {
167 inner: Arc::new(value),
168 }
169 }
170}
171
172impl AccessTokenCredentials {
173 pub async fn access_token(&self) -> Result<AccessToken> {
174 self.inner.access_token().await
175 }
176}
177
178impl CredentialsProvider for AccessTokenCredentials {
181 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
182 self.inner.headers(extensions).await
183 }
184
185 async fn universe_domain(&self) -> Option<String> {
186 self.inner.universe_domain().await
187 }
188}
189
190#[derive(Clone)]
192pub struct AccessToken {
193 pub token: String,
195}
196
197impl std::fmt::Debug for AccessToken {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct("AccessToken")
200 .field("token", &"[censored]")
201 .finish()
202 }
203}
204
205impl std::convert::From<CacheableResource<Token>> for Result<AccessToken> {
206 fn from(token: CacheableResource<Token>) -> Self {
207 match token {
208 CacheableResource::New { data, .. } => Ok(data.into()),
209 CacheableResource::NotModified => Err(errors::CredentialsError::from_msg(
210 false,
211 "Expecting token to be present",
212 )),
213 }
214 }
215}
216
217impl std::convert::From<Token> for AccessToken {
218 fn from(token: Token) -> Self {
219 Self { token: token.token }
220 }
221}
222
223pub trait AccessTokenCredentialsProvider: CredentialsProvider + std::fmt::Debug {
229 fn access_token(&self) -> impl Future<Output = Result<AccessToken>> + Send;
231}
232
233pub trait CredentialsProvider: std::fmt::Debug {
273 fn headers(
295 &self,
296 extensions: Extensions,
297 ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
298
299 fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
301}
302
303pub(crate) mod dynamic {
304 use super::Result;
305 use super::{CacheableResource, Extensions, HeaderMap};
306
307 #[async_trait::async_trait]
309 pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
310 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
332
333 async fn universe_domain(&self) -> Option<String> {
335 Some("googleapis.com".to_string())
336 }
337 }
338
339 #[async_trait::async_trait]
341 impl<T> CredentialsProvider for T
342 where
343 T: super::CredentialsProvider + Send + Sync,
344 {
345 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
346 T::headers(self, extensions).await
347 }
348 async fn universe_domain(&self) -> Option<String> {
349 T::universe_domain(self).await
350 }
351 }
352
353 #[async_trait::async_trait]
355 pub trait AccessTokenCredentialsProvider:
356 CredentialsProvider + Send + Sync + std::fmt::Debug
357 {
358 async fn access_token(&self) -> Result<super::AccessToken>;
359 }
360
361 #[async_trait::async_trait]
362 impl<T> AccessTokenCredentialsProvider for T
363 where
364 T: super::AccessTokenCredentialsProvider + Send + Sync,
365 {
366 async fn access_token(&self) -> Result<super::AccessToken> {
367 T::access_token(self).await
368 }
369 }
370}
371
372#[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> {
546 let json_data = match load_adc()? {
547 AdcContents::Contents(contents) => {
548 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
549 }
550 AdcContents::FallbackToMds => None,
551 };
552 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
553 .ok()
554 .or(self.quota_project_id);
555 build_credentials(json_data, quota_project_id, self.scopes)
556 }
557
558 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
576 let json_data = match load_adc()? {
577 AdcContents::Contents(contents) => {
578 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
579 }
580 AdcContents::FallbackToMds => None,
581 };
582 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
583 .ok()
584 .or(self.quota_project_id);
585 build_signer(json_data, quota_project_id, self.scopes)
586 }
587}
588
589#[derive(Debug, PartialEq)]
590enum AdcPath {
591 FromEnv(String),
592 WellKnown(String),
593}
594
595#[derive(Debug, PartialEq)]
596enum AdcContents {
597 Contents(String),
598 FallbackToMds,
599}
600
601fn extract_credential_type(json: &Value) -> BuildResult<&str> {
602 json.get("type")
603 .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
604 .as_str()
605 .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
606}
607
608macro_rules! config_builder {
616 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
617 let builder = config_common_builder!(
618 $builder_instance,
619 $quota_project_id_option,
620 $scopes_option,
621 $apply_scopes_closure
622 );
623 builder.build_access_token_credentials()
624 }};
625}
626
627macro_rules! config_signer {
630 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
631 let builder = config_common_builder!(
632 $builder_instance,
633 $quota_project_id_option,
634 $scopes_option,
635 $apply_scopes_closure
636 );
637 builder.build_signer()
638 }};
639}
640
641macro_rules! config_common_builder {
642 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
643 let builder = $builder_instance;
644 let builder = $quota_project_id_option
645 .into_iter()
646 .fold(builder, |b, qp| b.with_quota_project_id(qp));
647
648 let builder = $scopes_option
649 .into_iter()
650 .fold(builder, |b, s| $apply_scopes_closure(b, s));
651
652 builder
653 }};
654}
655
656fn build_credentials(
657 json: Option<Value>,
658 quota_project_id: Option<String>,
659 scopes: Option<Vec<String>>,
660) -> BuildResult<AccessTokenCredentials> {
661 match json {
662 None => config_builder!(
663 mds::Builder::from_adc(),
664 quota_project_id,
665 scopes,
666 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
667 ),
668 Some(json) => {
669 let cred_type = extract_credential_type(&json)?;
670 match cred_type {
671 "authorized_user" => {
672 config_builder!(
673 user_account::Builder::new(json),
674 quota_project_id,
675 scopes,
676 |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
677 )
678 }
679 "service_account" => config_builder!(
680 service_account::Builder::new(json),
681 quota_project_id,
682 scopes,
683 |b: service_account::Builder, s: Vec<String>| b
684 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
685 ),
686 "impersonated_service_account" => {
687 config_builder!(
688 impersonated::Builder::new(json),
689 quota_project_id,
690 scopes,
691 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
692 )
693 }
694 "external_account" => config_builder!(
695 external_account::Builder::new(json),
696 quota_project_id,
697 scopes,
698 |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
699 ),
700 _ => Err(BuilderError::unknown_type(cred_type)),
701 }
702 }
703 }
704}
705
706fn build_signer(
707 json: Option<Value>,
708 quota_project_id: Option<String>,
709 scopes: Option<Vec<String>>,
710) -> BuildResult<crate::signer::Signer> {
711 match json {
712 None => config_signer!(
713 mds::Builder::from_adc(),
714 quota_project_id,
715 scopes,
716 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
717 ),
718 Some(json) => {
719 let cred_type = extract_credential_type(&json)?;
720 match cred_type {
721 "authorized_user" => Err(BuilderError::not_supported(
722 "authorized_user signer is not supported",
723 )),
724 "service_account" => config_signer!(
725 service_account::Builder::new(json),
726 quota_project_id,
727 scopes,
728 |b: service_account::Builder, s: Vec<String>| b
729 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
730 ),
731 "impersonated_service_account" => {
732 config_signer!(
733 impersonated::Builder::new(json),
734 quota_project_id,
735 scopes,
736 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
737 )
738 }
739 "external_account" => Err(BuilderError::not_supported(
740 "external_account signer is not supported",
741 )),
742 _ => Err(BuilderError::unknown_type(cred_type)),
743 }
744 }
745 }
746}
747
748fn path_not_found(path: String) -> BuilderError {
749 BuilderError::loading(format!(
750 "{path}. {}",
751 concat!(
752 "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
753 "environment variable. Verify this environment variable points to ",
754 "a valid file."
755 )
756 ))
757}
758
759fn load_adc() -> BuildResult<AdcContents> {
760 match adc_path() {
761 None => Ok(AdcContents::FallbackToMds),
762 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
763 Ok(contents) => Ok(AdcContents::Contents(contents)),
764 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
765 Err(e) => Err(BuilderError::loading(e)),
766 },
767 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
768 Ok(contents) => Ok(AdcContents::Contents(contents)),
769 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
770 Err(e) => Err(BuilderError::loading(e)),
771 },
772 }
773}
774
775fn adc_path() -> Option<AdcPath> {
779 if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
780 return Some(AdcPath::FromEnv(path));
781 }
782 Some(AdcPath::WellKnown(adc_well_known_path()?))
783}
784
785#[cfg(target_os = "windows")]
789fn adc_well_known_path() -> Option<String> {
790 std::env::var("APPDATA")
791 .ok()
792 .map(|root| root + "/gcloud/application_default_credentials.json")
793}
794
795#[cfg(not(target_os = "windows"))]
799fn adc_well_known_path() -> Option<String> {
800 std::env::var("HOME")
801 .ok()
802 .map(|root| root + "/.config/gcloud/application_default_credentials.json")
803}
804
805#[cfg_attr(test, mutants::skip)]
816#[doc(hidden)]
817pub mod testing {
818 use super::CacheableResource;
819 use crate::Result;
820 use crate::credentials::Credentials;
821 use crate::credentials::dynamic::CredentialsProvider;
822 use http::{Extensions, HeaderMap};
823 use std::sync::Arc;
824
825 pub fn error_credentials(retryable: bool) -> Credentials {
829 Credentials {
830 inner: Arc::from(ErrorCredentials(retryable)),
831 }
832 }
833
834 #[derive(Debug, Default)]
835 struct ErrorCredentials(bool);
836
837 #[async_trait::async_trait]
838 impl CredentialsProvider for ErrorCredentials {
839 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
840 Err(super::CredentialsError::from_msg(self.0, "test-only"))
841 }
842
843 async fn universe_domain(&self) -> Option<String> {
844 None
845 }
846 }
847}
848
849#[cfg(test)]
850pub(crate) mod tests {
851 use super::*;
852 use crate::constants::TRUST_BOUNDARY_HEADER;
853 use crate::errors::is_gax_error_retryable;
854 use base64::Engine;
855 use google_cloud_gax::backoff_policy::BackoffPolicy;
856 use google_cloud_gax::retry_policy::RetryPolicy;
857 use google_cloud_gax::retry_result::RetryResult;
858 use google_cloud_gax::retry_state::RetryState;
859 use google_cloud_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>(
874 error: &'a (dyn Error + 'static),
875 ) -> Option<&'a T> {
876 let mut last_err = None;
877 let mut source = error.source();
878 while let Some(err) = source {
879 if let Some(target_err) = err.downcast_ref::<T>() {
880 last_err = Some(target_err);
881 }
882 source = err.source();
883 }
884 last_err
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_retryable = is_gax_error_retryable(&error);
928 if is_retryable {
929 RetryResult::Continue(error)
930 } else {
931 RetryResult::Permanent(error)
932 }
933 });
934 retry_policy
935 }
936
937 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
938 let mut backoff_policy = MockBackoffPolicy::new();
939 backoff_policy
940 .expect_on_failure()
941 .return_const(Duration::from_secs(0));
942 backoff_policy
943 }
944
945 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
946 let mut throttler = MockRetryThrottler::new();
947 throttler.expect_on_retry_failure().return_const(());
948 throttler
949 .expect_throttle_retry_attempt()
950 .return_const(false);
951 throttler.expect_on_success().return_const(());
952 throttler
953 }
954
955 pub(crate) fn get_headers_from_cache(
956 headers: CacheableResource<HeaderMap>,
957 ) -> Result<HeaderMap> {
958 match headers {
959 CacheableResource::New { data, .. } => Ok(data),
960 CacheableResource::NotModified => Err(CredentialsError::from_msg(
961 false,
962 "Expecting headers to be present",
963 )),
964 }
965 }
966
967 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
968 match headers {
969 CacheableResource::New { data, .. } => data
970 .get(AUTHORIZATION)
971 .and_then(|token_value| token_value.to_str().ok())
972 .and_then(|s| s.split_whitespace().nth(1))
973 .map(|s| s.to_string()),
974 CacheableResource::NotModified => None,
975 }
976 }
977
978 pub(crate) fn get_access_boundary_from_headers(
979 headers: CacheableResource<HeaderMap>,
980 ) -> Option<String> {
981 match headers {
982 CacheableResource::New { data, .. } => data
983 .get(TRUST_BOUNDARY_HEADER)
984 .and_then(|token_value| token_value.to_str().ok())
985 .map(|s| s.to_string()),
986 CacheableResource::NotModified => None,
987 }
988 }
989
990 pub(crate) fn get_token_type_from_headers(
991 headers: CacheableResource<HeaderMap>,
992 ) -> Option<String> {
993 match headers {
994 CacheableResource::New { data, .. } => data
995 .get(AUTHORIZATION)
996 .and_then(|token_value| token_value.to_str().ok())
997 .and_then(|s| s.split_whitespace().next())
998 .map(|s| s.to_string()),
999 CacheableResource::NotModified => None,
1000 }
1001 }
1002
1003 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
1004 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
1005 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
1006 let e_str: &str = "65537";
1007
1008 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1009 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1010 let public_exponent =
1011 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1012
1013 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1014 .expect("Failed to create RsaPrivateKey from primes")
1015 });
1016
1017 #[cfg(feature = "idtoken")]
1018 pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1019 let secret_key_bytes = [
1020 0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1021 0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1022 0x26, 0x6c, 0x75, 0xdf,
1023 ];
1024 p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1025 });
1026
1027 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1028 RSA_PRIVATE_KEY
1029 .to_pkcs8_pem(LineEnding::LF)
1030 .expect("Failed to encode key to PKCS#8 PEM")
1031 .to_string()
1032 });
1033
1034 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1035 let decoded = String::from_utf8(
1036 base64::engine::general_purpose::URL_SAFE_NO_PAD
1037 .decode(s)
1038 .unwrap(),
1039 )
1040 .unwrap();
1041 serde_json::from_str(&decoded).unwrap()
1042 }
1043
1044 #[cfg(target_os = "windows")]
1045 #[test]
1046 #[serial_test::serial]
1047 fn adc_well_known_path_windows() {
1048 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1049 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1050 assert_eq!(
1051 adc_well_known_path(),
1052 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1053 );
1054 assert_eq!(
1055 adc_path(),
1056 Some(AdcPath::WellKnown(
1057 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1058 ))
1059 );
1060 }
1061
1062 #[cfg(target_os = "windows")]
1063 #[test]
1064 #[serial_test::serial]
1065 fn adc_well_known_path_windows_no_appdata() {
1066 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1067 let _appdata = ScopedEnv::remove("APPDATA");
1068 assert_eq!(adc_well_known_path(), None);
1069 assert_eq!(adc_path(), None);
1070 }
1071
1072 #[cfg(not(target_os = "windows"))]
1073 #[test]
1074 #[serial_test::serial]
1075 fn adc_well_known_path_posix() {
1076 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1077 let _home = ScopedEnv::set("HOME", "/home/foo");
1078 assert_eq!(
1079 adc_well_known_path(),
1080 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1081 );
1082 assert_eq!(
1083 adc_path(),
1084 Some(AdcPath::WellKnown(
1085 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1086 ))
1087 );
1088 }
1089
1090 #[cfg(not(target_os = "windows"))]
1091 #[test]
1092 #[serial_test::serial]
1093 fn adc_well_known_path_posix_no_home() {
1094 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1095 let _appdata = ScopedEnv::remove("HOME");
1096 assert_eq!(adc_well_known_path(), None);
1097 assert_eq!(adc_path(), None);
1098 }
1099
1100 #[test]
1101 #[serial_test::serial]
1102 fn adc_path_from_env() {
1103 let _creds = ScopedEnv::set(
1104 "GOOGLE_APPLICATION_CREDENTIALS",
1105 "/usr/bar/application_default_credentials.json",
1106 );
1107 assert_eq!(
1108 adc_path(),
1109 Some(AdcPath::FromEnv(
1110 "/usr/bar/application_default_credentials.json".to_string()
1111 ))
1112 );
1113 }
1114
1115 #[test]
1116 #[serial_test::serial]
1117 fn load_adc_no_well_known_path_fallback_to_mds() {
1118 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1119 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1122 }
1123
1124 #[test]
1125 #[serial_test::serial]
1126 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1127 let dir = tempfile::TempDir::new().unwrap();
1129 let path = dir.path().to_str().unwrap();
1130 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1131 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1134 }
1135
1136 #[test]
1137 #[serial_test::serial]
1138 fn load_adc_no_file_at_env_is_error() {
1139 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1140 let err = load_adc().unwrap_err();
1141 assert!(err.is_loading(), "{err:?}");
1142 let msg = format!("{err:?}");
1143 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1144 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1145 }
1146
1147 #[test]
1148 #[serial_test::serial]
1149 fn load_adc_success() {
1150 let file = tempfile::NamedTempFile::new().unwrap();
1151 let path = file.into_temp_path();
1152 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1153 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1154
1155 assert_eq!(
1156 load_adc().unwrap(),
1157 AdcContents::Contents("contents".to_string())
1158 );
1159 }
1160
1161 #[test_case(true; "retryable")]
1162 #[test_case(false; "non-retryable")]
1163 #[tokio::test]
1164 async fn error_credentials(retryable: bool) {
1165 let credentials = super::testing::error_credentials(retryable);
1166 assert!(
1167 credentials.universe_domain().await.is_none(),
1168 "{credentials:?}"
1169 );
1170 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1171 assert_eq!(err.is_transient(), retryable, "{err:?}");
1172 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1173 assert_eq!(err.is_transient(), retryable, "{err:?}");
1174 }
1175
1176 #[tokio::test]
1177 #[serial_test::serial]
1178 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1179 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1180 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1183
1184 let mds = Builder::default()
1185 .with_quota_project_id("test-quota-project")
1186 .build()
1187 .unwrap();
1188 let fmt = format!("{mds:?}");
1189 assert!(fmt.contains("MDSCredentials"));
1190 assert!(
1191 fmt.contains("env-quota-project"),
1192 "Expected 'env-quota-project', got: {fmt}"
1193 );
1194 }
1195
1196 #[tokio::test]
1197 #[serial_test::serial]
1198 async fn create_access_token_credentials_with_quota_project_from_builder() {
1199 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1200 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1203
1204 let creds = Builder::default()
1205 .with_quota_project_id("test-quota-project")
1206 .build()
1207 .unwrap();
1208 let fmt = format!("{creds:?}");
1209 assert!(
1210 fmt.contains("test-quota-project"),
1211 "Expected 'test-quota-project', got: {fmt}"
1212 );
1213 }
1214
1215 #[tokio::test]
1216 #[serial_test::serial]
1217 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1218 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1219 let mut service_account_key = serde_json::json!({
1220 "type": "service_account",
1221 "project_id": "test-project-id",
1222 "private_key_id": "test-private-key-id",
1223 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1224 "client_email": "test-client-email",
1225 "universe_domain": "test-universe-domain"
1226 });
1227
1228 let scopes =
1229 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1230
1231 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1232
1233 let file = tempfile::NamedTempFile::new().unwrap();
1234 let path = file.into_temp_path();
1235 std::fs::write(&path, service_account_key.to_string())
1236 .expect("Unable to write to temporary file.");
1237 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1238
1239 let sac = Builder::default()
1240 .with_quota_project_id("test-quota-project")
1241 .with_scopes(scopes)
1242 .build()
1243 .unwrap();
1244
1245 let headers = sac.headers(Extensions::new()).await?;
1246 let token = get_token_from_headers(headers).unwrap();
1247 let parts: Vec<_> = token.split('.').collect();
1248 assert_eq!(parts.len(), 3);
1249 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1250
1251 let fmt = format!("{sac:?}");
1252 assert!(fmt.contains("ServiceAccountCredentials"));
1253 assert!(fmt.contains("test-quota-project"));
1254 assert_eq!(claims["scope"], scopes.join(" "));
1255
1256 Ok(())
1257 }
1258
1259 #[test]
1260 fn debug_access_token() {
1261 let expires_at = Instant::now() + Duration::from_secs(3600);
1262 let token = Token {
1263 token: "token-test-only".into(),
1264 token_type: "Bearer".into(),
1265 expires_at: Some(expires_at),
1266 metadata: None,
1267 };
1268 let access_token: AccessToken = token.into();
1269 let got = format!("{access_token:?}");
1270 assert!(!got.contains("token-test-only"), "{got}");
1271 assert!(got.contains("token: \"[censored]\""), "{got}");
1272 }
1273}