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