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