1use crate::access_boundary::CredentialsWithAccessBoundary;
95use crate::build_errors::Error as BuilderError;
96use crate::constants::DEFAULT_SCOPE;
97use crate::credentials::dynamic::{AccessTokenCredentialsProvider, CredentialsProvider};
98use crate::credentials::{
99 AccessToken, AccessTokenCredentials, CacheableResource, Credentials, build_credentials,
100 extract_credential_type,
101};
102use crate::errors::{self, CredentialsError};
103use crate::headers_util::{
104 self, ACCESS_TOKEN_REQUEST_TYPE, AuthHeadersBuilder, metrics_header_value,
105};
106use crate::retry::{Builder as RetryTokenProviderBuilder, TokenProviderWithRetry};
107use crate::token::{CachedTokenProvider, Token, TokenProvider};
108use crate::token_cache::TokenCache;
109use crate::{BuildResult, Result};
110use async_trait::async_trait;
111use google_cloud_gax::backoff_policy::BackoffPolicyArg;
112use google_cloud_gax::retry_policy::RetryPolicyArg;
113use google_cloud_gax::retry_throttler::RetryThrottlerArg;
114use http::{Extensions, HeaderMap};
115use reqwest::Client;
116use serde_json::Value;
117use std::fmt::Debug;
118use std::sync::Arc;
119use std::time::Duration;
120use time::OffsetDateTime;
121use tokio::time::Instant;
122
123pub(crate) const IMPERSONATED_CREDENTIAL_TYPE: &str = "imp";
124pub(crate) const DEFAULT_LIFETIME: Duration = Duration::from_secs(3600);
125pub(crate) const MSG: &str = "failed to fetch token";
126
127#[derive(Debug, Clone)]
128pub(crate) enum BuilderSource {
129 FromJson(Value),
130 FromCredentials(Credentials),
131}
132
133pub struct Builder {
153 source: BuilderSource,
154 service_account_impersonation_url: Option<String>,
155 delegates: Option<Vec<String>>,
156 scopes: Option<Vec<String>>,
157 quota_project_id: Option<String>,
158 lifetime: Option<Duration>,
159 retry_builder: RetryTokenProviderBuilder,
160 iam_endpoint_override: Option<String>,
161 is_access_boundary_enabled: bool,
162}
163
164impl Builder {
165 pub fn new(impersonated_credential: Value) -> Self {
173 Self {
174 source: BuilderSource::FromJson(impersonated_credential),
175 service_account_impersonation_url: None,
176 delegates: None,
177 scopes: None,
178 quota_project_id: None,
179 lifetime: None,
180 retry_builder: RetryTokenProviderBuilder::default(),
181 iam_endpoint_override: None,
182 is_access_boundary_enabled: true,
183 }
184 }
185
186 pub fn from_source_credentials(source_credentials: Credentials) -> Self {
204 Self {
205 source: BuilderSource::FromCredentials(source_credentials),
206 service_account_impersonation_url: None,
207 delegates: None,
208 scopes: None,
209 quota_project_id: None,
210 lifetime: None,
211 retry_builder: RetryTokenProviderBuilder::default(),
212 iam_endpoint_override: None,
213 is_access_boundary_enabled: true,
214 }
215 }
216
217 pub fn with_target_principal<S: Into<String>>(mut self, target_principal: S) -> Self {
234 self.service_account_impersonation_url = Some(format!(
235 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken",
236 target_principal.into()
237 ));
238 self
239 }
240
241 pub fn with_delegates<I, S>(mut self, delegates: I) -> Self
257 where
258 I: IntoIterator<Item = S>,
259 S: Into<String>,
260 {
261 self.delegates = Some(delegates.into_iter().map(|s| s.into()).collect());
262 self
263 }
264
265 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
287 where
288 I: IntoIterator<Item = S>,
289 S: Into<String>,
290 {
291 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
292 self
293 }
294
295 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
321 self.quota_project_id = Some(quota_project_id.into());
322 self
323 }
324
325 pub fn with_lifetime(mut self, lifetime: Duration) -> Self {
342 self.lifetime = Some(lifetime);
343 self
344 }
345
346 pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
363 self.retry_builder = self.retry_builder.with_retry_policy(v.into());
364 self
365 }
366
367 pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
385 self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
386 self
387 }
388
389 pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
412 self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
413 self
414 }
415
416 pub fn build(self) -> BuildResult<Credentials> {
433 Ok(self.build_credentials()?.into())
434 }
435
436 #[cfg(test)]
437 fn maybe_iam_endpoint_override(mut self, iam_endpoint_override: Option<String>) -> Self {
438 self.iam_endpoint_override = iam_endpoint_override;
439 self
440 }
441
442 #[cfg(test)]
443 fn without_access_boundary(mut self) -> Self {
444 self.is_access_boundary_enabled = false;
445 self
446 }
447
448 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
490 Ok(self.build_credentials()?.into())
491 }
492
493 fn build_credentials(
494 self,
495 ) -> BuildResult<CredentialsWithAccessBoundary<ImpersonatedServiceAccount<TokenCache>>> {
496 let is_access_boundary_enabled = self.is_access_boundary_enabled;
497 let service_account_impersonation_url = self.resolve_impersonation_url()?;
498 let client_email = extract_client_email(&service_account_impersonation_url)?;
499 let iam_endpoint_override = self.iam_endpoint_override.clone();
500 let (token_provider, quota_project_id) = self.build_components()?;
501 let access_boundary_url = crate::access_boundary::service_account_lookup_url(
502 &client_email,
503 iam_endpoint_override.as_deref(),
504 );
505 let creds = ImpersonatedServiceAccount {
506 token_provider: TokenCache::new(token_provider),
507 quota_project_id,
508 };
509
510 if !is_access_boundary_enabled {
511 return Ok(CredentialsWithAccessBoundary::new_no_op(creds));
512 }
513
514 Ok(CredentialsWithAccessBoundary::new(
515 creds,
516 Some(access_boundary_url),
517 ))
518 }
519
520 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
554 let iam_endpoint = self.iam_endpoint_override.clone();
555 let source = self.source.clone();
556 if let BuilderSource::FromJson(json) = source {
557 let signer = build_signer_from_json(json.clone())?;
559 if let Some(signer) = signer {
560 return Ok(signer);
561 }
562 }
563 let service_account_impersonation_url = self.resolve_impersonation_url()?;
564 let client_email = extract_client_email(&service_account_impersonation_url)?;
565 let creds = self.build()?;
566 let signer = crate::signer::iam::IamSigner::new(client_email, creds, iam_endpoint);
567 Ok(crate::signer::Signer {
568 inner: Arc::new(signer),
569 })
570 }
571
572 fn build_components(
573 self,
574 ) -> BuildResult<(
575 TokenProviderWithRetry<ImpersonatedTokenProvider>,
576 Option<String>,
577 )> {
578 let components = match self.source {
579 BuilderSource::FromJson(json) => build_components_from_json(json)?,
580 BuilderSource::FromCredentials(source_credentials) => {
581 build_components_from_credentials(
582 source_credentials,
583 self.service_account_impersonation_url,
584 )?
585 }
586 };
587
588 let scopes = self
589 .scopes
590 .or(components.scopes)
591 .unwrap_or_else(|| vec![DEFAULT_SCOPE.to_string()]);
592
593 let quota_project_id = self.quota_project_id.or(components.quota_project_id);
594 let delegates = self.delegates.or(components.delegates);
595
596 let token_provider = ImpersonatedTokenProvider {
597 source_credentials: components.source_credentials,
598 service_account_impersonation_url: components.service_account_impersonation_url,
599 delegates,
600 scopes,
601 lifetime: self.lifetime.unwrap_or(DEFAULT_LIFETIME),
602 };
603 let token_provider = self.retry_builder.build(token_provider);
604 Ok((token_provider, quota_project_id))
605 }
606
607 fn resolve_impersonation_url(&self) -> BuildResult<String> {
608 match self.source.clone() {
609 BuilderSource::FromJson(json) => {
610 let config = config_from_json(json)?;
611 Ok(config.service_account_impersonation_url)
612 }
613 BuilderSource::FromCredentials(_) => {
614 self.service_account_impersonation_url.clone().ok_or_else(|| {
615 BuilderError::parsing(
616 "`service_account_impersonation_url` is required when building from source credentials",
617 )
618 })
619 }
620 }
621 }
622}
623
624pub(crate) struct ImpersonatedCredentialComponents {
625 pub(crate) source_credentials: Credentials,
626 pub(crate) service_account_impersonation_url: String,
627 pub(crate) delegates: Option<Vec<String>>,
628 pub(crate) quota_project_id: Option<String>,
629 pub(crate) scopes: Option<Vec<String>>,
630}
631
632fn config_from_json(json: Value) -> BuildResult<ImpersonatedConfig> {
633 serde_json::from_value::<ImpersonatedConfig>(json).map_err(BuilderError::parsing)
634}
635
636pub(crate) fn build_components_from_json(
637 json: Value,
638) -> BuildResult<ImpersonatedCredentialComponents> {
639 let config = config_from_json(json)?;
640
641 let source_credential_type = extract_credential_type(&config.source_credentials)?;
642 if source_credential_type == "impersonated_service_account" {
643 return Err(BuilderError::parsing(
644 "source credential of type `impersonated_service_account` is not supported. \
645 Use the `delegates` field to specify a delegation chain.",
646 ));
647 }
648
649 let source_credentials = build_credentials(Some(config.source_credentials), None, None)?.into();
655
656 Ok(ImpersonatedCredentialComponents {
657 source_credentials,
658 service_account_impersonation_url: config.service_account_impersonation_url,
659 delegates: config.delegates,
660 quota_project_id: config.quota_project_id,
661 scopes: config.scopes,
662 })
663}
664
665fn build_signer_from_json(json: Value) -> BuildResult<Option<crate::signer::Signer>> {
669 use crate::credentials::service_account::ServiceAccountKey;
670 use crate::signer::service_account::ServiceAccountSigner;
671
672 let config = config_from_json(json)?;
673
674 let client_email = extract_client_email(&config.service_account_impersonation_url)?;
675 let source_credential_type = extract_credential_type(&config.source_credentials)?;
676 if source_credential_type == "service_account" {
677 let service_account_key =
678 serde_json::from_value::<ServiceAccountKey>(config.source_credentials)
679 .map_err(BuilderError::parsing)?;
680 let signing_provider = ServiceAccountSigner::from_impersonated_service_account(
681 service_account_key,
682 client_email,
683 );
684 let signer = crate::signer::Signer {
685 inner: Arc::new(signing_provider),
686 };
687 return Ok(Some(signer));
688 }
689 Ok(None)
690}
691
692fn extract_client_email(service_account_impersonation_url: &str) -> BuildResult<String> {
693 let mut parts = service_account_impersonation_url.split("/serviceAccounts/");
694 match (parts.nth(1), parts.next()) {
695 (Some(email), None) => Ok(email.trim_end_matches(":generateAccessToken").to_string()),
696 _ => Err(BuilderError::parsing(
697 "invalid service account impersonation URL",
698 )),
699 }
700}
701
702pub(crate) fn build_components_from_credentials(
703 source_credentials: Credentials,
704 service_account_impersonation_url: Option<String>,
705) -> BuildResult<ImpersonatedCredentialComponents> {
706 let url = service_account_impersonation_url.ok_or_else(|| {
707 BuilderError::parsing(
708 "`service_account_impersonation_url` is required when building from source credentials",
709 )
710 })?;
711 Ok(ImpersonatedCredentialComponents {
712 source_credentials,
713 service_account_impersonation_url: url,
714 delegates: None,
715 quota_project_id: None,
716 scopes: None,
717 })
718}
719
720#[derive(serde::Deserialize, Debug, PartialEq)]
721struct ImpersonatedConfig {
722 service_account_impersonation_url: String,
723 source_credentials: Value,
724 delegates: Option<Vec<String>>,
725 quota_project_id: Option<String>,
726 scopes: Option<Vec<String>>,
727}
728
729#[derive(Debug)]
730struct ImpersonatedServiceAccount<T>
731where
732 T: CachedTokenProvider,
733{
734 token_provider: T,
735 quota_project_id: Option<String>,
736}
737
738#[async_trait::async_trait]
739impl<T> CredentialsProvider for ImpersonatedServiceAccount<T>
740where
741 T: CachedTokenProvider,
742{
743 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
744 let token = self.token_provider.token(extensions).await?;
745
746 AuthHeadersBuilder::new(&token)
747 .maybe_quota_project_id(self.quota_project_id.as_deref())
748 .build()
749 }
750}
751
752#[async_trait::async_trait]
753impl<T> AccessTokenCredentialsProvider for ImpersonatedServiceAccount<T>
754where
755 T: CachedTokenProvider,
756{
757 async fn access_token(&self) -> Result<AccessToken> {
758 let token = self.token_provider.token(Extensions::new()).await?;
759 token.into()
760 }
761}
762
763struct ImpersonatedTokenProvider {
764 source_credentials: Credentials,
765 service_account_impersonation_url: String,
766 delegates: Option<Vec<String>>,
767 scopes: Vec<String>,
768 lifetime: Duration,
769}
770
771impl Debug for ImpersonatedTokenProvider {
772 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773 f.debug_struct("ImpersonatedTokenProvider")
774 .field("source_credentials", &self.source_credentials)
775 .field(
776 "service_account_impersonation_url",
777 &self.service_account_impersonation_url,
778 )
779 .field("delegates", &self.delegates)
780 .field("scopes", &self.scopes)
781 .field("lifetime", &self.lifetime)
782 .finish()
783 }
784}
785
786#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
787struct GenerateAccessTokenRequest {
788 #[serde(skip_serializing_if = "Option::is_none")]
789 delegates: Option<Vec<String>>,
790 scope: Vec<String>,
791 lifetime: String,
792}
793
794pub(crate) async fn generate_access_token(
795 source_headers: HeaderMap,
796 delegates: Option<Vec<String>>,
797 scopes: Vec<String>,
798 lifetime: Duration,
799 service_account_impersonation_url: &str,
800) -> Result<Token> {
801 let client = Client::new();
802 let body = GenerateAccessTokenRequest {
803 delegates,
804 scope: scopes,
805 lifetime: format!("{}s", lifetime.as_secs_f64()),
806 };
807
808 let response = client
809 .post(service_account_impersonation_url)
810 .header("Content-Type", "application/json")
811 .header(
812 headers_util::X_GOOG_API_CLIENT,
813 metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
814 )
815 .headers(source_headers)
816 .json(&body)
817 .send()
818 .await
819 .map_err(|e| errors::from_http_error(e, MSG))?;
820
821 if !response.status().is_success() {
822 let err = errors::from_http_response(response, MSG).await;
823 return Err(err);
824 }
825
826 let token_response = response
827 .json::<GenerateAccessTokenResponse>()
828 .await
829 .map_err(|e| {
830 let retryable = !e.is_decode();
831 CredentialsError::from_source(retryable, e)
832 })?;
833
834 let parsed_dt = OffsetDateTime::parse(
835 &token_response.expire_time,
836 &time::format_description::well_known::Rfc3339,
837 )
838 .map_err(errors::non_retryable)?;
839
840 let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
841 let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
842
843 let token = Token {
844 token: token_response.access_token,
845 token_type: "Bearer".to_string(),
846 expires_at: Some(expires_at),
847 metadata: None,
848 };
849 Ok(token)
850}
851
852#[async_trait]
853impl TokenProvider for ImpersonatedTokenProvider {
854 async fn token(&self) -> Result<Token> {
855 let source_headers = self.source_credentials.headers(Extensions::new()).await?;
856 let source_headers = match source_headers {
857 CacheableResource::New { data, .. } => data,
858 CacheableResource::NotModified => {
859 unreachable!("requested source credentials without a caching etag")
860 }
861 };
862 generate_access_token(
863 source_headers,
864 self.delegates.clone(),
865 self.scopes.clone(),
866 self.lifetime,
867 &self.service_account_impersonation_url,
868 )
869 .await
870 }
871}
872
873#[derive(serde::Deserialize)]
874struct GenerateAccessTokenResponse {
875 #[serde(rename = "accessToken")]
876 access_token: String,
877 #[serde(rename = "expireTime")]
878 expire_time: String,
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884 use crate::credentials::service_account::ServiceAccountKey;
885 use crate::credentials::tests::PKCS8_PK;
886 use crate::credentials::tests::{
887 find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
888 get_mock_retry_throttler,
889 };
890 use crate::errors::CredentialsError;
891 use base64::{Engine, prelude::BASE64_STANDARD};
892 use httptest::cycle;
893 use httptest::{Expectation, Server, matchers::*, responders::*};
894 use serde_json::Value;
895 use serde_json::json;
896 use serial_test::parallel;
897
898 type TestResult = anyhow::Result<()>;
899
900 #[tokio::test]
901 #[parallel]
902 async fn test_generate_access_token_success() -> TestResult {
903 let server = Server::run();
904 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
905 .format(&time::format_description::well_known::Rfc3339)
906 .unwrap();
907 server.expect(
908 Expectation::matching(all_of![
909 request::method_path(
910 "POST",
911 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
912 ),
913 request::headers(contains(("authorization", "Bearer test-token"))),
914 ])
915 .respond_with(json_encoded(json!({
916 "accessToken": "test-impersonated-token",
917 "expireTime": expire_time
918 }))),
919 );
920
921 let mut headers = HeaderMap::new();
922 headers.insert("authorization", "Bearer test-token".parse().unwrap());
923 let token = generate_access_token(
924 headers,
925 None,
926 vec!["scope".to_string()],
927 DEFAULT_LIFETIME,
928 &server
929 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
930 .to_string(),
931 )
932 .await?;
933
934 assert_eq!(token.token, "test-impersonated-token");
935 Ok(())
936 }
937
938 #[tokio::test]
939 #[parallel]
940 async fn test_generate_access_token_403() -> TestResult {
941 let server = Server::run();
942 server.expect(
943 Expectation::matching(all_of![
944 request::method_path(
945 "POST",
946 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
947 ),
948 request::headers(contains(("authorization", "Bearer test-token"))),
949 ])
950 .respond_with(status_code(403)),
951 );
952
953 let mut headers = HeaderMap::new();
954 headers.insert("authorization", "Bearer test-token".parse().unwrap());
955 let err = generate_access_token(
956 headers,
957 None,
958 vec!["scope".to_string()],
959 DEFAULT_LIFETIME,
960 &server
961 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
962 .to_string(),
963 )
964 .await
965 .unwrap_err();
966
967 assert!(!err.is_transient());
968 Ok(())
969 }
970
971 #[tokio::test]
972 #[parallel]
973 async fn test_generate_access_token_no_auth_header() -> TestResult {
974 let server = Server::run();
975 server.expect(
976 Expectation::matching(request::method_path(
977 "POST",
978 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
979 ))
980 .respond_with(status_code(401)),
981 );
982
983 let err = generate_access_token(
984 HeaderMap::new(),
985 None,
986 vec!["scope".to_string()],
987 DEFAULT_LIFETIME,
988 &server
989 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
990 .to_string(),
991 )
992 .await
993 .unwrap_err();
994
995 assert!(!err.is_transient());
996 Ok(())
997 }
998
999 #[tokio::test]
1000 #[parallel]
1001 async fn test_impersonated_service_account() -> TestResult {
1002 let server = Server::run();
1003 server.expect(
1004 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1005 json_encoded(json!({
1006 "access_token": "test-user-account-token",
1007 "expires_in": 3600,
1008 "token_type": "Bearer",
1009 })),
1010 ),
1011 );
1012 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1013 .format(&time::format_description::well_known::Rfc3339)
1014 .unwrap();
1015 server.expect(
1016 Expectation::matching(all_of![
1017 request::method_path(
1018 "POST",
1019 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1020 ),
1021 request::headers(contains((
1022 "authorization",
1023 "Bearer test-user-account-token"
1024 ))),
1025 request::body(json_decoded(eq(json!({
1026 "scope": ["scope1", "scope2"],
1027 "lifetime": "3600s"
1028 }))))
1029 ])
1030 .respond_with(json_encoded(json!({
1031 "accessToken": "test-impersonated-token",
1032 "expireTime": expire_time
1033 }))),
1034 );
1035
1036 let impersonated_credential = json!({
1037 "type": "impersonated_service_account",
1038 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1039 "source_credentials": {
1040 "type": "authorized_user",
1041 "client_id": "test-client-id",
1042 "client_secret": "test-client-secret",
1043 "refresh_token": "test-refresh-token",
1044 "token_uri": server.url("/token").to_string()
1045 }
1046 });
1047 let (token_provider, _) = Builder::new(impersonated_credential)
1048 .with_scopes(vec!["scope1", "scope2"])
1049 .build_components()?;
1050
1051 let token = token_provider.token().await?;
1052 assert_eq!(token.token, "test-impersonated-token");
1053 assert_eq!(token.token_type, "Bearer");
1054
1055 Ok(())
1056 }
1057
1058 #[tokio::test]
1059 #[parallel]
1060 async fn test_impersonated_service_account_default_scope() -> TestResult {
1061 let server = Server::run();
1062 server.expect(
1063 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1064 json_encoded(json!({
1065 "access_token": "test-user-account-token",
1066 "expires_in": 3600,
1067 "token_type": "Bearer",
1068 })),
1069 ),
1070 );
1071 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1072 .format(&time::format_description::well_known::Rfc3339)
1073 .unwrap();
1074 server.expect(
1075 Expectation::matching(all_of![
1076 request::method_path(
1077 "POST",
1078 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1079 ),
1080 request::headers(contains((
1081 "authorization",
1082 "Bearer test-user-account-token"
1083 ))),
1084 request::body(json_decoded(eq(json!({
1085 "scope": [DEFAULT_SCOPE],
1086 "lifetime": "3600s"
1087 }))))
1088 ])
1089 .respond_with(json_encoded(json!({
1090 "accessToken": "test-impersonated-token",
1091 "expireTime": expire_time
1092 }))),
1093 );
1094
1095 let impersonated_credential = json!({
1096 "type": "impersonated_service_account",
1097 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1098 "source_credentials": {
1099 "type": "authorized_user",
1100 "client_id": "test-client-id",
1101 "client_secret": "test-client-secret",
1102 "refresh_token": "test-refresh-token",
1103 "token_uri": server.url("/token").to_string()
1104 }
1105 });
1106 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1107
1108 let token = token_provider.token().await?;
1109 assert_eq!(token.token, "test-impersonated-token");
1110 assert_eq!(token.token_type, "Bearer");
1111
1112 Ok(())
1113 }
1114
1115 #[tokio::test]
1116 #[parallel]
1117 async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
1118 let server = Server::run();
1119 server.expect(
1120 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1121 json_encoded(json!({
1122 "access_token": "test-user-account-token",
1123 "expires_in": 3600,
1124 "token_type": "Bearer",
1125 })),
1126 ),
1127 );
1128 let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
1129 .format(&time::format_description::well_known::Rfc3339)
1130 .unwrap();
1131 server.expect(
1132 Expectation::matching(all_of![
1133 request::method_path(
1134 "POST",
1135 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1136 ),
1137 request::headers(contains((
1138 "authorization",
1139 "Bearer test-user-account-token"
1140 ))),
1141 request::body(json_decoded(eq(json!({
1142 "scope": ["scope1", "scope2"],
1143 "lifetime": "3.5s"
1144 }))))
1145 ])
1146 .respond_with(json_encoded(json!({
1147 "accessToken": "test-impersonated-token",
1148 "expireTime": expire_time
1149 }))),
1150 );
1151
1152 let impersonated_credential = json!({
1153 "type": "impersonated_service_account",
1154 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1155 "source_credentials": {
1156 "type": "authorized_user",
1157 "client_id": "test-client-id",
1158 "client_secret": "test-client-secret",
1159 "refresh_token": "test-refresh-token",
1160 "token_uri": server.url("/token").to_string()
1161 }
1162 });
1163 let (token_provider, _) = Builder::new(impersonated_credential)
1164 .with_scopes(vec!["scope1", "scope2"])
1165 .with_lifetime(Duration::from_secs_f32(3.5))
1166 .build_components()?;
1167
1168 let token = token_provider.token().await?;
1169 assert_eq!(token.token, "test-impersonated-token");
1170
1171 Ok(())
1172 }
1173
1174 #[tokio::test]
1175 #[parallel]
1176 async fn test_with_delegates() -> TestResult {
1177 let server = Server::run();
1178 server.expect(
1179 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1180 json_encoded(json!({
1181 "access_token": "test-user-account-token",
1182 "expires_in": 3600,
1183 "token_type": "Bearer",
1184 })),
1185 ),
1186 );
1187 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1188 .format(&time::format_description::well_known::Rfc3339)
1189 .unwrap();
1190 server.expect(
1191 Expectation::matching(all_of![
1192 request::method_path(
1193 "POST",
1194 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1195 ),
1196 request::headers(contains((
1197 "authorization",
1198 "Bearer test-user-account-token"
1199 ))),
1200 request::body(json_decoded(eq(json!({
1201 "scope": [DEFAULT_SCOPE],
1202 "lifetime": "3600s",
1203 "delegates": ["delegate1", "delegate2"]
1204 }))))
1205 ])
1206 .respond_with(json_encoded(json!({
1207 "accessToken": "test-impersonated-token",
1208 "expireTime": expire_time
1209 }))),
1210 );
1211
1212 let impersonated_credential = json!({
1213 "type": "impersonated_service_account",
1214 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1215 "source_credentials": {
1216 "type": "authorized_user",
1217 "client_id": "test-client-id",
1218 "client_secret": "test-client-secret",
1219 "refresh_token": "test-refresh-token",
1220 "token_uri": server.url("/token").to_string()
1221 }
1222 });
1223 let (token_provider, _) = Builder::new(impersonated_credential)
1224 .with_delegates(vec!["delegate1", "delegate2"])
1225 .build_components()?;
1226
1227 let token = token_provider.token().await?;
1228 assert_eq!(token.token, "test-impersonated-token");
1229 assert_eq!(token.token_type, "Bearer");
1230
1231 Ok(())
1232 }
1233
1234 #[tokio::test]
1235 #[parallel]
1236 async fn test_impersonated_service_account_fail() -> TestResult {
1237 let server = Server::run();
1238 server.expect(
1239 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1240 json_encoded(json!({
1241 "access_token": "test-user-account-token",
1242 "expires_in": 3600,
1243 "token_type": "Bearer",
1244 })),
1245 ),
1246 );
1247 server.expect(
1248 Expectation::matching(request::method_path(
1249 "POST",
1250 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1251 ))
1252 .respond_with(status_code(500)),
1253 );
1254
1255 let impersonated_credential = json!({
1256 "type": "impersonated_service_account",
1257 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1258 "source_credentials": {
1259 "type": "authorized_user",
1260 "client_id": "test-client-id",
1261 "client_secret": "test-client-secret",
1262 "refresh_token": "test-refresh-token",
1263 "token_uri": server.url("/token").to_string()
1264 }
1265 });
1266 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1267
1268 let err = token_provider.token().await.unwrap_err();
1269 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1270 assert!(original_err.is_transient());
1271
1272 Ok(())
1273 }
1274
1275 #[tokio::test]
1276 #[parallel]
1277 async fn debug_token_provider() {
1278 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1279 "type": "authorized_user",
1280 "client_id": "test-client-id",
1281 "client_secret": "test-client-secret",
1282 "refresh_token": "test-refresh-token"
1283 }))
1284 .build()
1285 .unwrap();
1286
1287 let expected = ImpersonatedTokenProvider {
1288 source_credentials,
1289 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1290 delegates: Some(vec!["delegate1".to_string()]),
1291 scopes: vec!["scope1".to_string()],
1292 lifetime: Duration::from_secs(3600),
1293 };
1294 let fmt = format!("{expected:?}");
1295 assert!(fmt.contains("UserCredentials"), "{fmt}");
1296 assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1297 assert!(fmt.contains("delegate1"), "{fmt}");
1298 assert!(fmt.contains("scope1"), "{fmt}");
1299 assert!(fmt.contains("3600s"), "{fmt}");
1300 }
1301
1302 #[test]
1303 fn impersonated_config_full_from_json_success() {
1304 let source_credentials_json = json!({
1305 "type": "authorized_user",
1306 "client_id": "test-client-id",
1307 "client_secret": "test-client-secret",
1308 "refresh_token": "test-refresh-token"
1309 });
1310 let json = json!({
1311 "type": "impersonated_service_account",
1312 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1313 "source_credentials": source_credentials_json,
1314 "delegates": ["delegate1"],
1315 "quota_project_id": "test-project-id",
1316 "scopes": ["scope1"],
1317 });
1318
1319 let expected = ImpersonatedConfig {
1320 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1321 source_credentials: source_credentials_json,
1322 delegates: Some(vec!["delegate1".to_string()]),
1323 quota_project_id: Some("test-project-id".to_string()),
1324 scopes: Some(vec!["scope1".to_string()]),
1325 };
1326 let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1327 assert_eq!(actual, expected);
1328 }
1329
1330 #[test]
1331 fn impersonated_config_partial_from_json_success() {
1332 let source_credentials_json = json!({
1333 "type": "authorized_user",
1334 "client_id": "test-client-id",
1335 "client_secret": "test-client-secret",
1336 "refresh_token": "test-refresh-token"
1337 });
1338 let json = json!({
1339 "type": "impersonated_service_account",
1340 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1341 "source_credentials": source_credentials_json
1342 });
1343
1344 let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1345 assert_eq!(
1346 config.service_account_impersonation_url,
1347 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1348 );
1349 assert_eq!(config.source_credentials, source_credentials_json);
1350 assert_eq!(config.delegates, None);
1351 assert_eq!(config.quota_project_id, None);
1352 assert_eq!(config.scopes, None);
1353 }
1354
1355 #[tokio::test]
1356 #[parallel]
1357 async fn test_impersonated_service_account_source_fail() -> TestResult {
1358 #[derive(Debug)]
1359 struct MockSourceCredentialsFail;
1360
1361 #[async_trait]
1362 impl CredentialsProvider for MockSourceCredentialsFail {
1363 async fn headers(
1364 &self,
1365 _extensions: Extensions,
1366 ) -> Result<CacheableResource<HeaderMap>> {
1367 Err(errors::non_retryable_from_str("source failed"))
1368 }
1369 }
1370
1371 let source_credentials = Credentials {
1372 inner: Arc::new(MockSourceCredentialsFail),
1373 };
1374
1375 let token_provider = ImpersonatedTokenProvider {
1376 source_credentials,
1377 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1378 delegates: Some(vec!["delegate1".to_string()]),
1379 scopes: vec!["scope1".to_string()],
1380 lifetime: DEFAULT_LIFETIME,
1381 };
1382
1383 let err = token_provider.token().await.unwrap_err();
1384 assert!(err.to_string().contains("source failed"));
1385
1386 Ok(())
1387 }
1388
1389 #[tokio::test]
1390 #[parallel]
1391 async fn test_missing_impersonation_url_fail() {
1392 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1393 "type": "authorized_user",
1394 "client_id": "test-client-id",
1395 "client_secret": "test-client-secret",
1396 "refresh_token": "test-refresh-token"
1397 }))
1398 .build()
1399 .unwrap();
1400
1401 let result = Builder::from_source_credentials(source_credentials).build();
1402 assert!(result.is_err(), "{result:?}");
1403 let err = result.unwrap_err();
1404 assert!(err.is_parsing());
1405 assert!(
1406 err.to_string()
1407 .contains("`service_account_impersonation_url` is required")
1408 );
1409 }
1410
1411 #[tokio::test]
1412 #[parallel]
1413 async fn test_nested_impersonated_credentials_fail() {
1414 let nested_impersonated = json!({
1415 "type": "impersonated_service_account",
1416 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1417 "source_credentials": {
1418 "type": "impersonated_service_account",
1419 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1420 "source_credentials": {
1421 "type": "authorized_user",
1422 "client_id": "test-client-id",
1423 "client_secret": "test-client-secret",
1424 "refresh_token": "test-refresh-token"
1425 }
1426 }
1427 });
1428
1429 let result = Builder::new(nested_impersonated).build();
1430 assert!(result.is_err(), "{result:?}");
1431 let err = result.unwrap_err();
1432 assert!(err.is_parsing());
1433 assert!(
1434 err.to_string().contains(
1435 "source credential of type `impersonated_service_account` is not supported"
1436 )
1437 );
1438 }
1439
1440 #[tokio::test]
1441 #[parallel]
1442 async fn test_malformed_impersonated_credentials_fail() {
1443 let malformed_impersonated = json!({
1444 "type": "impersonated_service_account",
1445 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1446 });
1447
1448 let result = Builder::new(malformed_impersonated).build();
1449 assert!(result.is_err(), "{result:?}");
1450 let err = result.unwrap_err();
1451 assert!(err.is_parsing());
1452 assert!(
1453 err.to_string()
1454 .contains("missing field `source_credentials`")
1455 );
1456 }
1457
1458 #[tokio::test]
1459 #[parallel]
1460 async fn test_invalid_source_credential_type_fail() {
1461 let invalid_source = json!({
1462 "type": "impersonated_service_account",
1463 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1464 "source_credentials": {
1465 "type": "invalid_type",
1466 }
1467 });
1468
1469 let result = Builder::new(invalid_source).build();
1470 assert!(result.is_err(), "{result:?}");
1471 let err = result.unwrap_err();
1472 assert!(err.is_unknown_type());
1473 }
1474
1475 #[tokio::test]
1476 #[parallel]
1477 async fn test_missing_expiry() -> TestResult {
1478 let server = Server::run();
1479 server.expect(
1480 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1481 json_encoded(json!({
1482 "access_token": "test-user-account-token",
1483 "expires_in": 3600,
1484 "token_type": "Bearer",
1485 })),
1486 ),
1487 );
1488 server.expect(
1489 Expectation::matching(request::method_path(
1490 "POST",
1491 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1492 ))
1493 .respond_with(json_encoded(json!({
1494 "accessToken": "test-impersonated-token",
1495 }))),
1496 );
1497
1498 let impersonated_credential = json!({
1499 "type": "impersonated_service_account",
1500 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1501 "source_credentials": {
1502 "type": "authorized_user",
1503 "client_id": "test-client-id",
1504 "client_secret": "test-client-secret",
1505 "refresh_token": "test-refresh-token",
1506 "token_uri": server.url("/token").to_string()
1507 }
1508 });
1509 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1510
1511 let err = token_provider.token().await.unwrap_err();
1512 assert!(!err.is_transient());
1513
1514 Ok(())
1515 }
1516
1517 #[tokio::test]
1518 #[parallel]
1519 async fn test_invalid_expiry_format() -> TestResult {
1520 let server = Server::run();
1521 server.expect(
1522 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1523 json_encoded(json!({
1524 "access_token": "test-user-account-token",
1525 "expires_in": 3600,
1526 "token_type": "Bearer",
1527 })),
1528 ),
1529 );
1530 server.expect(
1531 Expectation::matching(request::method_path(
1532 "POST",
1533 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1534 ))
1535 .respond_with(json_encoded(json!({
1536 "accessToken": "test-impersonated-token",
1537 "expireTime": "invalid-format"
1538 }))),
1539 );
1540
1541 let impersonated_credential = json!({
1542 "type": "impersonated_service_account",
1543 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1544 "source_credentials": {
1545 "type": "authorized_user",
1546 "client_id": "test-client-id",
1547 "client_secret": "test-client-secret",
1548 "refresh_token": "test-refresh-token",
1549 "token_uri": server.url("/token").to_string()
1550 }
1551 });
1552 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1553
1554 let err = token_provider.token().await.unwrap_err();
1555 assert!(!err.is_transient());
1556
1557 Ok(())
1558 }
1559
1560 #[tokio::test]
1561 #[parallel]
1562 async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1563 let server = Server::run();
1564 server.expect(
1565 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1566 json_encoded(json!({
1567 "access_token": "test-user-account-token",
1568 "expires_in": 3600,
1569 "token_type": "Bearer",
1570 })),
1571 ),
1572 );
1573 server.expect(
1574 Expectation::matching(request::method_path(
1575 "POST",
1576 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1577 ))
1578 .respond_with(json_encoded(json!("bad json"))),
1579 );
1580
1581 let impersonated_credential = json!({
1582 "type": "impersonated_service_account",
1583 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1584 "source_credentials": {
1585 "type": "authorized_user",
1586 "client_id": "test-client-id",
1587 "client_secret": "test-client-secret",
1588 "refresh_token": "test-refresh-token",
1589 "token_uri": server.url("/token").to_string()
1590 }
1591 });
1592 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1593
1594 let e = token_provider.token().await.err().unwrap();
1595 assert!(!e.is_transient(), "{e}");
1596
1597 Ok(())
1598 }
1599
1600 #[tokio::test]
1601 #[parallel]
1602 async fn token_provider_nonretryable_error() -> TestResult {
1603 let server = Server::run();
1604 server.expect(
1605 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1606 json_encoded(json!({
1607 "access_token": "test-user-account-token",
1608 "expires_in": 3600,
1609 "token_type": "Bearer",
1610 })),
1611 ),
1612 );
1613 server.expect(
1614 Expectation::matching(request::method_path(
1615 "POST",
1616 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1617 ))
1618 .respond_with(status_code(401)),
1619 );
1620
1621 let impersonated_credential = json!({
1622 "type": "impersonated_service_account",
1623 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1624 "source_credentials": {
1625 "type": "authorized_user",
1626 "client_id": "test-client-id",
1627 "client_secret": "test-client-secret",
1628 "refresh_token": "test-refresh-token",
1629 "token_uri": server.url("/token").to_string()
1630 }
1631 });
1632 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1633
1634 let err = token_provider.token().await.unwrap_err();
1635 assert!(!err.is_transient());
1636
1637 Ok(())
1638 }
1639
1640 #[tokio::test]
1641 #[parallel]
1642 async fn credential_full_with_quota_project_from_builder() -> TestResult {
1643 let server = Server::run();
1644 server.expect(
1645 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1646 json_encoded(json!({
1647 "access_token": "test-user-account-token",
1648 "expires_in": 3600,
1649 "token_type": "Bearer",
1650 })),
1651 ),
1652 );
1653 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1654 .format(&time::format_description::well_known::Rfc3339)
1655 .unwrap();
1656 server.expect(
1657 Expectation::matching(request::method_path(
1658 "POST",
1659 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1660 ))
1661 .respond_with(json_encoded(json!({
1662 "accessToken": "test-impersonated-token",
1663 "expireTime": expire_time
1664 }))),
1665 );
1666
1667 let impersonated_credential = json!({
1668 "type": "impersonated_service_account",
1669 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1670 "source_credentials": {
1671 "type": "authorized_user",
1672 "client_id": "test-client-id",
1673 "client_secret": "test-client-secret",
1674 "refresh_token": "test-refresh-token",
1675 "token_uri": server.url("/token").to_string()
1676 }
1677 });
1678 let creds = Builder::new(impersonated_credential)
1679 .with_quota_project_id("test-project")
1680 .build()?;
1681
1682 let headers = creds.headers(Extensions::new()).await?;
1683 match headers {
1684 CacheableResource::New { data, .. } => {
1685 assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1686 }
1687 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1688 }
1689
1690 Ok(())
1691 }
1692
1693 #[tokio::test]
1694 #[parallel]
1695 async fn access_token_credentials_success() -> TestResult {
1696 let server = Server::run();
1697 server.expect(
1698 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1699 json_encoded(json!({
1700 "access_token": "test-user-account-token",
1701 "expires_in": 3600,
1702 "token_type": "Bearer",
1703 })),
1704 ),
1705 );
1706 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1707 .format(&time::format_description::well_known::Rfc3339)
1708 .unwrap();
1709 server.expect(
1710 Expectation::matching(request::method_path(
1711 "POST",
1712 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1713 ))
1714 .respond_with(json_encoded(json!({
1715 "accessToken": "test-impersonated-token",
1716 "expireTime": expire_time
1717 }))),
1718 );
1719
1720 let impersonated_credential = json!({
1721 "type": "impersonated_service_account",
1722 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1723 "source_credentials": {
1724 "type": "authorized_user",
1725 "client_id": "test-client-id",
1726 "client_secret": "test-client-secret",
1727 "refresh_token": "test-refresh-token",
1728 "token_uri": server.url("/token").to_string()
1729 }
1730 });
1731 let creds = Builder::new(impersonated_credential).build_access_token_credentials()?;
1732
1733 let access_token = creds.access_token().await?;
1734 assert_eq!(access_token.token, "test-impersonated-token");
1735
1736 Ok(())
1737 }
1738
1739 #[tokio::test]
1740 #[parallel]
1741 async fn test_with_target_principal() {
1742 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1743 "type": "authorized_user",
1744 "client_id": "test-client-id",
1745 "client_secret": "test-client-secret",
1746 "refresh_token": "test-refresh-token"
1747 }))
1748 .build()
1749 .unwrap();
1750
1751 let (token_provider, _) = Builder::from_source_credentials(source_credentials)
1752 .with_target_principal("test-principal@example.iam.gserviceaccount.com")
1753 .build_components()
1754 .unwrap();
1755
1756 assert_eq!(
1757 token_provider.inner.service_account_impersonation_url,
1758 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
1759 );
1760 }
1761
1762 #[tokio::test]
1763 #[parallel]
1764 async fn credential_full_with_quota_project_from_json() -> TestResult {
1765 let server = Server::run();
1766 server.expect(
1767 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1768 json_encoded(json!({
1769 "access_token": "test-user-account-token",
1770 "expires_in": 3600,
1771 "token_type": "Bearer",
1772 })),
1773 ),
1774 );
1775 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1776 .format(&time::format_description::well_known::Rfc3339)
1777 .unwrap();
1778 server.expect(
1779 Expectation::matching(request::method_path(
1780 "POST",
1781 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1782 ))
1783 .respond_with(json_encoded(json!({
1784 "accessToken": "test-impersonated-token",
1785 "expireTime": expire_time
1786 }))),
1787 );
1788
1789 let impersonated_credential = json!({
1790 "type": "impersonated_service_account",
1791 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1792 "source_credentials": {
1793 "type": "authorized_user",
1794 "client_id": "test-client-id",
1795 "client_secret": "test-client-secret",
1796 "refresh_token": "test-refresh-token",
1797 "token_uri": server.url("/token").to_string()
1798 },
1799 "quota_project_id": "test-project-from-json",
1800 });
1801
1802 let creds = Builder::new(impersonated_credential).build()?;
1803
1804 let headers = creds.headers(Extensions::new()).await?;
1805 println!("headers: {:#?}", headers);
1806 match headers {
1807 CacheableResource::New { data, .. } => {
1808 assert_eq!(
1809 data.get("x-goog-user-project").unwrap(),
1810 "test-project-from-json"
1811 );
1812 }
1813 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1814 }
1815
1816 Ok(())
1817 }
1818
1819 #[tokio::test]
1820 #[parallel]
1821 async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
1822 let server = Server::run();
1823
1824 server.expect(
1827 Expectation::matching(all_of![
1828 request::method_path("POST", "/source_token"),
1829 request::body(json_decoded(
1830 |body: &serde_json::Value| body["scopes"].is_null()
1831 ))
1832 ])
1833 .respond_with(json_encoded(json!({
1834 "access_token": "source-token",
1835 "expires_in": 3600,
1836 "token_type": "Bearer",
1837 }))),
1838 );
1839
1840 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1841 .format(&time::format_description::well_known::Rfc3339)
1842 .unwrap();
1843
1844 server.expect(
1847 Expectation::matching(all_of![
1848 request::method_path(
1849 "POST",
1850 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1851 ),
1852 request::headers(contains(("authorization", "Bearer source-token"))),
1853 request::body(json_decoded(eq(json!({
1854 "scope": ["impersonated-scope"],
1855 "lifetime": "3600s"
1856 }))))
1857 ])
1858 .respond_with(json_encoded(json!({
1859 "accessToken": "impersonated-token",
1860 "expireTime": expire_time
1861 }))),
1862 );
1863
1864 let impersonated_credential = json!({
1865 "type": "impersonated_service_account",
1866 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1867 "source_credentials": {
1868 "type": "authorized_user",
1869 "client_id": "test-client-id",
1870 "client_secret": "test-client-secret",
1871 "refresh_token": "test-refresh-token",
1872 "token_uri": server.url("/source_token").to_string()
1873 }
1874 });
1875
1876 let creds = Builder::new(impersonated_credential)
1877 .with_scopes(vec!["impersonated-scope"])
1878 .with_quota_project_id("impersonated-quota-project")
1879 .build()?;
1880
1881 let fmt = format!("{creds:?}");
1883 assert!(fmt.contains("impersonated-quota-project"));
1884
1885 let _token = creds.headers(Extensions::new()).await?;
1887
1888 Ok(())
1889 }
1890
1891 #[tokio::test]
1892 #[parallel]
1893 async fn test_impersonated_metrics_header() -> TestResult {
1894 let server = Server::run();
1895 server.expect(
1896 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1897 json_encoded(json!({
1898 "access_token": "test-user-account-token",
1899 "expires_in": 3600,
1900 "token_type": "Bearer",
1901 })),
1902 ),
1903 );
1904 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1905 .format(&time::format_description::well_known::Rfc3339)
1906 .unwrap();
1907 server.expect(
1908 Expectation::matching(all_of![
1909 request::method_path(
1910 "POST",
1911 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1912 ),
1913 request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
1914 request::headers(contains((
1915 "x-goog-api-client",
1916 matches("auth-request-type/at")
1917 )))
1918 ])
1919 .respond_with(json_encoded(json!({
1920 "accessToken": "test-impersonated-token",
1921 "expireTime": expire_time
1922 }))),
1923 );
1924
1925 let impersonated_credential = json!({
1926 "type": "impersonated_service_account",
1927 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1928 "source_credentials": {
1929 "type": "authorized_user",
1930 "client_id": "test-client-id",
1931 "client_secret": "test-client-secret",
1932 "refresh_token": "test-refresh-token",
1933 "token_uri": server.url("/token").to_string()
1934 }
1935 });
1936 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1937
1938 let token = token_provider.token().await?;
1939 assert_eq!(token.token, "test-impersonated-token");
1940 assert_eq!(token.token_type, "Bearer");
1941
1942 Ok(())
1943 }
1944
1945 #[tokio::test]
1946 #[parallel]
1947 async fn test_impersonated_retries_for_success() -> TestResult {
1948 let mut server = Server::run();
1949 server.expect(
1951 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1952 json_encoded(json!({
1953 "access_token": "test-user-account-token",
1954 "expires_in": 3600,
1955 "token_type": "Bearer",
1956 })),
1957 ),
1958 );
1959
1960 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1961 .format(&time::format_description::well_known::Rfc3339)
1962 .unwrap();
1963
1964 let impersonation_path =
1966 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1967 server.expect(
1968 Expectation::matching(request::method_path("POST", impersonation_path))
1969 .times(3)
1970 .respond_with(cycle![
1971 status_code(503).body("try-again"),
1972 status_code(503).body("try-again"),
1973 status_code(200)
1974 .append_header("Content-Type", "application/json")
1975 .body(
1976 json!({
1977 "accessToken": "test-impersonated-token",
1978 "expireTime": expire_time
1979 })
1980 .to_string()
1981 ),
1982 ]),
1983 );
1984
1985 let impersonated_credential = json!({
1986 "type": "impersonated_service_account",
1987 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1988 "source_credentials": {
1989 "type": "authorized_user",
1990 "client_id": "test-client-id",
1991 "client_secret": "test-client-secret",
1992 "refresh_token": "test-refresh-token",
1993 "token_uri": server.url("/token").to_string()
1994 }
1995 });
1996
1997 let (token_provider, _) = Builder::new(impersonated_credential)
1998 .with_retry_policy(get_mock_auth_retry_policy(3))
1999 .with_backoff_policy(get_mock_backoff_policy())
2000 .with_retry_throttler(get_mock_retry_throttler())
2001 .build_components()?;
2002
2003 let token = token_provider.token().await?;
2004 assert_eq!(token.token, "test-impersonated-token");
2005
2006 server.verify_and_clear();
2007 Ok(())
2008 }
2009
2010 #[tokio::test]
2011 #[parallel]
2012 async fn test_scopes_from_json() -> TestResult {
2013 let server = Server::run();
2014 server.expect(
2015 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2016 json_encoded(json!({
2017 "access_token": "test-user-account-token",
2018 "expires_in": 3600,
2019 "token_type": "Bearer",
2020 })),
2021 ),
2022 );
2023 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2024 .format(&time::format_description::well_known::Rfc3339)
2025 .unwrap();
2026 server.expect(
2027 Expectation::matching(all_of![
2028 request::method_path(
2029 "POST",
2030 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2031 ),
2032 request::body(json_decoded(eq(json!({
2033 "scope": ["scope-from-json"],
2034 "lifetime": "3600s"
2035 }))))
2036 ])
2037 .respond_with(json_encoded(json!({
2038 "accessToken": "test-impersonated-token",
2039 "expireTime": expire_time
2040 }))),
2041 );
2042
2043 let impersonated_credential = json!({
2044 "type": "impersonated_service_account",
2045 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2046 "scopes": ["scope-from-json"],
2047 "source_credentials": {
2048 "type": "authorized_user",
2049 "client_id": "test-client-id",
2050 "client_secret": "test-client-secret",
2051 "refresh_token": "test-refresh-token",
2052 "token_uri": server.url("/token").to_string()
2053 }
2054 });
2055 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
2056
2057 let token = token_provider.token().await?;
2058 assert_eq!(token.token, "test-impersonated-token");
2059
2060 Ok(())
2061 }
2062
2063 #[tokio::test]
2064 #[parallel]
2065 async fn test_with_scopes_overrides_json_scopes() -> TestResult {
2066 let server = Server::run();
2067 server.expect(
2068 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2069 json_encoded(json!({
2070 "access_token": "test-user-account-token",
2071 "expires_in": 3600,
2072 "token_type": "Bearer",
2073 })),
2074 ),
2075 );
2076 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2077 .format(&time::format_description::well_known::Rfc3339)
2078 .unwrap();
2079 server.expect(
2080 Expectation::matching(all_of![
2081 request::method_path(
2082 "POST",
2083 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2084 ),
2085 request::body(json_decoded(eq(json!({
2086 "scope": ["scope-from-with-scopes"],
2087 "lifetime": "3600s"
2088 }))))
2089 ])
2090 .respond_with(json_encoded(json!({
2091 "accessToken": "test-impersonated-token",
2092 "expireTime": expire_time
2093 }))),
2094 );
2095
2096 let impersonated_credential = json!({
2097 "type": "impersonated_service_account",
2098 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2099 "scopes": ["scope-from-json"],
2100 "source_credentials": {
2101 "type": "authorized_user",
2102 "client_id": "test-client-id",
2103 "client_secret": "test-client-secret",
2104 "refresh_token": "test-refresh-token",
2105 "token_uri": server.url("/token").to_string()
2106 }
2107 });
2108 let (token_provider, _) = Builder::new(impersonated_credential)
2109 .with_scopes(vec!["scope-from-with-scopes"])
2110 .build_components()?;
2111
2112 let token = token_provider.token().await?;
2113 assert_eq!(token.token, "test-impersonated-token");
2114
2115 Ok(())
2116 }
2117
2118 #[tokio::test]
2119 #[parallel]
2120 async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
2121 let mut server = Server::run();
2122 server.expect(
2124 Expectation::matching(request::method_path("POST", "/token")).respond_with(
2125 json_encoded(json!({
2126 "access_token": "test-user-account-token",
2127 "expires_in": 3600,
2128 "token_type": "Bearer",
2129 })),
2130 ),
2131 );
2132
2133 let impersonation_path =
2135 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2136 server.expect(
2137 Expectation::matching(request::method_path("POST", impersonation_path))
2138 .times(1)
2139 .respond_with(status_code(401)),
2140 );
2141
2142 let impersonated_credential = json!({
2143 "type": "impersonated_service_account",
2144 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2145 "source_credentials": {
2146 "type": "authorized_user",
2147 "client_id": "test-client-id",
2148 "client_secret": "test-client-secret",
2149 "refresh_token": "test-refresh-token",
2150 "token_uri": server.url("/token").to_string()
2151 }
2152 });
2153
2154 let (token_provider, _) = Builder::new(impersonated_credential)
2155 .with_retry_policy(get_mock_auth_retry_policy(3))
2156 .with_backoff_policy(get_mock_backoff_policy())
2157 .with_retry_throttler(get_mock_retry_throttler())
2158 .build_components()?;
2159
2160 let err = token_provider.token().await.unwrap_err();
2161 assert!(!err.is_transient());
2162
2163 server.verify_and_clear();
2164 Ok(())
2165 }
2166
2167 #[tokio::test]
2168 #[parallel]
2169 async fn test_impersonated_remote_signer() -> TestResult {
2170 let server = Server::run();
2171 server.expect(
2172 Expectation::matching(request::method_path("POST", "/token"))
2173 .times(2..)
2174 .respond_with(json_encoded(json!({
2175 "access_token": "test-user-account-token",
2176 "expires_in": 3600,
2177 "token_type": "Bearer",
2178 }))),
2179 );
2180 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2181 .format(&time::format_description::well_known::Rfc3339)
2182 .unwrap();
2183 server.expect(
2184 Expectation::matching(request::method_path(
2185 "POST",
2186 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2187 ))
2188 .times(2)
2189 .respond_with(json_encoded(json!({
2190 "accessToken": "test-impersonated-token",
2191 "expireTime": expire_time
2192 }))),
2193 );
2194
2195 server.expect(
2196 Expectation::matching(all_of![
2197 request::method_path(
2198 "POST",
2199 "/v1/projects/-/serviceAccounts/test-principal:signBlob"
2200 ),
2201 request::headers(contains((
2202 "authorization",
2203 "Bearer test-impersonated-token"
2204 ))),
2205 ])
2206 .times(2)
2207 .respond_with(json_encoded(json!({
2208 "signedBlob": BASE64_STANDARD.encode("signed_blob"),
2209 }))),
2210 );
2211 let impersonation_url = server
2212 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2213 .to_string();
2214
2215 let user_credential = json!({
2217 "type": "authorized_user",
2218 "client_id": "test-client-id",
2219 "client_secret": "test-client-secret",
2220 "refresh_token": "test-refresh-token",
2221 "token_uri": server.url("/token").to_string()
2222 });
2223 let source_credential =
2224 crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2225 let mut builder_from_source = Builder::from_source_credentials(source_credential)
2226 .with_target_principal("test-principal");
2227 builder_from_source.service_account_impersonation_url = Some(impersonation_url.clone());
2228
2229 let impersonated_credential = json!({
2231 "type": "impersonated_service_account",
2232 "service_account_impersonation_url": impersonation_url,
2233 "source_credentials": user_credential,
2234 });
2235 let builder_from_json = Builder::new(impersonated_credential);
2236
2237 for builder in [builder_from_source, builder_from_json] {
2238 let iam_endpoint = server.url("").to_string().trim_end_matches('/').to_string();
2239 let signer = builder
2240 .maybe_iam_endpoint_override(Some(iam_endpoint))
2241 .without_access_boundary()
2242 .build_signer()?;
2243
2244 let client_email = signer.client_email().await?;
2245 assert_eq!(client_email, "test-principal");
2246
2247 let result = signer.sign(b"test").await?;
2248 assert_eq!(result.as_ref(), b"signed_blob");
2249 }
2250
2251 Ok(())
2252 }
2253
2254 #[tokio::test]
2255 #[parallel]
2256 async fn test_impersonated_sa_signer() -> TestResult {
2257 let service_account = json!({
2258 "type": "service_account",
2259 "client_email": "test-client-email",
2260 "private_key_id": "test-private-key-id",
2261 "private_key": Value::from(PKCS8_PK.clone()),
2262 "project_id": "test-project-id",
2263 });
2264 let impersonated_credential = json!({
2265 "type": "impersonated_service_account",
2266 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2267 "source_credentials": service_account.clone(),
2268 });
2269
2270 let signer = Builder::new(impersonated_credential).build_signer()?;
2271
2272 let client_email = signer.client_email().await?;
2273 assert_eq!(client_email, "test-principal");
2274
2275 let result = signer.sign(b"test").await?;
2276
2277 let service_account_key = serde_json::from_value::<ServiceAccountKey>(service_account)?;
2278 let inner_signer = service_account_key.signer().unwrap();
2279 let inner_result = inner_signer.sign(b"test")?;
2280 assert_eq!(result.as_ref(), inner_result);
2281
2282 Ok(())
2283 }
2284
2285 #[tokio::test]
2286 #[parallel]
2287 async fn test_impersonated_signer_with_invalid_email() -> TestResult {
2288 let impersonated_credential = json!({
2289 "type": "impersonated_service_account",
2290 "service_account_impersonation_url": "http://example.com/test-principal:generateIdToken",
2291 "source_credentials": json!({
2292 "type": "service_account",
2293 "client_email": "test-client-email",
2294 "private_key_id": "test-private-key-id",
2295 "private_key": "test-private-key",
2296 "project_id": "test-project-id",
2297 }),
2298 });
2299
2300 let error = Builder::new(impersonated_credential)
2301 .build_signer()
2302 .unwrap_err();
2303
2304 assert!(error.is_parsing());
2305 assert!(
2306 error
2307 .to_string()
2308 .contains("invalid service account impersonation URL"),
2309 "error: {}",
2310 error
2311 );
2312
2313 Ok(())
2314 }
2315
2316 #[tokio::test]
2317 #[parallel]
2318 #[cfg(google_cloud_unstable_trusted_boundaries)]
2319 async fn e2e_access_boundary() -> TestResult {
2320 use crate::credentials::tests::{get_access_boundary_from_headers, get_token_from_headers};
2321 let server = Server::run();
2322 server.expect(
2323 Expectation::matching(request::method_path("POST", "/token"))
2324 .times(2..)
2325 .respond_with(json_encoded(json!({
2326 "access_token": "test-user-account-token",
2327 "expires_in": 3600,
2328 "token_type": "Bearer",
2329 }))),
2330 );
2331 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2332 .format(&time::format_description::well_known::Rfc3339)
2333 .unwrap();
2334 server.expect(
2335 Expectation::matching(request::method_path(
2336 "POST",
2337 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2338 ))
2339 .times(2)
2340 .respond_with(json_encoded(json!({
2341 "accessToken": "test-impersonated-token",
2342 "expireTime": expire_time
2343 }))),
2344 );
2345
2346 server.expect(
2347 Expectation::matching(all_of![
2348 request::method_path(
2349 "GET",
2350 "/v1/projects/-/serviceAccounts/test-principal/allowedLocations"
2351 ),
2352 request::headers(contains((
2353 "authorization",
2354 "Bearer test-impersonated-token"
2355 ))),
2356 ])
2357 .times(2)
2358 .respond_with(json_encoded(json!({
2359 "locations": ["us-central1", "us-east1"],
2360 "encodedLocations": "0x1234"
2361 }))),
2362 );
2363 let impersonation_url = server
2364 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2365 .to_string();
2366
2367 let user_credential = json!({
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 let source_credential =
2376 crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2377 let mut builder_from_source = Builder::from_source_credentials(source_credential)
2378 .with_target_principal("test-principal");
2379 builder_from_source.service_account_impersonation_url = Some(impersonation_url.clone());
2380
2381 let impersonated_credential = json!({
2383 "type": "impersonated_service_account",
2384 "service_account_impersonation_url": impersonation_url,
2385 "source_credentials": user_credential,
2386 });
2387 let builder_from_json = Builder::new(impersonated_credential);
2388
2389 for builder in [builder_from_source, builder_from_json] {
2390 let iam_endpoint = server.url("").to_string().trim_end_matches('/').to_string();
2391 let creds = builder
2392 .maybe_iam_endpoint_override(Some(iam_endpoint))
2393 .build_credentials()?;
2394
2395 creds.wait_for_boundary().await;
2397
2398 let headers = creds.headers(Extensions::new()).await?;
2399 let token = get_token_from_headers(headers.clone());
2400 let access_boundary = get_access_boundary_from_headers(headers);
2401 assert!(token.is_some(), "should have some token: {token:?}");
2402 assert_eq!(
2403 access_boundary.as_deref(),
2404 Some("0x1234"),
2405 "should be 0x1234 but found: {access_boundary:?}"
2406 );
2407 }
2408
2409 Ok(())
2410 }
2411}