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