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