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(crate) mod crypto_provider;
32pub mod external_account;
33pub(crate) mod external_account_sources;
34#[cfg(feature = "__gdch")]
35pub(crate) mod gdch;
36#[cfg(feature = "idtoken")]
37pub mod idtoken;
38pub mod impersonated;
39pub(crate) mod internal;
40pub mod mds;
41pub mod service_account;
42pub mod subject_token;
43pub mod user_account;
44pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
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 {
62 let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
63 Self(value)
64 }
65}
66
67#[derive(Clone, PartialEq, Debug)]
74pub enum CacheableResource<T> {
75 NotModified,
77 New {
79 entity_tag: EntityTag,
81 data: T,
83 },
84}
85
86#[derive(Clone, Debug)]
119pub struct Credentials {
120 inner: Arc<dyn dynamic::CredentialsProvider>,
129}
130
131impl<T> std::convert::From<T> for Credentials
132where
133 T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
134{
135 fn from(value: T) -> Self {
136 Self {
137 inner: Arc::new(value),
138 }
139 }
140}
141
142impl Credentials {
143 pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
174 self.inner.headers(extensions).await
175 }
176
177 pub async fn universe_domain(&self) -> Option<String> {
188 self.inner.universe_domain().await
189 }
190}
191
192#[derive(Clone, Debug)]
200pub struct AccessTokenCredentials {
201 inner: Arc<dyn dynamic::AccessTokenCredentialsProvider>,
210}
211
212impl<T> std::convert::From<T> for AccessTokenCredentials
213where
214 T: crate::credentials::AccessTokenCredentialsProvider + Send + Sync + 'static,
215{
216 fn from(value: T) -> Self {
217 Self {
218 inner: Arc::new(value),
219 }
220 }
221}
222
223impl AccessTokenCredentials {
224 pub async fn access_token(&self) -> Result<AccessToken> {
226 self.inner.access_token().await
227 }
228}
229
230impl CredentialsProvider for AccessTokenCredentials {
233 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
234 self.inner.headers(extensions).await
235 }
236
237 async fn universe_domain(&self) -> Option<String> {
238 self.inner.universe_domain().await
239 }
240}
241
242#[derive(Clone)]
244pub struct AccessToken {
245 pub token: String,
247}
248
249impl std::fmt::Debug for AccessToken {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 f.debug_struct("AccessToken")
252 .field("token", &"[censored]")
253 .finish()
254 }
255}
256
257impl std::convert::From<CacheableResource<Token>> for Result<AccessToken> {
258 fn from(token: CacheableResource<Token>) -> Self {
259 match token {
260 CacheableResource::New { data, .. } => Ok(data.into()),
261 CacheableResource::NotModified => Err(errors::CredentialsError::from_msg(
262 false,
263 "Expecting token to be present",
264 )),
265 }
266 }
267}
268
269impl std::convert::From<Token> for AccessToken {
270 fn from(token: Token) -> Self {
271 Self { token: token.token }
272 }
273}
274
275pub trait AccessTokenCredentialsProvider: CredentialsProvider + std::fmt::Debug {
281 fn access_token(&self) -> impl Future<Output = Result<AccessToken>> + Send;
283}
284
285pub trait CredentialsProvider: std::fmt::Debug {
325 fn headers(
347 &self,
348 extensions: Extensions,
349 ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
350
351 fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
353}
354
355pub(crate) mod dynamic {
356 use super::Result;
357 use super::{CacheableResource, Extensions, HeaderMap};
358
359 #[async_trait::async_trait]
361 pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
362 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
384
385 async fn universe_domain(&self) -> Option<String> {
387 Some("googleapis.com".to_string())
388 }
389 }
390
391 #[async_trait::async_trait]
393 impl<T> CredentialsProvider for T
394 where
395 T: super::CredentialsProvider + Send + Sync,
396 {
397 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
398 T::headers(self, extensions).await
399 }
400 async fn universe_domain(&self) -> Option<String> {
401 T::universe_domain(self).await
402 }
403 }
404
405 #[async_trait::async_trait]
407 pub trait AccessTokenCredentialsProvider:
408 CredentialsProvider + Send + Sync + std::fmt::Debug
409 {
410 async fn access_token(&self) -> Result<super::AccessToken>;
411 }
412
413 #[async_trait::async_trait]
414 impl<T> AccessTokenCredentialsProvider for T
415 where
416 T: super::AccessTokenCredentialsProvider + Send + Sync,
417 {
418 async fn access_token(&self) -> Result<super::AccessToken> {
419 T::access_token(self).await
420 }
421 }
422}
423
424#[derive(Debug)]
478pub struct Builder {
479 quota_project_id: Option<String>,
480 scopes: Option<Vec<String>>,
481 universe_domain: Option<String>,
482}
483
484impl Default for Builder {
485 fn default() -> Self {
497 Self {
498 quota_project_id: None,
499 scopes: None,
500 universe_domain: None,
501 }
502 }
503}
504
505impl Builder {
506 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
528 self.quota_project_id = Some(quota_project_id.into());
529 self
530 }
531
532 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
552 where
553 I: IntoIterator<Item = S>,
554 S: Into<String>,
555 {
556 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
557 self
558 }
559
560 pub fn with_universe_domain<S: Into<String>>(mut self, universe_domain: S) -> Self {
575 self.universe_domain = Some(universe_domain.into());
576 self
577 }
578
579 pub fn build(self) -> BuildResult<Credentials> {
591 Ok(self.build_access_token_credentials()?.into())
592 }
593
594 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
619 let json_data = match load_adc()? {
620 AdcContents::Contents(contents) => {
621 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
622 }
623 AdcContents::FallbackToMds => None,
624 };
625 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
626 .ok()
627 .or(self.quota_project_id);
628 build_credentials(
629 json_data,
630 quota_project_id,
631 self.scopes,
632 self.universe_domain,
633 )
634 }
635
636 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
654 let json_data = match load_adc()? {
655 AdcContents::Contents(contents) => {
656 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
657 }
658 AdcContents::FallbackToMds => None,
659 };
660 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
661 .ok()
662 .or(self.quota_project_id);
663 build_signer(
664 json_data,
665 quota_project_id,
666 self.scopes,
667 self.universe_domain,
668 )
669 }
670}
671
672#[derive(Debug, PartialEq)]
673enum AdcPath {
674 FromEnv(std::path::PathBuf),
675 WellKnown(std::path::PathBuf),
676}
677
678#[derive(Debug, PartialEq)]
679enum AdcContents {
680 Contents(String),
681 FallbackToMds,
682}
683
684fn extract_credential_type(json: &Value) -> BuildResult<&str> {
685 json.get("type")
686 .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
687 .as_str()
688 .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
689}
690
691macro_rules! config_builder {
699 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $universe_domain_option:expr, $apply_scopes_closure:expr) => {{
700 let builder = config_common_builder!(
701 $builder_instance,
702 $quota_project_id_option,
703 $scopes_option,
704 $universe_domain_option,
705 $apply_scopes_closure
706 );
707 builder.build_access_token_credentials()
708 }};
709}
710
711macro_rules! config_signer {
714 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $universe_domain_option:expr, $apply_scopes_closure:expr) => {{
715 let builder = config_common_builder!(
716 $builder_instance,
717 $quota_project_id_option,
718 $scopes_option,
719 $universe_domain_option,
720 $apply_scopes_closure
721 );
722 builder.build_signer()
723 }};
724}
725
726macro_rules! config_common_builder {
727 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $universe_domain_option:expr, $apply_scopes_closure:expr) => {{
728 let builder = $builder_instance;
729 let builder = $quota_project_id_option
730 .into_iter()
731 .fold(builder, |b, qp| b.with_quota_project_id(qp));
732
733 let builder = $universe_domain_option
734 .into_iter()
735 .fold(builder, |b, ud| b.with_universe_domain(ud));
736
737 let builder = $scopes_option
738 .into_iter()
739 .fold(builder, |b, s| $apply_scopes_closure(b, s));
740
741 builder
742 }};
743}
744
745fn build_credentials(
746 json: Option<Value>,
747 quota_project_id: Option<String>,
748 scopes: Option<Vec<String>>,
749 universe_domain: Option<String>,
750) -> BuildResult<AccessTokenCredentials> {
751 match json {
752 None => config_builder!(
753 mds::Builder::from_adc(),
754 quota_project_id,
755 scopes,
756 universe_domain.clone(),
757 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
758 ),
759 Some(json) => {
760 let cred_type = extract_credential_type(&json)?;
761 match cred_type {
762 "authorized_user" => {
763 config_builder!(
764 user_account::Builder::new(json),
765 quota_project_id,
766 scopes,
767 universe_domain.clone(),
768 |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
769 )
770 }
771 "service_account" => config_builder!(
772 service_account::Builder::new(json),
773 quota_project_id,
774 scopes,
775 universe_domain.clone(),
776 |b: service_account::Builder, s: Vec<String>| b
777 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
778 ),
779 "impersonated_service_account" => {
780 config_builder!(
781 impersonated::Builder::new(json),
782 quota_project_id,
783 scopes,
784 universe_domain.clone(),
785 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
786 )
787 }
788 "external_account" => config_builder!(
789 external_account::Builder::new(json),
790 quota_project_id,
791 scopes,
792 universe_domain.clone(),
793 |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
794 ),
795 _ => Err(BuilderError::unknown_type(cred_type)),
796 }
797 }
798 }
799}
800
801fn build_signer(
802 json: Option<Value>,
803 quota_project_id: Option<String>,
804 scopes: Option<Vec<String>>,
805 universe_domain: Option<String>,
806) -> BuildResult<crate::signer::Signer> {
807 match json {
808 None => config_signer!(
809 mds::Builder::from_adc(),
810 quota_project_id,
811 scopes,
812 universe_domain.clone(),
813 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
814 ),
815 Some(json) => {
816 let cred_type = extract_credential_type(&json)?;
817 match cred_type {
818 "authorized_user" => Err(BuilderError::not_supported(
819 "authorized_user signer is not supported",
820 )),
821 "service_account" => config_signer!(
822 service_account::Builder::new(json),
823 quota_project_id,
824 scopes,
825 universe_domain.clone(),
826 |b: service_account::Builder, s: Vec<String>| b
827 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
828 ),
829 "impersonated_service_account" => {
830 config_signer!(
831 impersonated::Builder::new(json),
832 quota_project_id,
833 scopes,
834 universe_domain.clone(),
835 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
836 )
837 }
838 "external_account" => Err(BuilderError::not_supported(
839 "external_account signer is not supported",
840 )),
841 _ => Err(BuilderError::unknown_type(cred_type)),
842 }
843 }
844 }
845}
846
847fn path_not_found(path: std::path::PathBuf) -> BuilderError {
848 BuilderError::loading(format!(
849 "{}. {}",
850 path.display(),
851 concat!(
852 "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
853 "environment variable. Verify this environment variable points to ",
854 "a valid file."
855 )
856 ))
857}
858
859fn load_adc() -> BuildResult<AdcContents> {
860 match adc_path() {
861 None => Ok(AdcContents::FallbackToMds),
862 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
863 Ok(contents) => Ok(AdcContents::Contents(contents)),
864 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
865 Err(e) => Err(BuilderError::loading(e)),
866 },
867 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(&path) {
868 Ok(contents) => Ok(AdcContents::Contents(contents)),
869 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
870 Err(e) => Err(BuilderError::loading(e)),
871 },
872 }
873}
874
875fn adc_path() -> Option<AdcPath> {
879 if let Some(path) = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS") {
880 return Some(AdcPath::FromEnv(std::path::PathBuf::from(path)));
881 }
882 Some(AdcPath::WellKnown(adc_well_known_path()?))
883}
884
885#[cfg(target_os = "windows")]
889fn adc_well_known_path() -> Option<std::path::PathBuf> {
890 std::env::var_os("APPDATA").map(|root| {
891 std::path::PathBuf::from(root).join("gcloud/application_default_credentials.json")
892 })
893}
894
895#[cfg(not(target_os = "windows"))]
899fn adc_well_known_path() -> Option<std::path::PathBuf> {
900 std::env::var_os("HOME").map(|root| {
901 std::path::PathBuf::from(root).join(".config/gcloud/application_default_credentials.json")
902 })
903}
904
905#[cfg_attr(test, mutants::skip)]
916#[doc(hidden)]
917pub mod testing {
918 use super::CacheableResource;
919 use crate::Result;
920 use crate::credentials::Credentials;
921 use crate::credentials::dynamic::CredentialsProvider;
922 use http::{Extensions, HeaderMap};
923 use std::sync::Arc;
924
925 pub fn error_credentials(retryable: bool) -> Credentials {
929 Credentials {
930 inner: Arc::from(ErrorCredentials(retryable)),
931 }
932 }
933
934 #[derive(Debug, Default)]
935 struct ErrorCredentials(bool);
936
937 #[async_trait::async_trait]
938 impl CredentialsProvider for ErrorCredentials {
939 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
940 Err(super::CredentialsError::from_msg(self.0, "test-only"))
941 }
942
943 async fn universe_domain(&self) -> Option<String> {
944 None
945 }
946 }
947}
948
949#[cfg(test)]
950pub(crate) mod tests {
951 use super::*;
952 use crate::constants::TRUST_BOUNDARY_HEADER;
953 use crate::errors::is_gax_error_retryable;
954 use base64::Engine;
955 use google_cloud_gax::backoff_policy::BackoffPolicy;
956 use google_cloud_gax::retry_policy::RetryPolicy;
957 use google_cloud_gax::retry_result::RetryResult;
958 use google_cloud_gax::retry_state::RetryState;
959 use google_cloud_gax::retry_throttler::RetryThrottler;
960 use mockall::mock;
961 use reqwest::header::AUTHORIZATION;
962 use rsa::BigUint;
963 use rsa::RsaPrivateKey;
964 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
965 use scoped_env::ScopedEnv;
966 use std::error::Error;
967 use std::sync::LazyLock;
968 use test_case::test_case;
969 use tokio::time::Duration;
970 use tokio::time::Instant;
971
972 pub(crate) fn find_source_error<'a, T: Error + 'static>(
974 error: &'a (dyn Error + 'static),
975 ) -> Option<&'a T> {
976 let mut last_err = None;
977 let mut source = error.source();
978 while let Some(err) = source {
979 if let Some(target_err) = err.downcast_ref::<T>() {
980 last_err = Some(target_err);
981 }
982 source = err.source();
983 }
984 last_err
985 }
986
987 mock! {
988 #[derive(Debug)]
989 pub RetryPolicy {}
990 impl RetryPolicy for RetryPolicy {
991 fn on_error(
992 &self,
993 state: &RetryState,
994 error: google_cloud_gax::error::Error,
995 ) -> RetryResult;
996 }
997 }
998
999 mock! {
1000 #[derive(Debug)]
1001 pub BackoffPolicy {}
1002 impl BackoffPolicy for BackoffPolicy {
1003 fn on_failure(&self, state: &RetryState) -> std::time::Duration;
1004 }
1005 }
1006
1007 mockall::mock! {
1008 #[derive(Debug)]
1009 pub RetryThrottler {}
1010 impl RetryThrottler for RetryThrottler {
1011 fn throttle_retry_attempt(&self) -> bool;
1012 fn on_retry_failure(&mut self, error: &RetryResult);
1013 fn on_success(&mut self);
1014 }
1015 }
1016
1017 mockall::mock! {
1019 #[derive(Debug)]
1020 pub Credentials {}
1021
1022 impl crate::credentials::CredentialsProvider for Credentials {
1023 async fn headers(&self, extensions: http::Extensions) -> std::result::Result<crate::credentials::CacheableResource<http::HeaderMap>, crate::errors::CredentialsError>;
1024 async fn universe_domain(&self) -> Option<String>;
1025 }
1026
1027 impl crate::credentials::AccessTokenCredentialsProvider for Credentials {
1028 async fn access_token(&self) -> std::result::Result<crate::credentials::AccessToken, crate::errors::CredentialsError>;
1029 }
1030 }
1031
1032 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
1033
1034 pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
1035 let mut retry_policy = MockRetryPolicy::new();
1036 retry_policy
1037 .expect_on_error()
1038 .returning(move |state, error| {
1039 if state.attempt_count >= attempts as u32 {
1040 return RetryResult::Exhausted(error);
1041 }
1042 let is_retryable = is_gax_error_retryable(&error);
1043 if is_retryable {
1044 RetryResult::Continue(error)
1045 } else {
1046 RetryResult::Permanent(error)
1047 }
1048 });
1049 retry_policy
1050 }
1051
1052 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
1053 let mut backoff_policy = MockBackoffPolicy::new();
1054 backoff_policy
1055 .expect_on_failure()
1056 .return_const(Duration::from_secs(0));
1057 backoff_policy
1058 }
1059
1060 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
1061 let mut throttler = MockRetryThrottler::new();
1062 throttler.expect_on_retry_failure().return_const(());
1063 throttler
1064 .expect_throttle_retry_attempt()
1065 .return_const(false);
1066 throttler.expect_on_success().return_const(());
1067 throttler
1068 }
1069
1070 pub(crate) fn get_headers_from_cache(
1071 headers: CacheableResource<HeaderMap>,
1072 ) -> Result<HeaderMap> {
1073 match headers {
1074 CacheableResource::New { data, .. } => Ok(data),
1075 CacheableResource::NotModified => Err(CredentialsError::from_msg(
1076 false,
1077 "Expecting headers to be present",
1078 )),
1079 }
1080 }
1081
1082 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
1083 match headers {
1084 CacheableResource::New { data, .. } => data
1085 .get(AUTHORIZATION)
1086 .and_then(|token_value| token_value.to_str().ok())
1087 .and_then(|s| s.split_whitespace().nth(1))
1088 .map(|s| s.to_string()),
1089 CacheableResource::NotModified => None,
1090 }
1091 }
1092
1093 pub(crate) fn get_access_boundary_from_headers(
1094 headers: CacheableResource<HeaderMap>,
1095 ) -> Option<String> {
1096 match headers {
1097 CacheableResource::New { data, .. } => data
1098 .get(TRUST_BOUNDARY_HEADER)
1099 .and_then(|token_value| token_value.to_str().ok())
1100 .map(|s| s.to_string()),
1101 CacheableResource::NotModified => None,
1102 }
1103 }
1104
1105 pub(crate) fn get_token_type_from_headers(
1106 headers: CacheableResource<HeaderMap>,
1107 ) -> Option<String> {
1108 match headers {
1109 CacheableResource::New { data, .. } => data
1110 .get(AUTHORIZATION)
1111 .and_then(|token_value| token_value.to_str().ok())
1112 .and_then(|s| s.split_whitespace().next())
1113 .map(|s| s.to_string()),
1114 CacheableResource::NotModified => None,
1115 }
1116 }
1117
1118 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
1119 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
1120 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
1121 let e_str: &str = "65537";
1122
1123 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1124 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1125 let public_exponent =
1126 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1127
1128 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1129 .expect("Failed to create RsaPrivateKey from primes")
1130 });
1131
1132 #[cfg(any(feature = "idtoken", feature = "__gdch"))]
1133 pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1134 let secret_key_bytes = [
1135 0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1136 0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1137 0x26, 0x6c, 0x75, 0xdf,
1138 ];
1139 p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1140 });
1141
1142 #[cfg(feature = "__gdch")]
1143 pub static ES256_PEM: LazyLock<String> = LazyLock::new(|| {
1144 (*ES256_PRIVATE_KEY)
1145 .to_sec1_pem(LineEnding::LF)
1146 .expect("Failed to encode EC key to PEM")
1147 .to_string()
1148 });
1149
1150 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1151 RSA_PRIVATE_KEY
1152 .to_pkcs8_pem(LineEnding::LF)
1153 .expect("Failed to encode key to PKCS#8 PEM")
1154 .to_string()
1155 });
1156
1157 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1158 let decoded = String::from_utf8(
1159 base64::engine::general_purpose::URL_SAFE_NO_PAD
1160 .decode(s)
1161 .unwrap(),
1162 )
1163 .unwrap();
1164 serde_json::from_str(&decoded).unwrap()
1165 }
1166
1167 #[cfg(target_os = "windows")]
1168 #[test]
1169 #[serial_test::serial]
1170 fn adc_well_known_path_windows() {
1171 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1172 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1173 assert_eq!(
1174 adc_well_known_path(),
1175 Some(std::path::PathBuf::from(
1176 "C:/Users/foo/gcloud/application_default_credentials.json"
1177 ))
1178 );
1179 assert_eq!(
1180 adc_path(),
1181 Some(AdcPath::WellKnown(std::path::PathBuf::from(
1182 "C:/Users/foo/gcloud/application_default_credentials.json"
1183 )))
1184 );
1185 }
1186
1187 #[cfg(target_os = "windows")]
1188 #[test]
1189 #[serial_test::serial]
1190 fn adc_well_known_path_windows_no_appdata() {
1191 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1192 let _appdata = ScopedEnv::remove("APPDATA");
1193 assert_eq!(adc_well_known_path(), None);
1194 assert_eq!(adc_path(), None);
1195 }
1196
1197 #[cfg(not(target_os = "windows"))]
1198 #[test]
1199 #[serial_test::serial]
1200 fn adc_well_known_path_posix() {
1201 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1202 let _home = ScopedEnv::set("HOME", "/home/foo");
1203 assert_eq!(
1204 adc_well_known_path(),
1205 Some(std::path::PathBuf::from(
1206 "/home/foo/.config/gcloud/application_default_credentials.json"
1207 ))
1208 );
1209 assert_eq!(
1210 adc_path(),
1211 Some(AdcPath::WellKnown(std::path::PathBuf::from(
1212 "/home/foo/.config/gcloud/application_default_credentials.json"
1213 )))
1214 );
1215 }
1216
1217 #[cfg(not(target_os = "windows"))]
1218 #[test]
1219 #[serial_test::serial]
1220 fn adc_well_known_path_posix_no_home() {
1221 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1222 let _appdata = ScopedEnv::remove("HOME");
1223 assert_eq!(adc_well_known_path(), None);
1224 assert_eq!(adc_path(), None);
1225 }
1226
1227 #[test]
1228 #[serial_test::serial]
1229 fn adc_path_from_env() {
1230 let _creds = ScopedEnv::set(
1231 "GOOGLE_APPLICATION_CREDENTIALS",
1232 "/usr/bar/application_default_credentials.json",
1233 );
1234 assert_eq!(
1235 adc_path(),
1236 Some(AdcPath::FromEnv(std::path::PathBuf::from(
1237 "/usr/bar/application_default_credentials.json"
1238 )))
1239 );
1240 }
1241
1242 #[cfg(unix)]
1243 #[test]
1244 #[serial_test::serial]
1245 fn adc_path_from_env_non_utf8() {
1246 use std::os::unix::ffi::OsStringExt;
1247 let non_utf8_bytes = vec![
1248 b'/', b'u', b's', b'r', b'/', b'b', b'a', b'r', b'/', 0xff, b'.', b'j', b's', b'o',
1249 b'n',
1250 ];
1251 let non_utf8_os_str = std::ffi::OsString::from_vec(non_utf8_bytes);
1252
1253 let _creds = ScopedEnv::set(
1254 std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
1255 non_utf8_os_str.as_os_str(),
1256 );
1257
1258 assert_eq!(
1259 adc_path(),
1260 Some(AdcPath::FromEnv(std::path::PathBuf::from(
1261 non_utf8_os_str.clone()
1262 )))
1263 );
1264 }
1265
1266 #[cfg(unix)]
1267 #[test]
1268 #[serial_test::serial]
1269 fn load_adc_no_file_at_env_is_error_non_utf8() {
1270 use std::os::unix::ffi::OsStringExt;
1271 let non_utf8_bytes = vec![
1272 b'f', b'i', b'l', b'e', b'-', 0xff, b'.', b'j', b's', b'o', b'n',
1273 ];
1274 let non_utf8_os_str = std::ffi::OsString::from_vec(non_utf8_bytes);
1275
1276 let _creds = ScopedEnv::set(
1277 std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
1278 non_utf8_os_str.as_os_str(),
1279 );
1280
1281 let err = load_adc().unwrap_err();
1282 assert!(err.is_loading(), "{err:?}");
1283 let msg = format!("{err:?}");
1284 assert!(msg.contains("file-"), "{err:?}");
1285 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1286 }
1287
1288 #[cfg(target_os = "windows")]
1289 #[test]
1290 #[serial_test::serial]
1291 fn adc_path_from_env_non_utf16() {
1292 use std::os::windows::ffi::OsStringExt;
1293 let non_utf16_wide = vec![
1294 b'C' as u16,
1295 b':' as u16,
1296 b'/' as u16,
1297 0xD800,
1298 b'.' as u16,
1299 b'j' as u16,
1300 b's' as u16,
1301 b'o' as u16,
1302 b'n' as u16,
1303 ];
1304 let non_utf16_os_str = std::ffi::OsString::from_wide(&non_utf16_wide);
1305
1306 let _creds = ScopedEnv::set(
1307 std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
1308 non_utf16_os_str.as_os_str(),
1309 );
1310
1311 assert_eq!(
1312 adc_path(),
1313 Some(AdcPath::FromEnv(std::path::PathBuf::from(
1314 non_utf16_os_str.clone()
1315 )))
1316 );
1317 }
1318
1319 #[cfg(target_os = "windows")]
1320 #[test]
1321 #[serial_test::serial]
1322 fn load_adc_no_file_at_env_is_error_non_utf16() {
1323 use std::os::windows::ffi::OsStringExt;
1324 let non_utf16_wide = vec![
1325 b'f' as u16,
1326 b'i' as u16,
1327 b'l' as u16,
1328 b'e' as u16,
1329 b'-' as u16,
1330 0xD800,
1331 b'.' as u16,
1332 b'j' as u16,
1333 b's' as u16,
1334 b'o' as u16,
1335 b'n' as u16,
1336 ];
1337 let non_utf16_os_str = std::ffi::OsString::from_wide(&non_utf16_wide);
1338
1339 let _creds = ScopedEnv::set(
1340 std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
1341 non_utf16_os_str.as_os_str(),
1342 );
1343
1344 let err = load_adc().unwrap_err();
1345 assert!(err.is_loading(), "{err:?}");
1346 let msg = format!("{err:?}");
1347 assert!(msg.contains("file-"), "{err:?}");
1348 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1349 }
1350
1351 #[test]
1352 #[serial_test::serial]
1353 fn load_adc_no_well_known_path_fallback_to_mds() {
1354 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1355 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1358 }
1359
1360 #[test]
1361 #[serial_test::serial]
1362 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1363 let dir = tempfile::TempDir::new().unwrap();
1365 let path = dir.path().to_str().unwrap();
1366 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1367 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1370 }
1371
1372 #[test]
1373 #[serial_test::serial]
1374 fn load_adc_no_file_at_env_is_error() {
1375 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1376 let err = load_adc().unwrap_err();
1377 assert!(err.is_loading(), "{err:?}");
1378 let msg = format!("{err:?}");
1379 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1380 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1381 }
1382
1383 #[test]
1384 #[serial_test::serial]
1385 fn load_adc_success() {
1386 let file = tempfile::NamedTempFile::new().unwrap();
1387 let path = file.into_temp_path();
1388 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1389 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1390
1391 assert_eq!(
1392 load_adc().unwrap(),
1393 AdcContents::Contents("contents".to_string())
1394 );
1395 }
1396
1397 #[test_case(true; "retryable")]
1398 #[test_case(false; "non-retryable")]
1399 #[tokio::test]
1400 async fn error_credentials(retryable: bool) {
1401 let credentials = super::testing::error_credentials(retryable);
1402 assert!(
1403 credentials.universe_domain().await.is_none(),
1404 "{credentials:?}"
1405 );
1406 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1407 assert_eq!(err.is_transient(), retryable, "{err:?}");
1408 let err = credentials.headers(Extensions::new()).await.err().unwrap();
1409 assert_eq!(err.is_transient(), retryable, "{err:?}");
1410 }
1411
1412 #[tokio::test]
1413 #[serial_test::serial]
1414 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1415 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1416 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1419
1420 let mds = Builder::default()
1421 .with_quota_project_id("test-quota-project")
1422 .build()
1423 .unwrap();
1424 let fmt = format!("{mds:?}");
1425 assert!(fmt.contains("MDSCredentials"));
1426 assert!(
1427 fmt.contains("env-quota-project"),
1428 "Expected 'env-quota-project', got: {fmt}"
1429 );
1430 }
1431
1432 #[tokio::test]
1433 #[serial_test::serial]
1434 async fn create_access_token_credentials_with_quota_project_from_builder() {
1435 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1436 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1439
1440 let creds = Builder::default()
1441 .with_quota_project_id("test-quota-project")
1442 .build()
1443 .unwrap();
1444 let fmt = format!("{creds:?}");
1445 assert!(
1446 fmt.contains("test-quota-project"),
1447 "Expected 'test-quota-project', got: {fmt}"
1448 );
1449 }
1450
1451 #[tokio::test]
1452 #[serial_test::serial]
1453 async fn create_access_token_credentials_with_universe_domain_from_builder() {
1454 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1455 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1458
1459 let creds = Builder::default()
1460 .with_universe_domain("my-custom-universe.com")
1461 .build()
1462 .unwrap();
1463
1464 let universe_domain = creds.universe_domain().await;
1465 assert_eq!(universe_domain, Some("my-custom-universe.com".to_string()));
1466 }
1467
1468 #[tokio::test]
1469 #[serial_test::serial]
1470 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1471 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1472 let mut service_account_key = serde_json::json!({
1473 "type": "service_account",
1474 "project_id": "test-project-id",
1475 "private_key_id": "test-private-key-id",
1476 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1477 "client_email": "test-client-email",
1478 "universe_domain": "test-universe-domain"
1479 });
1480
1481 let scopes =
1482 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1483
1484 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1485
1486 let file = tempfile::NamedTempFile::new().unwrap();
1487 let path = file.into_temp_path();
1488 std::fs::write(&path, service_account_key.to_string())
1489 .expect("Unable to write to temporary file.");
1490 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1491
1492 let sac = Builder::default()
1493 .with_quota_project_id("test-quota-project")
1494 .with_scopes(scopes)
1495 .build()
1496 .unwrap();
1497
1498 let headers = sac.headers(Extensions::new()).await?;
1499 let token = get_token_from_headers(headers).unwrap();
1500 let parts: Vec<_> = token.split('.').collect();
1501 assert_eq!(parts.len(), 3);
1502 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1503
1504 let fmt = format!("{sac:?}");
1505 assert!(fmt.contains("ServiceAccountCredentials"));
1506 assert!(fmt.contains("test-quota-project"));
1507 assert_eq!(claims["scope"], scopes.join(" "));
1508
1509 Ok(())
1510 }
1511
1512 #[test]
1513 fn debug_access_token() {
1514 let expires_at = Instant::now() + Duration::from_secs(3600);
1515 let token = Token {
1516 token: "token-test-only".into(),
1517 token_type: "Bearer".into(),
1518 expires_at: Some(expires_at),
1519 metadata: None,
1520 };
1521 let access_token: AccessToken = token.into();
1522 let got = format!("{access_token:?}");
1523 assert!(!got.contains("token-test-only"), "{got}");
1524 assert!(got.contains("token: \"[censored]\""), "{got}");
1525 }
1526}