1use crate::access_boundary::CredentialsWithAccessBoundary;
93use crate::build_errors::Error as BuilderError;
94use crate::constants::DEFAULT_SCOPE;
95use crate::credentials::dynamic::{AccessTokenCredentialsProvider, CredentialsProvider};
96use crate::credentials::{
97 AccessToken, AccessTokenCredentials, CacheableResource, Credentials, build_credentials,
98 extract_credential_type,
99};
100use crate::errors::{self, CredentialsError};
101use crate::headers_util::{
102 self, ACCESS_TOKEN_REQUEST_TYPE, AuthHeadersBuilder, metrics_header_value,
103};
104use crate::retry::{Builder as RetryTokenProviderBuilder, TokenProviderWithRetry};
105use crate::token::{CachedTokenProvider, Token, TokenProvider};
106use crate::token_cache::TokenCache;
107use crate::{BuildResult, Result};
108use async_trait::async_trait;
109use google_cloud_gax::Result as GaxResult;
110use google_cloud_gax::backoff_policy::{BackoffPolicy, BackoffPolicyArg};
111use google_cloud_gax::error::Error as GaxError;
112use google_cloud_gax::exponential_backoff::ExponentialBackoff;
113use google_cloud_gax::retry_loop_internal::retry_loop;
114use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicy, RetryPolicyArg, RetryPolicyExt};
115use google_cloud_gax::retry_throttler::{
116 AdaptiveThrottler, RetryThrottlerArg, SharedRetryThrottler,
117};
118use http::{Extensions, HeaderMap};
119use reqwest::Client;
120use serde_json::Value;
121use std::fmt::Debug;
122use std::sync::Arc;
123use std::time::Duration;
124use time::OffsetDateTime;
125use tokio::time::Instant;
126
127pub(crate) const IMPERSONATED_CREDENTIAL_TYPE: &str = "imp";
128pub(crate) const DEFAULT_LIFETIME: Duration = Duration::from_secs(3600);
129pub(crate) const MSG: &str = "failed to fetch token";
130
131#[derive(Debug, Clone)]
132pub(crate) enum BuilderSource {
133 FromJson(Value),
134 FromCredentials(Credentials),
135}
136
137pub struct Builder {
157 source: BuilderSource,
158 service_account_impersonation_url: Option<ImpersonationUrl>,
159 delegates: Option<Vec<String>>,
160 scopes: Option<Vec<String>>,
161 quota_project_id: Option<String>,
162 universe_domain: Option<String>,
163 lifetime: Option<Duration>,
164 retry_builder: RetryTokenProviderBuilder,
165 iam_endpoint_override: Option<String>,
166 is_access_boundary_enabled: bool,
167}
168
169#[derive(Debug, Clone)]
170pub(crate) struct ImpersonationUrl {
171 pub(crate) endpoint: Option<String>,
173 kind: ImpersonationUrlKind,
174}
175
176#[derive(Debug, Clone)]
177pub(crate) enum ImpersonationUrlKind {
178 TargetPrincipal(String),
179 Exact(String),
180}
181
182impl ImpersonationUrl {
183 pub(crate) fn exact(url: String) -> Self {
184 Self {
185 endpoint: None,
186 kind: ImpersonationUrlKind::Exact(url),
187 }
188 }
189
190 pub(crate) fn target_principal(principal: String) -> Self {
191 Self {
192 endpoint: None,
193 kind: ImpersonationUrlKind::TargetPrincipal(principal),
194 }
195 }
196
197 pub(crate) async fn access_token_url(&self, creds: &Credentials) -> String {
198 match &self.kind {
199 ImpersonationUrlKind::TargetPrincipal(principal) => {
200 self.impersonation_url_for_method(creds, principal, "generateAccessToken")
201 .await
202 }
203 ImpersonationUrlKind::Exact(url) => url.clone(),
204 }
205 }
206
207 #[cfg(feature = "idtoken")]
208 pub(crate) async fn id_token_url(&self, creds: &Credentials) -> String {
209 match &self.kind {
210 ImpersonationUrlKind::TargetPrincipal(principal) => {
211 self.impersonation_url_for_method(creds, principal, "generateIdToken")
212 .await
213 }
214 ImpersonationUrlKind::Exact(url) => {
215 url.replace("generateAccessToken", "generateIdToken")
216 }
217 }
218 }
219
220 async fn impersonation_url_for_method(
221 &self,
222 creds: &Credentials,
223 principal: &str,
224 method: &str,
225 ) -> String {
226 let universe_domain = crate::universe_domain::resolve(creds).await;
227 let endpoint = match &self.endpoint {
228 Some(endpoint) => endpoint.to_string(),
229 None => format!("https://iamcredentials.{}", universe_domain),
230 };
231 format!(
232 "{}/v1/projects/-/serviceAccounts/{}:{}",
233 endpoint, principal, method
234 )
235 }
236
237 pub(crate) fn client_email(self) -> BuildResult<String> {
238 match self.kind {
239 ImpersonationUrlKind::TargetPrincipal(client_email) => Ok(client_email),
240 ImpersonationUrlKind::Exact(url) => extract_client_email(&url),
241 }
242 }
243}
244
245impl Builder {
246 pub fn new(impersonated_credential: Value) -> Self {
254 Self {
255 source: BuilderSource::FromJson(impersonated_credential),
256 service_account_impersonation_url: None,
257 delegates: None,
258 scopes: None,
259 quota_project_id: None,
260 universe_domain: None,
261 lifetime: None,
262 retry_builder: RetryTokenProviderBuilder::default(),
263 iam_endpoint_override: None,
264 is_access_boundary_enabled: true,
265 }
266 }
267
268 pub fn from_source_credentials(source_credentials: Credentials) -> Self {
285 Self {
286 source: BuilderSource::FromCredentials(source_credentials),
287 service_account_impersonation_url: None,
288 delegates: None,
289 scopes: None,
290 quota_project_id: None,
291 universe_domain: None,
292 lifetime: None,
293 retry_builder: RetryTokenProviderBuilder::default(),
294 iam_endpoint_override: None,
295 is_access_boundary_enabled: true,
296 }
297 }
298
299 pub fn with_target_principal<S: Into<String>>(mut self, target_principal: S) -> Self {
316 self.service_account_impersonation_url =
317 Some(ImpersonationUrl::target_principal(target_principal.into()));
318 self
319 }
320
321 pub fn with_delegates<I, S>(mut self, delegates: I) -> Self
337 where
338 I: IntoIterator<Item = S>,
339 S: Into<String>,
340 {
341 self.delegates = Some(delegates.into_iter().map(|s| s.into()).collect());
342 self
343 }
344
345 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
367 where
368 I: IntoIterator<Item = S>,
369 S: Into<String>,
370 {
371 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
372 self
373 }
374
375 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
401 self.quota_project_id = Some(quota_project_id.into());
402 self
403 }
404
405 pub fn with_universe_domain<S: Into<String>>(mut self, universe_domain: S) -> Self {
427 self.universe_domain = Some(universe_domain.into());
428 self
429 }
430
431 pub fn with_lifetime(mut self, lifetime: Duration) -> Self {
448 self.lifetime = Some(lifetime);
449 self
450 }
451
452 pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
469 self.retry_builder = self.retry_builder.with_retry_policy(v.into());
470 self
471 }
472
473 pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
491 self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
492 self
493 }
494
495 pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
518 self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
519 self
520 }
521
522 pub fn build(self) -> BuildResult<Credentials> {
539 Ok(self.build_credentials()?.into())
540 }
541
542 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
583 Ok(self.build_credentials()?.into())
584 }
585
586 fn build_credentials(
587 self,
588 ) -> BuildResult<CredentialsWithAccessBoundary<ImpersonatedServiceAccount<TokenCache>>> {
589 let is_access_boundary_enabled = self.is_access_boundary_enabled;
590 let impersonation_url = self.resolve_impersonation_url()?;
591 let client_email = impersonation_url.client_email()?;
592 let iam_endpoint_override = self.iam_endpoint_override.clone();
593 let universe_domain_override = self.universe_domain.clone();
594 let (token_provider, quota_project_id, source_credentials) = self.build_components()?;
595 let access_boundary_url = crate::access_boundary::service_account_lookup_url(
596 &client_email,
597 iam_endpoint_override.as_deref(),
598 );
599 let creds = ImpersonatedServiceAccount {
600 token_provider: TokenCache::new(token_provider),
601 quota_project_id,
602 universe_domain_override,
603 source_credentials,
604 };
605
606 if !is_access_boundary_enabled {
607 return Ok(CredentialsWithAccessBoundary::new_no_op(creds));
608 }
609
610 Ok(CredentialsWithAccessBoundary::new(
611 creds,
612 Some(access_boundary_url),
613 ))
614 }
615
616 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
649 let iam_endpoint = self.iam_endpoint_override.clone();
650 let source = self.source.clone();
651 if let BuilderSource::FromJson(json) = source
652 && let Some(signer) = build_signer_from_json(json.clone())?
653 {
654 return Ok(signer);
655 }
656 let impersonation_url = self.resolve_impersonation_url()?;
657 let client_email = impersonation_url.client_email()?;
658 let creds = self.build()?;
659 let signer = crate::signer::iam::IamSigner::new(client_email, creds, iam_endpoint);
660 Ok(crate::signer::Signer {
661 inner: Arc::new(signer),
662 })
663 }
664
665 fn build_components(
666 self,
667 ) -> BuildResult<(
668 TokenProviderWithRetry<ImpersonatedTokenProvider>,
669 Option<String>,
670 Credentials,
671 )> {
672 let components = match self.source {
673 BuilderSource::FromJson(json) => build_components_from_json(json)?,
674 BuilderSource::FromCredentials(source_credentials) => {
675 build_components_from_credentials(
676 source_credentials,
677 self.service_account_impersonation_url,
678 )?
679 }
680 };
681
682 let scopes = self
683 .scopes
684 .or(components.scopes)
685 .unwrap_or_else(|| vec![DEFAULT_SCOPE.to_string()]);
686
687 let quota_project_id = self.quota_project_id.or(components.quota_project_id);
688 let delegates = self.delegates.or(components.delegates);
689
690 let source_credentials = components.source_credentials;
691 let token_provider = ImpersonatedTokenProvider {
692 source_credentials: source_credentials.clone(),
693 service_account_impersonation_url: components.service_account_impersonation_url,
694 delegates,
695 scopes,
696 lifetime: self.lifetime.unwrap_or(DEFAULT_LIFETIME),
697 };
698 let token_provider = self.retry_builder.build(token_provider);
699 Ok((token_provider, quota_project_id, source_credentials))
700 }
701
702 fn resolve_impersonation_url(&self) -> BuildResult<ImpersonationUrl> {
703 match self.source.clone() {
704 BuilderSource::FromJson(json) => {
705 let config = config_from_json(json)?;
706 Ok(ImpersonationUrl::exact(config.service_account_impersonation_url))
707 }
708 BuilderSource::FromCredentials(_) => {
709 self.service_account_impersonation_url.clone().ok_or_else(|| {
710 BuilderError::parsing(
711 "`service_account_impersonation_url` is required when building from source credentials",
712 )
713 })
714 }
715 }
716 }
717}
718
719pub(crate) struct ImpersonatedCredentialComponents {
720 pub(crate) source_credentials: Credentials,
721 pub(crate) service_account_impersonation_url: ImpersonationUrl,
722 pub(crate) delegates: Option<Vec<String>>,
723 pub(crate) quota_project_id: Option<String>,
724 pub(crate) scopes: Option<Vec<String>>,
725}
726
727fn config_from_json(json: Value) -> BuildResult<ImpersonatedConfig> {
728 serde_json::from_value::<ImpersonatedConfig>(json).map_err(BuilderError::parsing)
729}
730
731pub(crate) fn build_components_from_json(
732 json: Value,
733) -> BuildResult<ImpersonatedCredentialComponents> {
734 let config = config_from_json(json)?;
735
736 let source_credential_type = extract_credential_type(&config.source_credentials)?;
737 if source_credential_type == "impersonated_service_account" {
738 return Err(BuilderError::parsing(
739 "source credential of type `impersonated_service_account` is not supported. \
740 Use the `delegates` field to specify a delegation chain.",
741 ));
742 }
743
744 let source_credentials =
750 build_credentials(Some(config.source_credentials), None, None, None)?.into();
751
752 Ok(ImpersonatedCredentialComponents {
753 source_credentials,
754 service_account_impersonation_url: ImpersonationUrl::exact(
755 config.service_account_impersonation_url,
756 ),
757 delegates: config.delegates,
758 quota_project_id: config.quota_project_id,
759 scopes: config.scopes,
760 })
761}
762
763fn build_signer_from_json(json: Value) -> BuildResult<Option<crate::signer::Signer>> {
767 use crate::credentials::service_account::ServiceAccountKey;
768 use crate::signer::service_account::ServiceAccountSigner;
769
770 let config = config_from_json(json)?;
771
772 let client_email = extract_client_email(&config.service_account_impersonation_url)?;
773 let source_credential_type = extract_credential_type(&config.source_credentials)?;
774 if source_credential_type == "service_account" {
775 let service_account_key =
776 serde_json::from_value::<ServiceAccountKey>(config.source_credentials)
777 .map_err(BuilderError::parsing)?;
778 let signing_provider = ServiceAccountSigner::from_impersonated_service_account(
779 service_account_key,
780 client_email,
781 );
782 let signer = crate::signer::Signer {
783 inner: Arc::new(signing_provider),
784 };
785 return Ok(Some(signer));
786 }
787 Ok(None)
788}
789
790fn extract_client_email(service_account_impersonation_url: &str) -> BuildResult<String> {
791 let mut parts = service_account_impersonation_url.split("/serviceAccounts/");
792 match (parts.nth(1), parts.next()) {
793 (Some(email), None) => Ok(email.trim_end_matches(":generateAccessToken").to_string()),
794 _ => Err(BuilderError::parsing(
795 "invalid service account impersonation URL",
796 )),
797 }
798}
799
800pub(crate) fn build_components_from_credentials(
801 source_credentials: Credentials,
802 impersonation_url: Option<ImpersonationUrl>,
803) -> BuildResult<ImpersonatedCredentialComponents> {
804 let url = impersonation_url.ok_or_else(|| {
805 BuilderError::parsing(
806 "`target_principal` is required when building from source credentials",
807 )
808 })?;
809 Ok(ImpersonatedCredentialComponents {
810 source_credentials,
811 service_account_impersonation_url: url,
812 delegates: None,
813 quota_project_id: None,
814 scopes: None,
815 })
816}
817
818#[derive(serde::Deserialize, Debug, PartialEq)]
819struct ImpersonatedConfig {
820 service_account_impersonation_url: String,
821 source_credentials: Value,
822 delegates: Option<Vec<String>>,
823 quota_project_id: Option<String>,
824 scopes: Option<Vec<String>>,
825 universe_domain: Option<String>,
826}
827
828#[derive(Debug)]
829struct ImpersonatedServiceAccount<T>
830where
831 T: CachedTokenProvider,
832{
833 token_provider: T,
834 quota_project_id: Option<String>,
835 universe_domain_override: Option<String>,
836 source_credentials: Credentials,
837}
838
839#[async_trait::async_trait]
840impl<T> CredentialsProvider for ImpersonatedServiceAccount<T>
841where
842 T: CachedTokenProvider,
843{
844 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
845 let token = self.token_provider.token(extensions).await?;
846
847 AuthHeadersBuilder::new(&token)
848 .maybe_quota_project_id(self.quota_project_id.as_deref())
849 .build()
850 }
851
852 async fn universe_domain(&self) -> Option<String> {
853 if let Some(universe_domain) = &self.universe_domain_override {
854 return Some(universe_domain.clone());
855 }
856 self.source_credentials.universe_domain().await
857 }
858}
859
860#[async_trait::async_trait]
861impl<T> AccessTokenCredentialsProvider for ImpersonatedServiceAccount<T>
862where
863 T: CachedTokenProvider,
864{
865 async fn access_token(&self) -> Result<AccessToken> {
866 let token = self.token_provider.token(Extensions::new()).await?;
867 token.into()
868 }
869}
870
871struct ImpersonatedTokenProvider {
872 source_credentials: Credentials,
873 service_account_impersonation_url: ImpersonationUrl,
874 delegates: Option<Vec<String>>,
875 scopes: Vec<String>,
876 lifetime: Duration,
877}
878
879impl Debug for ImpersonatedTokenProvider {
880 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
881 f.debug_struct("ImpersonatedTokenProvider")
882 .field("source_credentials", &self.source_credentials)
883 .field(
884 "service_account_impersonation_url",
885 &self.service_account_impersonation_url,
886 )
887 .field("delegates", &self.delegates)
888 .field("scopes", &self.scopes)
889 .field("lifetime", &self.lifetime)
890 .finish()
891 }
892}
893
894#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
895struct GenerateAccessTokenRequest {
896 #[serde(skip_serializing_if = "Option::is_none")]
897 delegates: Option<Vec<String>>,
898 scope: Vec<String>,
899 lifetime: String,
900}
901
902pub(crate) async fn generate_access_token(
903 source_headers: HeaderMap,
904 delegates: Option<Vec<String>>,
905 scopes: Vec<String>,
906 lifetime: Duration,
907 service_account_impersonation_url: &str,
908) -> Result<Token> {
909 GenerateAccessTokenClient::new(
910 source_headers,
911 delegates,
912 scopes,
913 lifetime,
914 service_account_impersonation_url.to_string(),
915 )
916 .fetch()
917 .await
918}
919
920async fn generate_access_token_call(
921 client: &Client,
922 url: &str,
923 source_headers: HeaderMap,
924 body: &GenerateAccessTokenRequest,
925) -> GaxResult<reqwest::Response> {
926 let response = client
927 .post(url)
928 .header("Content-Type", "application/json")
929 .header(
930 headers_util::X_GOOG_API_CLIENT,
931 metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
932 )
933 .headers(source_headers)
934 .json(body)
935 .send()
936 .await
937 .map_err(GaxError::io)?;
938
939 let status = response.status();
940 if !status.is_success() {
941 let err_headers = response.headers().clone();
942 let err_payload = response
943 .bytes()
944 .await
945 .map_err(|e| GaxError::transport(err_headers.clone(), e))?;
946 return Err(GaxError::http(status.as_u16(), err_headers, err_payload));
947 }
948
949 Ok(response)
950}
951
952#[derive(Debug)]
953struct GenerateAccessTokenClient {
954 source_headers: HeaderMap,
955 delegates: Option<Vec<String>>,
956 scopes: Vec<String>,
957 lifetime: Duration,
958 url: String,
959 retry_policy: Arc<dyn RetryPolicy>,
960 backoff_policy: Arc<dyn BackoffPolicy>,
961}
962
963impl GenerateAccessTokenClient {
964 fn new(
965 source_headers: HeaderMap,
966 delegates: Option<Vec<String>>,
967 scopes: Vec<String>,
968 lifetime: Duration,
969 url: String,
970 ) -> Self {
971 let retry_policy = Aip194Strict
972 .continue_on_too_many_requests()
973 .continue_on_client_timeout()
974 .with_time_limit(Duration::from_secs(60));
975 let backoff_policy = ExponentialBackoff::default();
976
977 Self {
978 source_headers,
979 delegates,
980 scopes,
981 lifetime,
982 url,
983 retry_policy: Arc::new(retry_policy),
984 backoff_policy: Arc::new(backoff_policy),
985 }
986 }
987
988 async fn fetch(self) -> Result<Token> {
989 let client = Client::new();
990 let body = GenerateAccessTokenRequest {
991 delegates: self.delegates,
992 scope: self.scopes,
993 lifetime: format!("{}s", self.lifetime.as_secs_f64()),
994 };
995
996 let sleep = async |d| tokio::time::sleep(d).await;
997 let retry_throttler: RetryThrottlerArg = AdaptiveThrottler::default().into();
998 let retry_throttler: SharedRetryThrottler = retry_throttler.into();
999 let url = self.url;
1000 let source_headers = self.source_headers;
1001
1002 let response = retry_loop(
1003 async move |d| {
1004 let attempt =
1005 generate_access_token_call(&client, &url, source_headers.clone(), &body);
1006 let max_time_limit = Duration::from_secs(10);
1007 let time_limit = d.map_or(max_time_limit, |d| d.min(max_time_limit));
1008 match tokio::time::timeout(time_limit, attempt).await {
1009 Ok(r) => r,
1010 Err(e) => Err(GaxError::timeout(e)),
1011 }
1012 },
1013 sleep,
1014 true, retry_throttler,
1016 self.retry_policy.clone(),
1017 self.backoff_policy.clone(),
1018 )
1019 .await
1020 .map_err(|e| errors::from_gax_error(e, MSG))?;
1021
1022 let token_response = response
1023 .json::<GenerateAccessTokenResponse>()
1024 .await
1025 .map_err(|e| {
1026 let retryable = !e.is_decode();
1027 CredentialsError::from_source(retryable, e)
1028 })?;
1029
1030 let parsed_dt = OffsetDateTime::parse(
1031 &token_response.expire_time,
1032 &time::format_description::well_known::Rfc3339,
1033 )
1034 .map_err(errors::non_retryable)?;
1035
1036 let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
1037 let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
1038
1039 let token = Token {
1040 token: token_response.access_token,
1041 token_type: "Bearer".to_string(),
1042 expires_at: Some(expires_at),
1043 metadata: None,
1044 };
1045 Ok(token)
1046 }
1047}
1048
1049#[async_trait]
1050impl TokenProvider for ImpersonatedTokenProvider {
1051 async fn token(&self) -> Result<Token> {
1052 let source_headers = self.source_credentials.headers(Extensions::new()).await?;
1053 let source_headers = match source_headers {
1054 CacheableResource::New { data, .. } => data,
1055 CacheableResource::NotModified => {
1056 unreachable!("requested source credentials without a caching etag")
1057 }
1058 };
1059
1060 let url = self
1066 .service_account_impersonation_url
1067 .access_token_url(&self.source_credentials)
1068 .await;
1069
1070 generate_access_token(
1071 source_headers,
1072 self.delegates.clone(),
1073 self.scopes.clone(),
1074 self.lifetime,
1075 &url,
1076 )
1077 .await
1078 }
1079}
1080
1081#[derive(serde::Deserialize)]
1082struct GenerateAccessTokenResponse {
1083 #[serde(rename = "accessToken")]
1084 access_token: String,
1085 #[serde(rename = "expireTime")]
1086 expire_time: String,
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use crate::credentials::service_account::ServiceAccountKey;
1093 use crate::credentials::tests::MockCredentials;
1094 use crate::credentials::tests::PKCS8_PK;
1095 use crate::credentials::tests::{
1096 find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
1097 get_mock_retry_throttler,
1098 };
1099 use crate::errors::CredentialsError;
1100 use crate::universe_domain::is_default_universe_domain;
1101 use base64::{Engine, prelude::BASE64_STANDARD};
1102 use httptest::cycle;
1103 use httptest::{Expectation, Server, matchers::*, responders::*};
1104 use serde_json::Value;
1105 use serde_json::json;
1106 use serial_test::parallel;
1107 use test_case::test_case;
1108
1109 type TestResult = anyhow::Result<()>;
1110
1111 impl Builder {
1112 fn maybe_iam_endpoint_override(mut self, iam_endpoint_override: Option<String>) -> Self {
1113 self.iam_endpoint_override = iam_endpoint_override;
1114 self
1115 }
1116
1117 fn without_access_boundary(mut self) -> Self {
1118 self.is_access_boundary_enabled = false;
1119 self
1120 }
1121
1122 fn with_impersonation_endpoint(mut self, endpoint: &str) -> Self {
1123 self.service_account_impersonation_url = self
1124 .service_account_impersonation_url
1125 .map(|u| u.with_endpoint(endpoint));
1126 self
1127 }
1128 }
1129
1130 impl ImpersonationUrl {
1131 pub(crate) fn with_endpoint(mut self, endpoint: &str) -> Self {
1132 self.endpoint = Some(endpoint.to_string());
1133 self
1134 }
1135 }
1136
1137 #[tokio::test]
1138 #[parallel]
1139 async fn test_generate_access_token_client_retry_success() -> TestResult {
1140 let server = Server::run();
1141 let impersonation_path =
1142 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1143 server.expect(
1144 Expectation::matching(request::method_path("POST", impersonation_path))
1145 .times(2)
1146 .respond_with(cycle![
1147 status_code(503).body("try-again"),
1148 json_encoded(json!({
1149 "accessToken": "impersonated-token-success",
1150 "expireTime": "2030-01-01T00:00:00Z",
1151 })),
1152 ]),
1153 );
1154
1155 let url = server.url(impersonation_path).to_string();
1156 let mut client = GenerateAccessTokenClient::new(
1157 HeaderMap::new(),
1158 None,
1159 vec!["scope1".to_string()],
1160 Duration::from_secs(3600),
1161 url,
1162 );
1163 client.retry_policy = Arc::new(get_mock_auth_retry_policy(3));
1164 client.backoff_policy = Arc::new(get_mock_backoff_policy());
1165
1166 let token = client.fetch().await?;
1167 assert_eq!(token.token, "impersonated-token-success");
1168 Ok(())
1169 }
1170
1171 #[tokio::test]
1172 #[parallel]
1173 async fn test_generate_access_token_client_retry_exhausted() {
1174 let server = Server::run();
1175 let impersonation_path =
1176 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1177 server.expect(
1178 Expectation::matching(request::method_path("POST", impersonation_path))
1179 .times(3)
1180 .respond_with(status_code(503)),
1181 );
1182
1183 let url = server.url(impersonation_path).to_string();
1184 let mut client = GenerateAccessTokenClient::new(
1185 HeaderMap::new(),
1186 None,
1187 vec!["scope1".to_string()],
1188 Duration::from_secs(3600),
1189 url,
1190 );
1191 client.retry_policy = Arc::new(get_mock_auth_retry_policy(3));
1192 client.backoff_policy = Arc::new(get_mock_backoff_policy());
1193
1194 let err = client.fetch().await.unwrap_err();
1195 assert!(err.is_transient(), "{err:?}");
1196 }
1197
1198 #[tokio::test]
1199 #[parallel]
1200 async fn test_generate_access_token_success() -> TestResult {
1201 let server = Server::run();
1202 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1203 .format(&time::format_description::well_known::Rfc3339)
1204 .unwrap();
1205 server.expect(
1206 Expectation::matching(all_of![
1207 request::method_path(
1208 "POST",
1209 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1210 ),
1211 request::headers(contains(("authorization", "Bearer test-token"))),
1212 ])
1213 .respond_with(json_encoded(json!({
1214 "accessToken": "test-impersonated-token",
1215 "expireTime": expire_time
1216 }))),
1217 );
1218
1219 let mut headers = HeaderMap::new();
1220 headers.insert("authorization", "Bearer test-token".parse().unwrap());
1221 let token = generate_access_token(
1222 headers,
1223 None,
1224 vec!["scope".to_string()],
1225 DEFAULT_LIFETIME,
1226 &server
1227 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1228 .to_string(),
1229 )
1230 .await?;
1231
1232 assert_eq!(token.token, "test-impersonated-token");
1233 Ok(())
1234 }
1235
1236 #[tokio::test]
1237 #[parallel]
1238 async fn test_generate_access_token_403() -> TestResult {
1239 let server = Server::run();
1240 server.expect(
1241 Expectation::matching(all_of![
1242 request::method_path(
1243 "POST",
1244 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1245 ),
1246 request::headers(contains(("authorization", "Bearer test-token"))),
1247 ])
1248 .respond_with(status_code(403)),
1249 );
1250
1251 let mut headers = HeaderMap::new();
1252 headers.insert("authorization", "Bearer test-token".parse().unwrap());
1253 let err = generate_access_token(
1254 headers,
1255 None,
1256 vec!["scope".to_string()],
1257 DEFAULT_LIFETIME,
1258 &server
1259 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1260 .to_string(),
1261 )
1262 .await
1263 .unwrap_err();
1264
1265 assert!(!err.is_transient());
1266 Ok(())
1267 }
1268
1269 #[tokio::test]
1270 #[parallel]
1271 async fn test_generate_access_token_no_auth_header() -> TestResult {
1272 let server = Server::run();
1273 server.expect(
1274 Expectation::matching(request::method_path(
1275 "POST",
1276 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1277 ))
1278 .respond_with(status_code(401)),
1279 );
1280
1281 let err = generate_access_token(
1282 HeaderMap::new(),
1283 None,
1284 vec!["scope".to_string()],
1285 DEFAULT_LIFETIME,
1286 &server
1287 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1288 .to_string(),
1289 )
1290 .await
1291 .unwrap_err();
1292
1293 assert!(!err.is_transient());
1294 Ok(())
1295 }
1296
1297 #[tokio::test]
1298 #[parallel]
1299 async fn test_impersonated_service_account() -> TestResult {
1300 let server = Server::run();
1301 server.expect(
1302 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1303 json_encoded(json!({
1304 "access_token": "test-user-account-token",
1305 "expires_in": 3600,
1306 "token_type": "Bearer",
1307 })),
1308 ),
1309 );
1310 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1311 .format(&time::format_description::well_known::Rfc3339)
1312 .unwrap();
1313 server.expect(
1314 Expectation::matching(all_of![
1315 request::method_path(
1316 "POST",
1317 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1318 ),
1319 request::headers(contains((
1320 "authorization",
1321 "Bearer test-user-account-token"
1322 ))),
1323 request::body(json_decoded(eq(json!({
1324 "scope": ["scope1", "scope2"],
1325 "lifetime": "3600s"
1326 }))))
1327 ])
1328 .respond_with(json_encoded(json!({
1329 "accessToken": "test-impersonated-token",
1330 "expireTime": expire_time
1331 }))),
1332 );
1333
1334 let impersonated_credential = json!({
1335 "type": "impersonated_service_account",
1336 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1337 "source_credentials": {
1338 "type": "authorized_user",
1339 "client_id": "test-client-id",
1340 "client_secret": "test-client-secret",
1341 "refresh_token": "test-refresh-token",
1342 "token_uri": server.url("/token").to_string()
1343 }
1344 });
1345 let (token_provider, _, _) = Builder::new(impersonated_credential)
1346 .with_scopes(vec!["scope1", "scope2"])
1347 .build_components()?;
1348
1349 let token = token_provider.token().await?;
1350 assert_eq!(token.token, "test-impersonated-token");
1351 assert_eq!(token.token_type, "Bearer");
1352
1353 Ok(())
1354 }
1355
1356 #[tokio::test]
1357 #[parallel]
1358 async fn test_impersonated_service_account_default_scope() -> TestResult {
1359 let server = Server::run();
1360 server.expect(
1361 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1362 json_encoded(json!({
1363 "access_token": "test-user-account-token",
1364 "expires_in": 3600,
1365 "token_type": "Bearer",
1366 })),
1367 ),
1368 );
1369 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1370 .format(&time::format_description::well_known::Rfc3339)
1371 .unwrap();
1372 server.expect(
1373 Expectation::matching(all_of![
1374 request::method_path(
1375 "POST",
1376 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1377 ),
1378 request::headers(contains((
1379 "authorization",
1380 "Bearer test-user-account-token"
1381 ))),
1382 request::body(json_decoded(eq(json!({
1383 "scope": [DEFAULT_SCOPE],
1384 "lifetime": "3600s"
1385 }))))
1386 ])
1387 .respond_with(json_encoded(json!({
1388 "accessToken": "test-impersonated-token",
1389 "expireTime": expire_time
1390 }))),
1391 );
1392
1393 let impersonated_credential = json!({
1394 "type": "impersonated_service_account",
1395 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1396 "source_credentials": {
1397 "type": "authorized_user",
1398 "client_id": "test-client-id",
1399 "client_secret": "test-client-secret",
1400 "refresh_token": "test-refresh-token",
1401 "token_uri": server.url("/token").to_string()
1402 }
1403 });
1404 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1405
1406 let token = token_provider.token().await?;
1407 assert_eq!(token.token, "test-impersonated-token");
1408 assert_eq!(token.token_type, "Bearer");
1409
1410 Ok(())
1411 }
1412
1413 #[tokio::test]
1414 #[parallel]
1415 async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
1416 let server = Server::run();
1417 server.expect(
1418 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1419 json_encoded(json!({
1420 "access_token": "test-user-account-token",
1421 "expires_in": 3600,
1422 "token_type": "Bearer",
1423 })),
1424 ),
1425 );
1426 let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
1427 .format(&time::format_description::well_known::Rfc3339)
1428 .unwrap();
1429 server.expect(
1430 Expectation::matching(all_of![
1431 request::method_path(
1432 "POST",
1433 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1434 ),
1435 request::headers(contains((
1436 "authorization",
1437 "Bearer test-user-account-token"
1438 ))),
1439 request::body(json_decoded(eq(json!({
1440 "scope": ["scope1", "scope2"],
1441 "lifetime": "3.5s"
1442 }))))
1443 ])
1444 .respond_with(json_encoded(json!({
1445 "accessToken": "test-impersonated-token",
1446 "expireTime": expire_time
1447 }))),
1448 );
1449
1450 let impersonated_credential = json!({
1451 "type": "impersonated_service_account",
1452 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1453 "source_credentials": {
1454 "type": "authorized_user",
1455 "client_id": "test-client-id",
1456 "client_secret": "test-client-secret",
1457 "refresh_token": "test-refresh-token",
1458 "token_uri": server.url("/token").to_string()
1459 }
1460 });
1461 let (token_provider, _, _) = Builder::new(impersonated_credential)
1462 .with_scopes(vec!["scope1", "scope2"])
1463 .with_lifetime(Duration::from_secs_f32(3.5))
1464 .build_components()?;
1465
1466 let token = token_provider.token().await?;
1467 assert_eq!(token.token, "test-impersonated-token");
1468
1469 Ok(())
1470 }
1471
1472 #[tokio::test]
1473 #[parallel]
1474 async fn test_with_delegates() -> TestResult {
1475 let server = Server::run();
1476 server.expect(
1477 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1478 json_encoded(json!({
1479 "access_token": "test-user-account-token",
1480 "expires_in": 3600,
1481 "token_type": "Bearer",
1482 })),
1483 ),
1484 );
1485 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1486 .format(&time::format_description::well_known::Rfc3339)
1487 .unwrap();
1488 server.expect(
1489 Expectation::matching(all_of![
1490 request::method_path(
1491 "POST",
1492 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1493 ),
1494 request::headers(contains((
1495 "authorization",
1496 "Bearer test-user-account-token"
1497 ))),
1498 request::body(json_decoded(eq(json!({
1499 "scope": [DEFAULT_SCOPE],
1500 "lifetime": "3600s",
1501 "delegates": ["delegate1", "delegate2"]
1502 }))))
1503 ])
1504 .respond_with(json_encoded(json!({
1505 "accessToken": "test-impersonated-token",
1506 "expireTime": expire_time
1507 }))),
1508 );
1509
1510 let impersonated_credential = json!({
1511 "type": "impersonated_service_account",
1512 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1513 "source_credentials": {
1514 "type": "authorized_user",
1515 "client_id": "test-client-id",
1516 "client_secret": "test-client-secret",
1517 "refresh_token": "test-refresh-token",
1518 "token_uri": server.url("/token").to_string()
1519 }
1520 });
1521 let (token_provider, _, _) = Builder::new(impersonated_credential)
1522 .with_delegates(vec!["delegate1", "delegate2"])
1523 .build_components()?;
1524
1525 let token = token_provider.token().await?;
1526 assert_eq!(token.token, "test-impersonated-token");
1527 assert_eq!(token.token_type, "Bearer");
1528
1529 Ok(())
1530 }
1531
1532 #[tokio::test]
1533 #[parallel]
1534 async fn test_impersonated_service_account_fail() -> TestResult {
1535 let server = Server::run();
1536 server.expect(
1537 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1538 json_encoded(json!({
1539 "access_token": "test-user-account-token",
1540 "expires_in": 3600,
1541 "token_type": "Bearer",
1542 })),
1543 ),
1544 );
1545 server.expect(
1546 Expectation::matching(request::method_path(
1547 "POST",
1548 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1549 ))
1550 .respond_with(status_code(500)),
1551 );
1552
1553 let impersonated_credential = json!({
1554 "type": "impersonated_service_account",
1555 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1556 "source_credentials": {
1557 "type": "authorized_user",
1558 "client_id": "test-client-id",
1559 "client_secret": "test-client-secret",
1560 "refresh_token": "test-refresh-token",
1561 "token_uri": server.url("/token").to_string()
1562 }
1563 });
1564 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1565
1566 let err = token_provider.token().await.unwrap_err();
1567 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1568 assert!(original_err.is_transient());
1569
1570 Ok(())
1571 }
1572
1573 #[tokio::test]
1574 #[parallel]
1575 async fn debug_token_provider() {
1576 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1577 "type": "authorized_user",
1578 "client_id": "test-client-id",
1579 "client_secret": "test-client-secret",
1580 "refresh_token": "test-refresh-token"
1581 }))
1582 .build()
1583 .unwrap();
1584
1585 let expected = ImpersonatedTokenProvider {
1586 source_credentials,
1587 service_account_impersonation_url: ImpersonationUrl::exact(
1588 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1589 ),
1590 delegates: Some(vec!["delegate1".to_string()]),
1591 scopes: vec!["scope1".to_string()],
1592 lifetime: Duration::from_secs(3600),
1593 };
1594 let fmt = format!("{expected:?}");
1595 assert!(fmt.contains("UserCredentials"), "{fmt}");
1596 assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1597 assert!(fmt.contains("delegate1"), "{fmt}");
1598 assert!(fmt.contains("scope1"), "{fmt}");
1599 assert!(fmt.contains("3600s"), "{fmt}");
1600 }
1601
1602 #[test]
1603 fn impersonated_config_full_from_json_success() {
1604 let source_credentials_json = json!({
1605 "type": "authorized_user",
1606 "client_id": "test-client-id",
1607 "client_secret": "test-client-secret",
1608 "refresh_token": "test-refresh-token"
1609 });
1610 let json = json!({
1611 "type": "impersonated_service_account",
1612 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1613 "source_credentials": source_credentials_json,
1614 "delegates": ["delegate1"],
1615 "quota_project_id": "test-project-id",
1616 "scopes": ["scope1"],
1617 });
1618
1619 let expected = ImpersonatedConfig {
1620 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1621 source_credentials: source_credentials_json,
1622 delegates: Some(vec!["delegate1".to_string()]),
1623 quota_project_id: Some("test-project-id".to_string()),
1624 scopes: Some(vec!["scope1".to_string()]),
1625 universe_domain: None,
1626 };
1627 let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1628 assert_eq!(actual, expected);
1629 }
1630
1631 #[test]
1632 fn impersonated_config_partial_from_json_success() {
1633 let source_credentials_json = json!({
1634 "type": "authorized_user",
1635 "client_id": "test-client-id",
1636 "client_secret": "test-client-secret",
1637 "refresh_token": "test-refresh-token"
1638 });
1639 let json = json!({
1640 "type": "impersonated_service_account",
1641 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1642 "source_credentials": source_credentials_json
1643 });
1644
1645 let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1646 assert_eq!(
1647 config.service_account_impersonation_url,
1648 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1649 );
1650 assert_eq!(config.source_credentials, source_credentials_json);
1651 assert_eq!(config.delegates, None);
1652 assert_eq!(config.quota_project_id, None);
1653 assert_eq!(config.scopes, None);
1654 }
1655
1656 #[tokio::test]
1657 #[parallel]
1658 async fn test_impersonated_service_account_source_fail() -> TestResult {
1659 let mut mock = MockCredentials::new();
1660 mock.expect_headers()
1661 .returning(|_| Err(errors::non_retryable_from_str("source failed")));
1662 let source_credentials = Credentials::from(mock);
1663
1664 let token_provider = ImpersonatedTokenProvider {
1665 source_credentials,
1666 service_account_impersonation_url: ImpersonationUrl::exact(
1667 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1668 ),
1669 delegates: Some(vec!["delegate1".to_string()]),
1670 scopes: vec!["scope1".to_string()],
1671 lifetime: DEFAULT_LIFETIME,
1672 };
1673
1674 let err = token_provider.token().await.unwrap_err();
1675 assert!(err.to_string().contains("source failed"));
1676
1677 Ok(())
1678 }
1679
1680 #[tokio::test]
1681 #[parallel]
1682 async fn test_missing_impersonation_url_fail() {
1683 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1684 "type": "authorized_user",
1685 "client_id": "test-client-id",
1686 "client_secret": "test-client-secret",
1687 "refresh_token": "test-refresh-token"
1688 }))
1689 .build()
1690 .unwrap();
1691
1692 let result = Builder::from_source_credentials(source_credentials).build();
1693 assert!(result.is_err(), "{result:?}");
1694 let err = result.unwrap_err();
1695 assert!(err.is_parsing());
1696 assert!(
1697 err.to_string()
1698 .contains("`service_account_impersonation_url` is required")
1699 );
1700 }
1701
1702 #[tokio::test]
1703 #[parallel]
1704 async fn test_nested_impersonated_credentials_fail() {
1705 let nested_impersonated = json!({
1706 "type": "impersonated_service_account",
1707 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1708 "source_credentials": {
1709 "type": "impersonated_service_account",
1710 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1711 "source_credentials": {
1712 "type": "authorized_user",
1713 "client_id": "test-client-id",
1714 "client_secret": "test-client-secret",
1715 "refresh_token": "test-refresh-token"
1716 }
1717 }
1718 });
1719
1720 let result = Builder::new(nested_impersonated).build();
1721 assert!(result.is_err(), "{result:?}");
1722 let err = result.unwrap_err();
1723 assert!(err.is_parsing());
1724 assert!(
1725 err.to_string().contains(
1726 "source credential of type `impersonated_service_account` is not supported"
1727 )
1728 );
1729 }
1730
1731 #[tokio::test]
1732 #[parallel]
1733 async fn test_malformed_impersonated_credentials_fail() {
1734 let malformed_impersonated = json!({
1735 "type": "impersonated_service_account",
1736 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1737 });
1738
1739 let result = Builder::new(malformed_impersonated).build();
1740 assert!(result.is_err(), "{result:?}");
1741 let err = result.unwrap_err();
1742 assert!(err.is_parsing());
1743 assert!(
1744 err.to_string()
1745 .contains("missing field `source_credentials`")
1746 );
1747 }
1748
1749 #[tokio::test]
1750 #[parallel]
1751 async fn test_invalid_source_credential_type_fail() {
1752 let invalid_source = json!({
1753 "type": "impersonated_service_account",
1754 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1755 "source_credentials": {
1756 "type": "invalid_type",
1757 }
1758 });
1759
1760 let result = Builder::new(invalid_source).build();
1761 assert!(result.is_err(), "{result:?}");
1762 let err = result.unwrap_err();
1763 assert!(err.is_unknown_type());
1764 }
1765
1766 #[tokio::test]
1767 #[parallel]
1768 async fn test_missing_expiry() -> TestResult {
1769 let server = Server::run();
1770 server.expect(
1771 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1772 json_encoded(json!({
1773 "access_token": "test-user-account-token",
1774 "expires_in": 3600,
1775 "token_type": "Bearer",
1776 })),
1777 ),
1778 );
1779 server.expect(
1780 Expectation::matching(request::method_path(
1781 "POST",
1782 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1783 ))
1784 .respond_with(json_encoded(json!({
1785 "accessToken": "test-impersonated-token",
1786 }))),
1787 );
1788
1789 let impersonated_credential = json!({
1790 "type": "impersonated_service_account",
1791 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1792 "source_credentials": {
1793 "type": "authorized_user",
1794 "client_id": "test-client-id",
1795 "client_secret": "test-client-secret",
1796 "refresh_token": "test-refresh-token",
1797 "token_uri": server.url("/token").to_string()
1798 }
1799 });
1800 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1801
1802 let err = token_provider.token().await.unwrap_err();
1803 assert!(!err.is_transient());
1804
1805 Ok(())
1806 }
1807
1808 #[tokio::test]
1809 #[parallel]
1810 async fn test_invalid_expiry_format() -> TestResult {
1811 let server = Server::run();
1812 server.expect(
1813 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1814 json_encoded(json!({
1815 "access_token": "test-user-account-token",
1816 "expires_in": 3600,
1817 "token_type": "Bearer",
1818 })),
1819 ),
1820 );
1821 server.expect(
1822 Expectation::matching(request::method_path(
1823 "POST",
1824 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1825 ))
1826 .respond_with(json_encoded(json!({
1827 "accessToken": "test-impersonated-token",
1828 "expireTime": "invalid-format"
1829 }))),
1830 );
1831
1832 let impersonated_credential = json!({
1833 "type": "impersonated_service_account",
1834 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1835 "source_credentials": {
1836 "type": "authorized_user",
1837 "client_id": "test-client-id",
1838 "client_secret": "test-client-secret",
1839 "refresh_token": "test-refresh-token",
1840 "token_uri": server.url("/token").to_string()
1841 }
1842 });
1843 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1844
1845 let err = token_provider.token().await.unwrap_err();
1846 assert!(!err.is_transient());
1847
1848 Ok(())
1849 }
1850
1851 #[tokio::test]
1852 #[parallel]
1853 async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1854 let server = Server::run();
1855 server.expect(
1856 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1857 json_encoded(json!({
1858 "access_token": "test-user-account-token",
1859 "expires_in": 3600,
1860 "token_type": "Bearer",
1861 })),
1862 ),
1863 );
1864 server.expect(
1865 Expectation::matching(request::method_path(
1866 "POST",
1867 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1868 ))
1869 .respond_with(json_encoded(json!("bad json"))),
1870 );
1871
1872 let impersonated_credential = json!({
1873 "type": "impersonated_service_account",
1874 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1875 "source_credentials": {
1876 "type": "authorized_user",
1877 "client_id": "test-client-id",
1878 "client_secret": "test-client-secret",
1879 "refresh_token": "test-refresh-token",
1880 "token_uri": server.url("/token").to_string()
1881 }
1882 });
1883 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1884
1885 let e = token_provider.token().await.err().unwrap();
1886 assert!(!e.is_transient(), "{e}");
1887
1888 Ok(())
1889 }
1890
1891 #[tokio::test]
1892 #[parallel]
1893 async fn token_provider_nonretryable_error() -> TestResult {
1894 let server = Server::run();
1895 server.expect(
1896 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1897 json_encoded(json!({
1898 "access_token": "test-user-account-token",
1899 "expires_in": 3600,
1900 "token_type": "Bearer",
1901 })),
1902 ),
1903 );
1904 server.expect(
1905 Expectation::matching(request::method_path(
1906 "POST",
1907 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1908 ))
1909 .respond_with(status_code(401)),
1910 );
1911
1912 let impersonated_credential = json!({
1913 "type": "impersonated_service_account",
1914 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1915 "source_credentials": {
1916 "type": "authorized_user",
1917 "client_id": "test-client-id",
1918 "client_secret": "test-client-secret",
1919 "refresh_token": "test-refresh-token",
1920 "token_uri": server.url("/token").to_string()
1921 }
1922 });
1923 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1924
1925 let err = token_provider.token().await.unwrap_err();
1926 assert!(!err.is_transient());
1927
1928 Ok(())
1929 }
1930
1931 #[tokio::test]
1932 #[parallel]
1933 async fn credential_full_with_quota_project_from_builder() -> TestResult {
1934 let server = Server::run();
1935 server.expect(
1936 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1937 json_encoded(json!({
1938 "access_token": "test-user-account-token",
1939 "expires_in": 3600,
1940 "token_type": "Bearer",
1941 })),
1942 ),
1943 );
1944 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1945 .format(&time::format_description::well_known::Rfc3339)
1946 .unwrap();
1947 server.expect(
1948 Expectation::matching(request::method_path(
1949 "POST",
1950 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1951 ))
1952 .respond_with(json_encoded(json!({
1953 "accessToken": "test-impersonated-token",
1954 "expireTime": expire_time
1955 }))),
1956 );
1957
1958 let impersonated_credential = json!({
1959 "type": "impersonated_service_account",
1960 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1961 "source_credentials": {
1962 "type": "authorized_user",
1963 "client_id": "test-client-id",
1964 "client_secret": "test-client-secret",
1965 "refresh_token": "test-refresh-token",
1966 "token_uri": server.url("/token").to_string()
1967 }
1968 });
1969 let creds = Builder::new(impersonated_credential)
1970 .with_quota_project_id("test-project")
1971 .build()?;
1972
1973 let headers = creds.headers(Extensions::new()).await?;
1974 match headers {
1975 CacheableResource::New { data, .. } => {
1976 assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1977 }
1978 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1979 }
1980
1981 Ok(())
1982 }
1983
1984 #[tokio::test]
1985 #[parallel]
1986 async fn access_token_credentials_success() -> TestResult {
1987 let server = Server::run();
1988 server.expect(
1989 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1990 json_encoded(json!({
1991 "access_token": "test-user-account-token",
1992 "expires_in": 3600,
1993 "token_type": "Bearer",
1994 })),
1995 ),
1996 );
1997 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1998 .format(&time::format_description::well_known::Rfc3339)
1999 .unwrap();
2000 server.expect(
2001 Expectation::matching(request::method_path(
2002 "POST",
2003 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2004 ))
2005 .respond_with(json_encoded(json!({
2006 "accessToken": "test-impersonated-token",
2007 "expireTime": expire_time
2008 }))),
2009 );
2010
2011 let impersonated_credential = json!({
2012 "type": "impersonated_service_account",
2013 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2014 "source_credentials": {
2015 "type": "authorized_user",
2016 "client_id": "test-client-id",
2017 "client_secret": "test-client-secret",
2018 "refresh_token": "test-refresh-token",
2019 "token_uri": server.url("/token").to_string()
2020 }
2021 });
2022 let creds = Builder::new(impersonated_credential).build_access_token_credentials()?;
2023
2024 let access_token = creds.access_token().await?;
2025 assert_eq!(access_token.token, "test-impersonated-token");
2026
2027 Ok(())
2028 }
2029
2030 #[tokio::test]
2031 #[parallel]
2032 async fn test_with_target_principal() {
2033 let source_credentials = crate::credentials::user_account::Builder::new(json!({
2034 "type": "authorized_user",
2035 "client_id": "test-client-id",
2036 "client_secret": "test-client-secret",
2037 "refresh_token": "test-refresh-token"
2038 }))
2039 .build()
2040 .unwrap();
2041
2042 let (token_provider, _, _) = Builder::from_source_credentials(source_credentials.clone())
2043 .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2044 .build_components()
2045 .unwrap();
2046
2047 let url = token_provider
2048 .inner
2049 .service_account_impersonation_url
2050 .access_token_url(&source_credentials)
2051 .await;
2052
2053 assert_eq!(
2054 url,
2055 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
2056 );
2057 }
2058
2059 fn source_creds_with_universe_domain(source_universe_domain: &str) -> Value {
2060 serde_json::json!({
2061 "type": "service_account",
2062 "client_email": "test-client-email",
2063 "private_key_id": "test-private-key-id",
2064 "private_key": Value::from(PKCS8_PK.clone()),
2065 "project_id": "test-project-id",
2066 "universe_domain": source_universe_domain,
2067 })
2068 }
2069
2070 fn service_account_builder_with_universe(source_universe_domain: &str) -> Builder {
2071 let source_creds = source_creds_with_universe_domain(source_universe_domain);
2072 let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2073 .build()
2074 .expect("Failed to build service account credentials");
2075 Builder::from_source_credentials(source_credentials)
2076 .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2077 }
2078
2079 fn mds_builder() -> Builder {
2080 let source_credentials = crate::credentials::mds::Builder::default()
2081 .build()
2082 .expect("Failed to build MDS credentials");
2083 Builder::from_source_credentials(source_credentials)
2084 .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2085 }
2086
2087 fn json_builder_with_universe(source_universe_domain: &str) -> Builder {
2088 let source_creds = source_creds_with_universe_domain(source_universe_domain);
2089 let impersonated_credential = serde_json::json!({
2090 "type": "impersonated_service_account",
2091 "service_account_impersonation_url": "https://iamcredentials.my-custom-universe.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2092 "source_credentials": source_creds,
2093 });
2094 Builder::new(impersonated_credential)
2095 }
2096
2097 #[test_case(service_account_builder_with_universe("my-custom-universe.com"); "service account as source")]
2098 #[test_case(json_builder_with_universe("my-custom-universe.com"); "credentials from json")]
2099 #[parallel]
2100 #[tokio::test]
2101 async fn universe_domain_from_source(builder: Builder) -> TestResult {
2102 let creds = builder.build()?;
2103 let universe_domain = creds.universe_domain().await;
2104
2105 assert_eq!(universe_domain.as_deref(), Some("my-custom-universe.com"));
2106
2107 Ok(())
2108 }
2109
2110 #[tokio::test]
2111 #[parallel]
2112 async fn universe_domain_mds_source() -> TestResult {
2113 let builder = mds_builder();
2114 let creds = builder.build()?;
2115 let universe_domain = creds.universe_domain().await;
2116
2117 assert!(is_default_universe_domain(universe_domain.as_deref()));
2118
2119 Ok(())
2120 }
2121
2122 #[test_case(service_account_builder_with_universe("my-custom-universe.com"); "service account as source")]
2123 #[test_case(json_builder_with_universe("my-custom-universe.com"); "credentials from json")]
2124 #[test_case(mds_builder(); "mds as source")]
2125 #[tokio::test]
2126 #[parallel]
2127 async fn universe_domain_override(builder: Builder) -> TestResult {
2128 let creds = builder
2129 .with_universe_domain("another-universe.com")
2130 .build()?;
2131
2132 let universe_domain = creds.universe_domain().await;
2133
2134 assert_eq!(universe_domain.as_deref(), Some("another-universe.com"));
2135
2136 Ok(())
2137 }
2138
2139 #[tokio::test]
2140 #[parallel]
2141 async fn credential_full_with_quota_project_from_json() -> TestResult {
2142 let server = Server::run();
2143 server.expect(
2144 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2145 json_encoded(json!({
2146 "access_token": "test-user-account-token",
2147 "expires_in": 3600,
2148 "token_type": "Bearer",
2149 })),
2150 ),
2151 );
2152 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2153 .format(&time::format_description::well_known::Rfc3339)
2154 .unwrap();
2155 server.expect(
2156 Expectation::matching(request::method_path(
2157 "POST",
2158 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2159 ))
2160 .respond_with(json_encoded(json!({
2161 "accessToken": "test-impersonated-token",
2162 "expireTime": expire_time
2163 }))),
2164 );
2165
2166 let impersonated_credential = json!({
2167 "type": "impersonated_service_account",
2168 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2169 "source_credentials": {
2170 "type": "authorized_user",
2171 "client_id": "test-client-id",
2172 "client_secret": "test-client-secret",
2173 "refresh_token": "test-refresh-token",
2174 "token_uri": server.url("/token").to_string()
2175 },
2176 "quota_project_id": "test-project-from-json",
2177 });
2178
2179 let creds = Builder::new(impersonated_credential).build()?;
2180
2181 let headers = creds.headers(Extensions::new()).await?;
2182 println!("headers: {:#?}", headers);
2183 match headers {
2184 CacheableResource::New { data, .. } => {
2185 assert_eq!(
2186 data.get("x-goog-user-project").unwrap(),
2187 "test-project-from-json"
2188 );
2189 }
2190 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
2191 }
2192
2193 Ok(())
2194 }
2195
2196 #[tokio::test]
2197 #[parallel]
2198 async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
2199 let server = Server::run();
2200
2201 server.expect(
2204 Expectation::matching(all_of![
2205 request::method_path("POST", "/source_token"),
2206 request::body(json_decoded(
2207 |body: &serde_json::Value| body["scopes"].is_null()
2208 ))
2209 ])
2210 .respond_with(json_encoded(json!({
2211 "access_token": "source-token",
2212 "expires_in": 3600,
2213 "token_type": "Bearer",
2214 }))),
2215 );
2216
2217 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2218 .format(&time::format_description::well_known::Rfc3339)
2219 .unwrap();
2220
2221 server.expect(
2224 Expectation::matching(all_of![
2225 request::method_path(
2226 "POST",
2227 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2228 ),
2229 request::headers(contains(("authorization", "Bearer source-token"))),
2230 request::body(json_decoded(eq(json!({
2231 "scope": ["impersonated-scope"],
2232 "lifetime": "3600s"
2233 }))))
2234 ])
2235 .respond_with(json_encoded(json!({
2236 "accessToken": "impersonated-token",
2237 "expireTime": expire_time
2238 }))),
2239 );
2240
2241 let impersonated_credential = json!({
2242 "type": "impersonated_service_account",
2243 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2244 "source_credentials": {
2245 "type": "authorized_user",
2246 "client_id": "test-client-id",
2247 "client_secret": "test-client-secret",
2248 "refresh_token": "test-refresh-token",
2249 "token_uri": server.url("/source_token").to_string()
2250 }
2251 });
2252
2253 let creds = Builder::new(impersonated_credential)
2254 .with_scopes(vec!["impersonated-scope"])
2255 .with_quota_project_id("impersonated-quota-project")
2256 .build()?;
2257
2258 let fmt = format!("{creds:?}");
2260 assert!(fmt.contains("impersonated-quota-project"));
2261
2262 let _token = creds.headers(Extensions::new()).await?;
2264
2265 Ok(())
2266 }
2267
2268 #[tokio::test]
2269 #[parallel]
2270 async fn test_impersonated_metrics_header() -> TestResult {
2271 let server = Server::run();
2272 server.expect(
2273 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2274 json_encoded(json!({
2275 "access_token": "test-user-account-token",
2276 "expires_in": 3600,
2277 "token_type": "Bearer",
2278 })),
2279 ),
2280 );
2281 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2282 .format(&time::format_description::well_known::Rfc3339)
2283 .unwrap();
2284 server.expect(
2285 Expectation::matching(all_of![
2286 request::method_path(
2287 "POST",
2288 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2289 ),
2290 request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
2291 request::headers(contains((
2292 "x-goog-api-client",
2293 matches("auth-request-type/at")
2294 )))
2295 ])
2296 .respond_with(json_encoded(json!({
2297 "accessToken": "test-impersonated-token",
2298 "expireTime": expire_time
2299 }))),
2300 );
2301
2302 let impersonated_credential = json!({
2303 "type": "impersonated_service_account",
2304 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2305 "source_credentials": {
2306 "type": "authorized_user",
2307 "client_id": "test-client-id",
2308 "client_secret": "test-client-secret",
2309 "refresh_token": "test-refresh-token",
2310 "token_uri": server.url("/token").to_string()
2311 }
2312 });
2313 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
2314
2315 let token = token_provider.token().await?;
2316 assert_eq!(token.token, "test-impersonated-token");
2317 assert_eq!(token.token_type, "Bearer");
2318
2319 Ok(())
2320 }
2321
2322 #[tokio::test]
2323 #[parallel]
2324 async fn test_impersonated_retries_for_success() -> TestResult {
2325 let mut server = Server::run();
2326 server.expect(
2328 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2329 json_encoded(json!({
2330 "access_token": "test-user-account-token",
2331 "expires_in": 3600,
2332 "token_type": "Bearer",
2333 })),
2334 ),
2335 );
2336
2337 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2338 .format(&time::format_description::well_known::Rfc3339)
2339 .unwrap();
2340
2341 let impersonation_path =
2343 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2344 server.expect(
2345 Expectation::matching(request::method_path("POST", impersonation_path))
2346 .times(3)
2347 .respond_with(cycle![
2348 status_code(503).body("try-again"),
2349 status_code(503).body("try-again"),
2350 status_code(200)
2351 .append_header("Content-Type", "application/json")
2352 .body(
2353 json!({
2354 "accessToken": "test-impersonated-token",
2355 "expireTime": expire_time
2356 })
2357 .to_string()
2358 ),
2359 ]),
2360 );
2361
2362 let impersonated_credential = json!({
2363 "type": "impersonated_service_account",
2364 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2365 "source_credentials": {
2366 "type": "authorized_user",
2367 "client_id": "test-client-id",
2368 "client_secret": "test-client-secret",
2369 "refresh_token": "test-refresh-token",
2370 "token_uri": server.url("/token").to_string()
2371 }
2372 });
2373
2374 let (token_provider, _, _) = Builder::new(impersonated_credential)
2375 .with_retry_policy(get_mock_auth_retry_policy(3))
2376 .with_backoff_policy(get_mock_backoff_policy())
2377 .with_retry_throttler(get_mock_retry_throttler())
2378 .build_components()?;
2379
2380 let token = token_provider.token().await?;
2381 assert_eq!(token.token, "test-impersonated-token");
2382
2383 server.verify_and_clear();
2384 Ok(())
2385 }
2386
2387 #[tokio::test]
2388 #[parallel]
2389 async fn test_scopes_from_json() -> TestResult {
2390 let server = Server::run();
2391 server.expect(
2392 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2393 json_encoded(json!({
2394 "access_token": "test-user-account-token",
2395 "expires_in": 3600,
2396 "token_type": "Bearer",
2397 })),
2398 ),
2399 );
2400 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2401 .format(&time::format_description::well_known::Rfc3339)
2402 .unwrap();
2403 server.expect(
2404 Expectation::matching(all_of![
2405 request::method_path(
2406 "POST",
2407 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2408 ),
2409 request::body(json_decoded(eq(json!({
2410 "scope": ["scope-from-json"],
2411 "lifetime": "3600s"
2412 }))))
2413 ])
2414 .respond_with(json_encoded(json!({
2415 "accessToken": "test-impersonated-token",
2416 "expireTime": expire_time
2417 }))),
2418 );
2419
2420 let impersonated_credential = json!({
2421 "type": "impersonated_service_account",
2422 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2423 "scopes": ["scope-from-json"],
2424 "source_credentials": {
2425 "type": "authorized_user",
2426 "client_id": "test-client-id",
2427 "client_secret": "test-client-secret",
2428 "refresh_token": "test-refresh-token",
2429 "token_uri": server.url("/token").to_string()
2430 }
2431 });
2432 let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
2433
2434 let token = token_provider.token().await?;
2435 assert_eq!(token.token, "test-impersonated-token");
2436
2437 Ok(())
2438 }
2439
2440 #[tokio::test]
2441 #[parallel]
2442 async fn test_with_scopes_overrides_json_scopes() -> TestResult {
2443 let server = Server::run();
2444 server.expect(
2445 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2446 json_encoded(json!({
2447 "access_token": "test-user-account-token",
2448 "expires_in": 3600,
2449 "token_type": "Bearer",
2450 })),
2451 ),
2452 );
2453 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2454 .format(&time::format_description::well_known::Rfc3339)
2455 .unwrap();
2456 server.expect(
2457 Expectation::matching(all_of![
2458 request::method_path(
2459 "POST",
2460 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2461 ),
2462 request::body(json_decoded(eq(json!({
2463 "scope": ["scope-from-with-scopes"],
2464 "lifetime": "3600s"
2465 }))))
2466 ])
2467 .respond_with(json_encoded(json!({
2468 "accessToken": "test-impersonated-token",
2469 "expireTime": expire_time
2470 }))),
2471 );
2472
2473 let impersonated_credential = json!({
2474 "type": "impersonated_service_account",
2475 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2476 "scopes": ["scope-from-json"],
2477 "source_credentials": {
2478 "type": "authorized_user",
2479 "client_id": "test-client-id",
2480 "client_secret": "test-client-secret",
2481 "refresh_token": "test-refresh-token",
2482 "token_uri": server.url("/token").to_string()
2483 }
2484 });
2485 let (token_provider, _, _) = Builder::new(impersonated_credential)
2486 .with_scopes(vec!["scope-from-with-scopes"])
2487 .build_components()?;
2488
2489 let token = token_provider.token().await?;
2490 assert_eq!(token.token, "test-impersonated-token");
2491
2492 Ok(())
2493 }
2494
2495 #[tokio::test]
2496 #[parallel]
2497 async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
2498 let mut server = Server::run();
2499 server.expect(
2501 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2502 json_encoded(json!({
2503 "access_token": "test-user-account-token",
2504 "expires_in": 3600,
2505 "token_type": "Bearer",
2506 })),
2507 ),
2508 );
2509
2510 let impersonation_path =
2512 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2513 server.expect(
2514 Expectation::matching(request::method_path("POST", impersonation_path))
2515 .times(1)
2516 .respond_with(status_code(401)),
2517 );
2518
2519 let impersonated_credential = json!({
2520 "type": "impersonated_service_account",
2521 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2522 "source_credentials": {
2523 "type": "authorized_user",
2524 "client_id": "test-client-id",
2525 "client_secret": "test-client-secret",
2526 "refresh_token": "test-refresh-token",
2527 "token_uri": server.url("/token").to_string()
2528 }
2529 });
2530
2531 let (token_provider, _, _) = Builder::new(impersonated_credential)
2532 .with_retry_policy(get_mock_auth_retry_policy(3))
2533 .with_backoff_policy(get_mock_backoff_policy())
2534 .with_retry_throttler(get_mock_retry_throttler())
2535 .build_components()?;
2536
2537 let err = token_provider.token().await.unwrap_err();
2538 assert!(!err.is_transient());
2539
2540 server.verify_and_clear();
2541 Ok(())
2542 }
2543
2544 #[tokio::test]
2545 #[parallel]
2546 async fn test_impersonated_remote_signer() -> TestResult {
2547 let server = Server::run();
2548 server.expect(
2549 Expectation::matching(request::method_path("POST", "/token"))
2550 .times(2..)
2551 .respond_with(json_encoded(json!({
2552 "access_token": "test-user-account-token",
2553 "expires_in": 3600,
2554 "token_type": "Bearer",
2555 }))),
2556 );
2557 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2558 .format(&time::format_description::well_known::Rfc3339)
2559 .unwrap();
2560 server.expect(
2561 Expectation::matching(request::method_path(
2562 "POST",
2563 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2564 ))
2565 .times(2)
2566 .respond_with(json_encoded(json!({
2567 "accessToken": "test-impersonated-token",
2568 "expireTime": expire_time
2569 }))),
2570 );
2571
2572 server.expect(
2573 Expectation::matching(all_of![
2574 request::method_path(
2575 "POST",
2576 "/v1/projects/-/serviceAccounts/test-principal:signBlob"
2577 ),
2578 request::headers(contains((
2579 "authorization",
2580 "Bearer test-impersonated-token"
2581 ))),
2582 ])
2583 .times(2)
2584 .respond_with(json_encoded(json!({
2585 "signedBlob": BASE64_STANDARD.encode("signed_blob"),
2586 }))),
2587 );
2588
2589 let endpoint = server.url("/").to_string();
2590 let endpoint = endpoint.trim_end_matches('/');
2591
2592 let user_credential = json!({
2594 "type": "authorized_user",
2595 "client_id": "test-client-id",
2596 "client_secret": "test-client-secret",
2597 "refresh_token": "test-refresh-token",
2598 "token_uri": server.url("/token").to_string()
2599 });
2600 let source_credential =
2601 crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2602
2603 let builder_from_source = Builder::from_source_credentials(source_credential)
2604 .with_target_principal("test-principal")
2605 .with_impersonation_endpoint(endpoint);
2606
2607 let impersonation_url = server
2608 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2609 .to_string();
2610 let impersonated_credential = json!({
2612 "type": "impersonated_service_account",
2613 "service_account_impersonation_url": impersonation_url,
2614 "source_credentials": user_credential,
2615 });
2616 let builder_from_json = Builder::new(impersonated_credential);
2617
2618 for builder in [builder_from_source, builder_from_json] {
2619 let iam_endpoint = server
2620 .url("/")
2621 .to_string()
2622 .trim_end_matches('/')
2623 .to_string();
2624 let signer = builder
2625 .maybe_iam_endpoint_override(Some(iam_endpoint))
2626 .without_access_boundary()
2627 .build_signer()?;
2628
2629 let client_email = signer.client_email().await?;
2630 assert_eq!(client_email, "test-principal");
2631
2632 let result = signer.sign(b"test").await?;
2633 assert_eq!(result.as_ref(), b"signed_blob");
2634 }
2635
2636 Ok(())
2637 }
2638
2639 #[tokio::test]
2640 #[parallel]
2641 async fn test_impersonated_sa_signer() -> TestResult {
2642 let service_account = json!({
2643 "type": "service_account",
2644 "client_email": "test-client-email",
2645 "private_key_id": "test-private-key-id",
2646 "private_key": Value::from(PKCS8_PK.clone()),
2647 "project_id": "test-project-id",
2648 });
2649 let impersonated_credential = json!({
2650 "type": "impersonated_service_account",
2651 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2652 "source_credentials": service_account.clone(),
2653 });
2654
2655 let signer = Builder::new(impersonated_credential).build_signer()?;
2656
2657 let client_email = signer.client_email().await?;
2658 assert_eq!(client_email, "test-principal");
2659
2660 let result = signer.sign(b"test").await?;
2661
2662 let service_account_key = serde_json::from_value::<ServiceAccountKey>(service_account)?;
2663 let inner_signer = service_account_key.signer().unwrap();
2664 let inner_result = inner_signer.sign(b"test")?;
2665 assert_eq!(result.as_ref(), inner_result);
2666
2667 Ok(())
2668 }
2669
2670 #[tokio::test]
2671 #[parallel]
2672 async fn test_impersonated_signer_with_invalid_email() -> TestResult {
2673 let impersonated_credential = json!({
2674 "type": "impersonated_service_account",
2675 "service_account_impersonation_url": "http://example.com/test-principal:generateIdToken",
2676 "source_credentials": json!({
2677 "type": "service_account",
2678 "client_email": "test-client-email",
2679 "private_key_id": "test-private-key-id",
2680 "private_key": "test-private-key",
2681 "project_id": "test-project-id",
2682 }),
2683 });
2684
2685 let error = Builder::new(impersonated_credential)
2686 .build_signer()
2687 .unwrap_err();
2688
2689 assert!(error.is_parsing());
2690 assert!(
2691 error
2692 .to_string()
2693 .contains("invalid service account impersonation URL"),
2694 "error: {}",
2695 error
2696 );
2697
2698 Ok(())
2699 }
2700
2701 #[tokio::test]
2702 #[parallel]
2703 #[cfg(google_cloud_unstable_trust_boundaries)]
2704 async fn e2e_access_boundary() -> TestResult {
2705 use crate::credentials::tests::{get_access_boundary_from_headers, get_token_from_headers};
2706 let server = Server::run();
2707 server.expect(
2708 Expectation::matching(request::method_path("POST", "/token"))
2709 .times(2..)
2710 .respond_with(json_encoded(json!({
2711 "access_token": "test-user-account-token",
2712 "expires_in": 3600,
2713 "token_type": "Bearer",
2714 }))),
2715 );
2716 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2717 .format(&time::format_description::well_known::Rfc3339)
2718 .unwrap();
2719 server.expect(
2720 Expectation::matching(request::method_path(
2721 "POST",
2722 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2723 ))
2724 .times(2)
2725 .respond_with(json_encoded(json!({
2726 "accessToken": "test-impersonated-token",
2727 "expireTime": expire_time
2728 }))),
2729 );
2730
2731 server.expect(
2732 Expectation::matching(all_of![
2733 request::method_path(
2734 "GET",
2735 "/v1/projects/-/serviceAccounts/test-principal/allowedLocations"
2736 ),
2737 request::headers(contains((
2738 "authorization",
2739 "Bearer test-impersonated-token"
2740 ))),
2741 ])
2742 .times(2)
2743 .respond_with(json_encoded(json!({
2744 "locations": ["us-central1", "us-east1"],
2745 "encodedLocations": "0x1234"
2746 }))),
2747 );
2748 let impersonation_url = server
2749 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2750 .to_string();
2751 let endpoint = server.url("/").to_string();
2752 let endpoint = endpoint.trim_end_matches('/');
2753
2754 let user_credential = json!({
2756 "type": "authorized_user",
2757 "client_id": "test-client-id",
2758 "client_secret": "test-client-secret",
2759 "refresh_token": "test-refresh-token",
2760 "token_uri": server.url("/token").to_string()
2761 });
2762 let source_credential =
2763 crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2764 let builder_from_source = Builder::from_source_credentials(source_credential)
2765 .with_target_principal("test-principal")
2766 .with_impersonation_endpoint(endpoint);
2767
2768 let impersonated_credential = json!({
2770 "type": "impersonated_service_account",
2771 "service_account_impersonation_url": impersonation_url,
2772 "source_credentials": user_credential,
2773 });
2774 let builder_from_json = Builder::new(impersonated_credential);
2775
2776 for builder in [builder_from_source, builder_from_json] {
2777 let iam_endpoint = server
2778 .url("/")
2779 .to_string()
2780 .trim_end_matches('/')
2781 .to_string();
2782 let creds = builder
2783 .maybe_iam_endpoint_override(Some(iam_endpoint))
2784 .build_credentials()?;
2785
2786 creds.wait_for_boundary().await;
2788
2789 let headers = creds.headers(Extensions::new()).await?;
2790 let token = get_token_from_headers(headers.clone());
2791 let access_boundary = get_access_boundary_from_headers(headers);
2792 assert!(token.is_some(), "should have some token: {token:?}");
2793 assert_eq!(
2794 access_boundary.as_deref(),
2795 Some("0x1234"),
2796 "should be 0x1234 but found: {access_boundary:?}"
2797 );
2798 }
2799
2800 Ok(())
2801 }
2802
2803 #[tokio::test]
2804 #[parallel]
2805 async fn test_impersonated_access_token_custom_universe_domain() -> TestResult {
2806 let server = Server::run();
2807 let universe_domain = "my-custom-universe.com".to_string();
2808 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2809 .format(&time::format_description::well_known::Rfc3339)
2810 .unwrap();
2811
2812 server.expect(
2813 Expectation::matching(all_of![
2814 request::method_path(
2815 "POST",
2816 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2817 ),
2818 request::headers(contains((
2819 "authorization",
2820 "Bearer test-user-account-token"
2821 ))),
2822 ])
2823 .respond_with(json_encoded(json!({
2824 "accessToken": "test-impersonated-token",
2825 "expireTime": expire_time
2826 }))),
2827 );
2828
2829 let universe_domain_clone = universe_domain.clone();
2830 let mut mock = MockCredentials::new();
2831 mock.expect_universe_domain()
2832 .returning(move || Some(universe_domain_clone.clone()));
2833 mock.expect_headers().returning(move |_| {
2834 let mut headers = HeaderMap::new();
2835 headers.insert(
2836 "authorization",
2837 "Bearer test-user-account-token".parse().unwrap(),
2838 );
2839 Ok(CacheableResource::New {
2840 entity_tag: Default::default(),
2841 data: headers,
2842 })
2843 });
2844 let source_credentials = Credentials::from(mock);
2845
2846 let builder = Builder::from_source_credentials(source_credentials.clone())
2847 .with_target_principal("test-principal");
2848
2849 let url = builder
2851 .service_account_impersonation_url
2852 .as_ref()
2853 .expect("url should be set from the with_target_principal call")
2854 .access_token_url(&source_credentials)
2855 .await;
2856
2857 assert_eq!(
2858 url,
2859 format!(
2860 "https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2861 )
2862 );
2863
2864 let endpoint = server.url("/").to_string();
2865 let endpoint = endpoint.trim_end_matches('/');
2866
2867 let creds = builder
2868 .with_impersonation_endpoint(endpoint)
2869 .build_access_token_credentials()?;
2870
2871 let token = creds.access_token().await?;
2872 assert_eq!(token.token, "test-impersonated-token");
2873
2874 Ok(())
2875 }
2876
2877 #[test_case(ImpersonationUrlKind::TargetPrincipal("test@example.com".to_string()), "test@example.com" ; "target principal")]
2878 #[test_case(ImpersonationUrlKind::Exact("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@example.com:generateAccessToken".to_string()), "test@example.com" ; "exact url")]
2879 fn impersonation_url_client_email(kind: ImpersonationUrlKind, expected: &str) {
2880 let impersonation_url = ImpersonationUrl {
2881 endpoint: None,
2882 kind,
2883 };
2884 assert_eq!(impersonation_url.client_email().unwrap(), expected);
2885 }
2886
2887 #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "googleapis.com", "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/user@example.com:generateAccessToken" ; "target principal default universe")]
2888 #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "my-custom-universe.com", "https://iamcredentials.my-custom-universe.com/v1/projects/-/serviceAccounts/user@example.com:generateAccessToken" ; "target principal custom universe")]
2889 #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()).with_endpoint("https://iam.example.com"), "googleapis.com", "https://iam.example.com/v1/projects/-/serviceAccounts/user@example.com:generateAccessToken" ; "target principal custom endpoint override")]
2890 #[test_case(ImpersonationUrl::exact("https://iam.example.com/v1/user@example.com:generateAccessToken".to_string()), "googleapis.com", "https://iam.example.com/v1/user@example.com:generateAccessToken" ; "exact url")]
2891 #[tokio::test]
2892 async fn impersonation_url_access_token_url(
2893 impersonation_url: ImpersonationUrl,
2894 universe: &str,
2895 expected_url: &str,
2896 ) -> TestResult {
2897 let source_creds = source_creds_with_universe_domain(universe);
2898 let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2899 .build()
2900 .expect("Failed to build service account credentials");
2901
2902 let url = impersonation_url
2903 .access_token_url(&source_credentials)
2904 .await;
2905 assert_eq!(url, expected_url);
2906 Ok(())
2907 }
2908
2909 #[cfg(feature = "idtoken")]
2910 #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "googleapis.com", "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/user@example.com:generateIdToken" ; "target principal default universe")]
2911 #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "my-custom-universe.com", "https://iamcredentials.my-custom-universe.com/v1/projects/-/serviceAccounts/user@example.com:generateIdToken" ; "target principal custom universe")]
2912 #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()).with_endpoint("https://iam.example.com"), "googleapis.com", "https://iam.example.com/v1/projects/-/serviceAccounts/user@example.com:generateIdToken" ; "target principal custom endpoint override")]
2913 #[test_case(ImpersonationUrl::exact("https://iam.example.com/v1/user@example.com:generateAccessToken".to_string()), "googleapis.com", "https://iam.example.com/v1/user@example.com:generateIdToken" ; "exact url id token")]
2914 #[tokio::test]
2915 async fn impersonation_url_id_token_url(
2916 impersonation_url: ImpersonationUrl,
2917 universe: &str,
2918 expected_url: &str,
2919 ) -> TestResult {
2920 let source_creds = source_creds_with_universe_domain(universe);
2921 let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2922 .build()
2923 .expect("Failed to build service account credentials");
2924
2925 let url = impersonation_url.id_token_url(&source_credentials).await;
2926 assert_eq!(url, expected_url);
2927 Ok(())
2928 }
2929}