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