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, build_cacheable_headers, 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 gax::backoff_policy::BackoffPolicyArg;
111use gax::retry_policy::RetryPolicyArg;
112use 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 build_cacheable_headers(&token, &self.quota_project_id)
710 }
711}
712
713#[async_trait::async_trait]
714impl<T> AccessTokenCredentialsProvider for ImpersonatedServiceAccount<T>
715where
716 T: CachedTokenProvider,
717{
718 async fn access_token(&self) -> Result<AccessToken> {
719 let token = self.token_provider.token(Extensions::new()).await?;
720 token.into()
721 }
722}
723
724struct ImpersonatedTokenProvider {
725 source_credentials: Credentials,
726 service_account_impersonation_url: String,
727 delegates: Option<Vec<String>>,
728 scopes: Vec<String>,
729 lifetime: Duration,
730}
731
732impl Debug for ImpersonatedTokenProvider {
733 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734 f.debug_struct("ImpersonatedTokenProvider")
735 .field("source_credentials", &self.source_credentials)
736 .field(
737 "service_account_impersonation_url",
738 &self.service_account_impersonation_url,
739 )
740 .field("delegates", &self.delegates)
741 .field("scopes", &self.scopes)
742 .field("lifetime", &self.lifetime)
743 .finish()
744 }
745}
746
747#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
748struct GenerateAccessTokenRequest {
749 #[serde(skip_serializing_if = "Option::is_none")]
750 delegates: Option<Vec<String>>,
751 scope: Vec<String>,
752 lifetime: String,
753}
754
755pub(crate) async fn generate_access_token(
756 source_headers: HeaderMap,
757 delegates: Option<Vec<String>>,
758 scopes: Vec<String>,
759 lifetime: Duration,
760 service_account_impersonation_url: &str,
761) -> Result<Token> {
762 let client = Client::new();
763 let body = GenerateAccessTokenRequest {
764 delegates,
765 scope: scopes,
766 lifetime: format!("{}s", lifetime.as_secs_f64()),
767 };
768
769 let response = client
770 .post(service_account_impersonation_url)
771 .header("Content-Type", "application/json")
772 .header(
773 headers_util::X_GOOG_API_CLIENT,
774 metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
775 )
776 .headers(source_headers)
777 .json(&body)
778 .send()
779 .await
780 .map_err(|e| errors::from_http_error(e, MSG))?;
781
782 if !response.status().is_success() {
783 let err = errors::from_http_response(response, MSG).await;
784 return Err(err);
785 }
786
787 let token_response = response
788 .json::<GenerateAccessTokenResponse>()
789 .await
790 .map_err(|e| {
791 let retryable = !e.is_decode();
792 CredentialsError::from_source(retryable, e)
793 })?;
794
795 let parsed_dt = OffsetDateTime::parse(
796 &token_response.expire_time,
797 &time::format_description::well_known::Rfc3339,
798 )
799 .map_err(errors::non_retryable)?;
800
801 let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
802 let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
803
804 let token = Token {
805 token: token_response.access_token,
806 token_type: "Bearer".to_string(),
807 expires_at: Some(expires_at),
808 metadata: None,
809 };
810 Ok(token)
811}
812
813#[async_trait]
814impl TokenProvider for ImpersonatedTokenProvider {
815 async fn token(&self) -> Result<Token> {
816 let source_headers = self.source_credentials.headers(Extensions::new()).await?;
817 let source_headers = match source_headers {
818 CacheableResource::New { data, .. } => data,
819 CacheableResource::NotModified => {
820 unreachable!("requested source credentials without a caching etag")
821 }
822 };
823 generate_access_token(
824 source_headers,
825 self.delegates.clone(),
826 self.scopes.clone(),
827 self.lifetime,
828 &self.service_account_impersonation_url,
829 )
830 .await
831 }
832}
833
834#[derive(serde::Deserialize)]
835struct GenerateAccessTokenResponse {
836 #[serde(rename = "accessToken")]
837 access_token: String,
838 #[serde(rename = "expireTime")]
839 expire_time: String,
840}
841
842#[cfg(test)]
843mod tests {
844 use super::*;
845 use crate::credentials::tests::{
846 find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
847 get_mock_retry_throttler,
848 };
849 use crate::errors::CredentialsError;
850 use httptest::cycle;
851 use httptest::{Expectation, Server, matchers::*, responders::*};
852 use serde_json::json;
853
854 type TestResult = anyhow::Result<()>;
855
856 #[tokio::test]
857 async fn test_generate_access_token_success() -> TestResult {
858 let server = Server::run();
859 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
860 .format(&time::format_description::well_known::Rfc3339)
861 .unwrap();
862 server.expect(
863 Expectation::matching(all_of![
864 request::method_path(
865 "POST",
866 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
867 ),
868 request::headers(contains(("authorization", "Bearer test-token"))),
869 ])
870 .respond_with(json_encoded(json!({
871 "accessToken": "test-impersonated-token",
872 "expireTime": expire_time
873 }))),
874 );
875
876 let mut headers = HeaderMap::new();
877 headers.insert("authorization", "Bearer test-token".parse().unwrap());
878 let token = generate_access_token(
879 headers,
880 None,
881 vec!["scope".to_string()],
882 DEFAULT_LIFETIME,
883 &server
884 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
885 .to_string(),
886 )
887 .await?;
888
889 assert_eq!(token.token, "test-impersonated-token");
890 Ok(())
891 }
892
893 #[tokio::test]
894 async fn test_generate_access_token_403() -> TestResult {
895 let server = Server::run();
896 server.expect(
897 Expectation::matching(all_of![
898 request::method_path(
899 "POST",
900 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
901 ),
902 request::headers(contains(("authorization", "Bearer test-token"))),
903 ])
904 .respond_with(status_code(403)),
905 );
906
907 let mut headers = HeaderMap::new();
908 headers.insert("authorization", "Bearer test-token".parse().unwrap());
909 let err = generate_access_token(
910 headers,
911 None,
912 vec!["scope".to_string()],
913 DEFAULT_LIFETIME,
914 &server
915 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
916 .to_string(),
917 )
918 .await
919 .unwrap_err();
920
921 assert!(!err.is_transient());
922 Ok(())
923 }
924
925 #[tokio::test]
926 async fn test_generate_access_token_no_auth_header() -> TestResult {
927 let server = Server::run();
928 server.expect(
929 Expectation::matching(request::method_path(
930 "POST",
931 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
932 ))
933 .respond_with(status_code(401)),
934 );
935
936 let err = generate_access_token(
937 HeaderMap::new(),
938 None,
939 vec!["scope".to_string()],
940 DEFAULT_LIFETIME,
941 &server
942 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
943 .to_string(),
944 )
945 .await
946 .unwrap_err();
947
948 assert!(!err.is_transient());
949 Ok(())
950 }
951
952 #[tokio::test]
953 async fn test_impersonated_service_account() -> TestResult {
954 let server = Server::run();
955 server.expect(
956 Expectation::matching(request::method_path("POST", "/token")).respond_with(
957 json_encoded(json!({
958 "access_token": "test-user-account-token",
959 "expires_in": 3600,
960 "token_type": "Bearer",
961 })),
962 ),
963 );
964 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
965 .format(&time::format_description::well_known::Rfc3339)
966 .unwrap();
967 server.expect(
968 Expectation::matching(all_of![
969 request::method_path(
970 "POST",
971 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
972 ),
973 request::headers(contains((
974 "authorization",
975 "Bearer test-user-account-token"
976 ))),
977 request::body(json_decoded(eq(json!({
978 "scope": ["scope1", "scope2"],
979 "lifetime": "3600s"
980 }))))
981 ])
982 .respond_with(json_encoded(json!({
983 "accessToken": "test-impersonated-token",
984 "expireTime": expire_time
985 }))),
986 );
987
988 let impersonated_credential = json!({
989 "type": "impersonated_service_account",
990 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
991 "source_credentials": {
992 "type": "authorized_user",
993 "client_id": "test-client-id",
994 "client_secret": "test-client-secret",
995 "refresh_token": "test-refresh-token",
996 "token_uri": server.url("/token").to_string()
997 }
998 });
999 let (token_provider, _) = Builder::new(impersonated_credential)
1000 .with_scopes(vec!["scope1", "scope2"])
1001 .build_components()?;
1002
1003 let token = token_provider.token().await?;
1004 assert_eq!(token.token, "test-impersonated-token");
1005 assert_eq!(token.token_type, "Bearer");
1006
1007 Ok(())
1008 }
1009
1010 #[tokio::test]
1011 async fn test_impersonated_service_account_default_scope() -> TestResult {
1012 let server = Server::run();
1013 server.expect(
1014 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1015 json_encoded(json!({
1016 "access_token": "test-user-account-token",
1017 "expires_in": 3600,
1018 "token_type": "Bearer",
1019 })),
1020 ),
1021 );
1022 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1023 .format(&time::format_description::well_known::Rfc3339)
1024 .unwrap();
1025 server.expect(
1026 Expectation::matching(all_of![
1027 request::method_path(
1028 "POST",
1029 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1030 ),
1031 request::headers(contains((
1032 "authorization",
1033 "Bearer test-user-account-token"
1034 ))),
1035 request::body(json_decoded(eq(json!({
1036 "scope": [DEFAULT_SCOPE],
1037 "lifetime": "3600s"
1038 }))))
1039 ])
1040 .respond_with(json_encoded(json!({
1041 "accessToken": "test-impersonated-token",
1042 "expireTime": expire_time
1043 }))),
1044 );
1045
1046 let impersonated_credential = json!({
1047 "type": "impersonated_service_account",
1048 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1049 "source_credentials": {
1050 "type": "authorized_user",
1051 "client_id": "test-client-id",
1052 "client_secret": "test-client-secret",
1053 "refresh_token": "test-refresh-token",
1054 "token_uri": server.url("/token").to_string()
1055 }
1056 });
1057 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1058
1059 let token = token_provider.token().await?;
1060 assert_eq!(token.token, "test-impersonated-token");
1061 assert_eq!(token.token_type, "Bearer");
1062
1063 Ok(())
1064 }
1065
1066 #[tokio::test]
1067 async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
1068 let server = Server::run();
1069 server.expect(
1070 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1071 json_encoded(json!({
1072 "access_token": "test-user-account-token",
1073 "expires_in": 3600,
1074 "token_type": "Bearer",
1075 })),
1076 ),
1077 );
1078 let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
1079 .format(&time::format_description::well_known::Rfc3339)
1080 .unwrap();
1081 server.expect(
1082 Expectation::matching(all_of![
1083 request::method_path(
1084 "POST",
1085 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1086 ),
1087 request::headers(contains((
1088 "authorization",
1089 "Bearer test-user-account-token"
1090 ))),
1091 request::body(json_decoded(eq(json!({
1092 "scope": ["scope1", "scope2"],
1093 "lifetime": "3.5s"
1094 }))))
1095 ])
1096 .respond_with(json_encoded(json!({
1097 "accessToken": "test-impersonated-token",
1098 "expireTime": expire_time
1099 }))),
1100 );
1101
1102 let impersonated_credential = json!({
1103 "type": "impersonated_service_account",
1104 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1105 "source_credentials": {
1106 "type": "authorized_user",
1107 "client_id": "test-client-id",
1108 "client_secret": "test-client-secret",
1109 "refresh_token": "test-refresh-token",
1110 "token_uri": server.url("/token").to_string()
1111 }
1112 });
1113 let (token_provider, _) = Builder::new(impersonated_credential)
1114 .with_scopes(vec!["scope1", "scope2"])
1115 .with_lifetime(Duration::from_secs_f32(3.5))
1116 .build_components()?;
1117
1118 let token = token_provider.token().await?;
1119 assert_eq!(token.token, "test-impersonated-token");
1120
1121 Ok(())
1122 }
1123
1124 #[tokio::test]
1125 async fn test_with_delegates() -> TestResult {
1126 let server = Server::run();
1127 server.expect(
1128 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1129 json_encoded(json!({
1130 "access_token": "test-user-account-token",
1131 "expires_in": 3600,
1132 "token_type": "Bearer",
1133 })),
1134 ),
1135 );
1136 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1137 .format(&time::format_description::well_known::Rfc3339)
1138 .unwrap();
1139 server.expect(
1140 Expectation::matching(all_of![
1141 request::method_path(
1142 "POST",
1143 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1144 ),
1145 request::headers(contains((
1146 "authorization",
1147 "Bearer test-user-account-token"
1148 ))),
1149 request::body(json_decoded(eq(json!({
1150 "scope": [DEFAULT_SCOPE],
1151 "lifetime": "3600s",
1152 "delegates": ["delegate1", "delegate2"]
1153 }))))
1154 ])
1155 .respond_with(json_encoded(json!({
1156 "accessToken": "test-impersonated-token",
1157 "expireTime": expire_time
1158 }))),
1159 );
1160
1161 let impersonated_credential = json!({
1162 "type": "impersonated_service_account",
1163 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1164 "source_credentials": {
1165 "type": "authorized_user",
1166 "client_id": "test-client-id",
1167 "client_secret": "test-client-secret",
1168 "refresh_token": "test-refresh-token",
1169 "token_uri": server.url("/token").to_string()
1170 }
1171 });
1172 let (token_provider, _) = Builder::new(impersonated_credential)
1173 .with_delegates(vec!["delegate1", "delegate2"])
1174 .build_components()?;
1175
1176 let token = token_provider.token().await?;
1177 assert_eq!(token.token, "test-impersonated-token");
1178 assert_eq!(token.token_type, "Bearer");
1179
1180 Ok(())
1181 }
1182
1183 #[tokio::test]
1184 async fn test_impersonated_service_account_fail() -> TestResult {
1185 let server = Server::run();
1186 server.expect(
1187 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1188 json_encoded(json!({
1189 "access_token": "test-user-account-token",
1190 "expires_in": 3600,
1191 "token_type": "Bearer",
1192 })),
1193 ),
1194 );
1195 server.expect(
1196 Expectation::matching(request::method_path(
1197 "POST",
1198 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1199 ))
1200 .respond_with(status_code(500)),
1201 );
1202
1203 let impersonated_credential = json!({
1204 "type": "impersonated_service_account",
1205 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1206 "source_credentials": {
1207 "type": "authorized_user",
1208 "client_id": "test-client-id",
1209 "client_secret": "test-client-secret",
1210 "refresh_token": "test-refresh-token",
1211 "token_uri": server.url("/token").to_string()
1212 }
1213 });
1214 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1215
1216 let err = token_provider.token().await.unwrap_err();
1217 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1218 assert!(original_err.is_transient());
1219
1220 Ok(())
1221 }
1222
1223 #[tokio::test]
1224 async fn debug_token_provider() {
1225 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1226 "type": "authorized_user",
1227 "client_id": "test-client-id",
1228 "client_secret": "test-client-secret",
1229 "refresh_token": "test-refresh-token"
1230 }))
1231 .build()
1232 .unwrap();
1233
1234 let expected = ImpersonatedTokenProvider {
1235 source_credentials,
1236 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1237 delegates: Some(vec!["delegate1".to_string()]),
1238 scopes: vec!["scope1".to_string()],
1239 lifetime: Duration::from_secs(3600),
1240 };
1241 let fmt = format!("{expected:?}");
1242 assert!(fmt.contains("UserCredentials"), "{fmt}");
1243 assert!(fmt.contains("test-client-id"), "{fmt}");
1244 assert!(!fmt.contains("test-client-secret"), "{fmt}");
1245 assert!(!fmt.contains("test-refresh-token"), "{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());
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());
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());
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());
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}