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