google_cloud_auth/credentials/
impersonated.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! [Impersonated service account] credentials.
16//!
17//! When the principal you are using doesn't have the permissions you need to
18//! accomplish your task, or you want to use a service account in a development
19//! environment, you can use service account impersonation. The typical principals
20//! used to impersonate a service account are [User Account] or another [Service Account].
21//!
22//! The principal that is trying to impersonate a target service account should have
23//! [Service Account Token Creator Role] on the target service account.
24//!
25//! ## Example: Creating credentials from a JSON object
26//!
27//! ```
28//! # use google_cloud_auth::credentials::impersonated;
29//! # use serde_json::json;
30//! # use std::time::Duration;
31//! # use http::Extensions;
32//! #
33//! # tokio_test::block_on(async {
34//! let source_credentials = json!({
35//!     "type": "authorized_user",
36//!     "client_id": "test-client-id",
37//!     "client_secret": "test-client-secret",
38//!     "refresh_token": "test-refresh-token"
39//! });
40//!
41//! let impersonated_credential = json!({
42//!     "type": "impersonated_service_account",
43//!     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
44//!     "source_credentials": source_credentials,
45//! });
46//!
47//! let credentials = impersonated::Builder::new(impersonated_credential)
48//!     .with_lifetime(Duration::from_secs(500))
49//!     .build()?;
50//! let headers = credentials.headers(Extensions::new()).await?;
51//! println!("Headers: {headers:?}");
52//! # Ok::<(), anyhow::Error>(())
53//! # });
54//! ```
55//!
56//! ## Example: Creating credentials with custom retry behavior
57//!
58//! ```
59//! # use google_cloud_auth::credentials::impersonated;
60//! # use serde_json::json;
61//! # use std::time::Duration;
62//! # use http::Extensions;
63//! # tokio_test::block_on(async {
64//! use gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
65//! use gax::exponential_backoff::ExponentialBackoff;
66//! # let source_credentials = json!({
67//! #     "type": "authorized_user",
68//! #     "client_id": "test-client-id",
69//! #     "client_secret": "test-client-secret",
70//! #     "refresh_token": "test-refresh-token"
71//! # });
72//! #
73//! # let impersonated_credential = json!({
74//! #     "type": "impersonated_service_account",
75//! #     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
76//! #     "source_credentials": source_credentials,
77//! # });
78//! let backoff = ExponentialBackoff::default();
79//! let credentials = impersonated::Builder::new(impersonated_credential)
80//!     .with_retry_policy(AlwaysRetry.with_attempt_limit(3))
81//!     .with_backoff_policy(backoff)
82//!     .build()?;
83//! let headers = credentials.headers(Extensions::new()).await?;
84//! println!("Headers: {headers:?}");
85//! # Ok::<(), anyhow::Error>(())
86//! # });
87//! ```
88//!
89//! [Impersonated service account]: https://cloud.google.com/docs/authentication/use-service-account-impersonation
90//! [User Account]: https://cloud.google.com/docs/authentication#user-accounts
91//! [Service Account]: https://cloud.google.com/iam/docs/service-account-overview
92//! [Service Account Token Creator Role]: https://cloud.google.com/docs/authentication/use-service-account-impersonation#required-roles
93
94use 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
130/// A builder for constructing Impersonated Service Account [Credentials] instance.
131///
132/// # Example
133/// ```
134/// # use google_cloud_auth::credentials::impersonated::Builder;
135/// # tokio_test::block_on(async {
136/// let impersonated_credential = serde_json::json!({
137///     "type": "impersonated_service_account",
138///     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
139///     "source_credentials": {
140///         "type": "authorized_user",
141///         "client_id": "test-client-id",
142///         "client_secret": "test-client-secret",
143///         "refresh_token": "test-refresh-token"
144///     }
145/// });
146/// let credentials = Builder::new(impersonated_credential).build();
147/// # });
148/// ```
149pub 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    /// Creates a new builder using `impersonated_service_account` JSON value.
161    ///
162    /// The `impersonated_service_account` JSON is typically generated using
163    /// a [gcloud command] for [application default login].
164    ///
165    /// [gcloud command]: https://cloud.google.com/docs/authentication/use-service-account-impersonation#adc
166    /// [application default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
167    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    /// Creates a new builder with a source credentials object.
180    ///
181    /// # Example
182    /// ```
183    /// # use google_cloud_auth::credentials::impersonated;
184    /// # use google_cloud_auth::credentials::user_account;
185    /// # use serde_json::json;
186    /// #
187    /// # tokio_test::block_on(async {
188    /// let source_credentials = user_account::Builder::new(json!({ /* add details here */ })).build()?;
189    ///
190    /// let creds = impersonated::Builder::from_source_credentials(source_credentials)
191    ///     .with_target_principal("test-principal")
192    ///     .build()?;
193    /// # Ok::<(), anyhow::Error>(())
194    /// # });
195    /// ```
196    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    /// Sets the target principal. This is required when using `from_source_credentials`.
209    /// Target principal is the email of the service account to impersonate.
210    ///
211    /// # Example
212    /// ```
213    /// # use google_cloud_auth::credentials::impersonated;
214    /// # use serde_json::json;
215    /// #
216    /// # tokio_test::block_on(async {
217    /// let impersonated_credential = json!({ /* add details here */ });
218    ///
219    /// let creds = impersonated::Builder::new(impersonated_credential.into())
220    ///     .with_target_principal("test-principal")
221    ///     .build();
222    /// # });
223    /// ```
224    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    /// Sets the chain of delegates.
233    ///
234    /// # Example
235    /// ```
236    /// # use google_cloud_auth::credentials::impersonated;
237    /// # use serde_json::json;
238    /// #
239    /// # tokio_test::block_on(async {
240    /// let impersonated_credential = json!({ /* add details here */ });
241    ///
242    /// let creds = impersonated::Builder::new(impersonated_credential.into())
243    ///     .with_delegates(["delegate1", "delegate2"])
244    ///     .build();
245    /// # });
246    /// ```
247    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    /// Sets the [scopes] for these credentials.
257    /// By default `https://www.googleapis.com/auth/cloud-platform` scope is used.
258    ///
259    /// # Example
260    /// ```
261    /// # use google_cloud_auth::credentials::impersonated;
262    /// # use serde_json::json;
263    /// #
264    /// # tokio_test::block_on(async {
265    /// let impersonated_credential = json!({ /* add details here */ });
266    ///
267    /// let creds = impersonated::Builder::new(impersonated_credential.into())
268    ///     .with_scopes(["https://www.googleapis.com/auth/pubsub"])
269    ///     .build();
270    /// # });
271    /// ```
272    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
273    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
274    where
275        I: IntoIterator<Item = S>,
276        S: Into<String>,
277    {
278        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
279        self
280    }
281
282    /// Sets the [quota project] for these credentials.
283    ///
284    /// For some services, you can use an account in
285    /// one project for authentication and authorization, and charge
286    /// the usage to a different project. This requires that the
287    /// target service account has `serviceusage.services.use`
288    /// permissions on the quota project.
289    ///
290    /// Any value set here overrides a `quota_project_id` value from the
291    /// input `impersonated_service_account` JSON.
292    ///
293    /// # Example
294    /// ```
295    /// # use google_cloud_auth::credentials::impersonated;
296    /// # use serde_json::json;
297    /// #
298    /// # tokio_test::block_on(async {
299    /// let impersonated_credential = json!({ /* add details here */ });
300    ///
301    /// let creds = impersonated::Builder::new(impersonated_credential.into())
302    ///     .with_quota_project_id("my-project")
303    ///     .build();
304    /// # });
305    /// ```
306    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
307    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
308        self.quota_project_id = Some(quota_project_id.into());
309        self
310    }
311
312    /// Sets the lifetime for the impersonated credentials.
313    ///
314    /// # Example
315    /// ```
316    /// # use google_cloud_auth::credentials::impersonated;
317    /// # use serde_json::json;
318    /// # use std::time::Duration;
319    /// #
320    /// # tokio_test::block_on(async {
321    /// let impersonated_credential = json!({ /* add details here */ });
322    ///
323    /// let creds = impersonated::Builder::new(impersonated_credential.into())
324    ///     .with_lifetime(Duration::from_secs(500))
325    ///     .build();
326    /// # });
327    /// ```
328    pub fn with_lifetime(mut self, lifetime: Duration) -> Self {
329        self.lifetime = Some(lifetime);
330        self
331    }
332
333    /// Configure the retry policy for fetching tokens.
334    ///
335    /// The retry policy controls how to handle retries, and sets limits on
336    /// the number of attempts or the total time spent retrying.
337    ///
338    /// ```
339    /// # use google_cloud_auth::credentials::impersonated::Builder;
340    /// # use serde_json::json;
341    /// # tokio_test::block_on(async {
342    /// use gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
343    /// let impersonated_credential = json!({ /* add details here */ });
344    /// let credentials = Builder::new(impersonated_credential.into())
345    ///     .with_retry_policy(AlwaysRetry.with_attempt_limit(3))
346    ///     .build();
347    /// # });
348    /// ```
349    pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
350        self.retry_builder = self.retry_builder.with_retry_policy(v.into());
351        self
352    }
353
354    /// Configure the retry backoff policy.
355    ///
356    /// The backoff policy controls how long to wait in between retry attempts.
357    ///
358    /// ```
359    /// # use google_cloud_auth::credentials::impersonated::Builder;
360    /// # use serde_json::json;
361    /// # use std::time::Duration;
362    /// # tokio_test::block_on(async {
363    /// use gax::exponential_backoff::ExponentialBackoff;
364    /// let policy = ExponentialBackoff::default();
365    /// let impersonated_credential = json!({ /* add details here */ });
366    /// let credentials = Builder::new(impersonated_credential.into())
367    ///     .with_backoff_policy(policy)
368    ///     .build();
369    /// # });
370    /// ```
371    pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
372        self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
373        self
374    }
375
376    /// Configure the retry throttler.
377    ///
378    /// Advanced applications may want to configure a retry throttler to
379    /// [Address Cascading Failures] and when [Handling Overload] conditions.
380    /// The authentication library throttles its retry loop, using a policy to
381    /// control the throttling algorithm. Use this method to fine tune or
382    /// customize the default retry throttler.
383    ///
384    /// [Handling Overload]: https://sre.google/sre-book/handling-overload/
385    /// [Address Cascading Failures]: https://sre.google/sre-book/addressing-cascading-failures/
386    ///
387    /// ```
388    /// # use google_cloud_auth::credentials::impersonated::Builder;
389    /// # use serde_json::json;
390    /// # tokio_test::block_on(async {
391    /// use gax::retry_throttler::AdaptiveThrottler;
392    /// let impersonated_credential = json!({ /* add details here */ });
393    /// let credentials = Builder::new(impersonated_credential.into())
394    ///     .with_retry_throttler(AdaptiveThrottler::default())
395    ///     .build();
396    /// # });
397    /// ```
398    pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
399        self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
400        self
401    }
402
403    /// Returns a [Credentials] instance with the configured settings.
404    ///
405    /// # Errors
406    ///
407    /// Returns a [BuilderError] for one of the following cases:
408    /// - If the `impersonated_service_account` provided to [`Builder::new`] cannot
409    ///   be successfully deserialized into the expected format. This typically happens
410    ///   if the JSON value is malformed or missing required fields. For more information,
411    ///   on how to generate `impersonated_service_account` json, consult the relevant
412    ///   section in the [application-default credentials] guide.
413    /// - If the `impersonated_service_account` provided to [`Builder::new`] has a
414    ///   `source_credentials` of `impersonated_service_account` type.
415    /// - If `service_account_impersonation_url` is not provided after initializing
416    ///   the builder with [`Builder::from_source_credentials`].
417    ///
418    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
419    pub fn build(self) -> BuildResult<Credentials> {
420        let (token_provider, quota_project_id) = self.build_components()?;
421        Ok(Credentials {
422            inner: Arc::new(ImpersonatedServiceAccount {
423                token_provider: TokenCache::new(token_provider),
424                quota_project_id,
425            }),
426        })
427    }
428
429    fn build_components(
430        self,
431    ) -> BuildResult<(
432        TokenProviderWithRetry<ImpersonatedTokenProvider>,
433        Option<String>,
434    )> {
435        let (source_credentials, service_account_impersonation_url, delegates, quota_project_id) =
436            match self.source {
437                BuilderSource::FromJson(json) => {
438                    let config = serde_json::from_value::<ImpersonatedConfig>(json)
439                        .map_err(BuilderError::parsing)?;
440
441                    let source_credential_type =
442                        extract_credential_type(&config.source_credentials)?;
443                    if source_credential_type == "impersonated_service_account" {
444                        return Err(BuilderError::parsing(
445                            "source credential of type `impersonated_service_account` is not supported. \
446                        Use the `delegates` field to specify a delegation chain.",
447                        ));
448                    }
449
450                    // Do not pass along scopes and quota project to the source credentials.
451                    // It is not necessary that the source and target credentials have same permissions on
452                    // the quota project and they typically need different scopes.
453                    // If user does want some specific scopes or quota, they can build using the
454                    // from_source_credentials method.
455                    let source_credentials =
456                        build_credentials(Some(config.source_credentials), None, None)?;
457
458                    (
459                        source_credentials,
460                        config.service_account_impersonation_url,
461                        config.delegates,
462                        config.quota_project_id,
463                    )
464                }
465                BuilderSource::FromCredentials(source_credentials) => {
466                    let url = self.service_account_impersonation_url.ok_or_else(|| {
467                    BuilderError::parsing("`service_account_impersonation_url` is required when building from source credentials")
468                })?;
469                    (source_credentials, url, None, None)
470                }
471            };
472
473        let scopes = self
474            .scopes
475            .unwrap_or_else(|| vec![DEFAULT_SCOPE.to_string()]);
476
477        let quota_project_id = self.quota_project_id.or(quota_project_id);
478        let delegates = self.delegates.or(delegates);
479
480        let token_provider = ImpersonatedTokenProvider {
481            source_credentials,
482            service_account_impersonation_url,
483            delegates,
484            scopes,
485            lifetime: self.lifetime.unwrap_or(DEFAULT_LIFETIME),
486        };
487        let token_provider = self.retry_builder.build(token_provider);
488        Ok((token_provider, quota_project_id))
489    }
490}
491
492#[derive(serde::Deserialize, Debug, PartialEq)]
493struct ImpersonatedConfig {
494    service_account_impersonation_url: String,
495    source_credentials: Value,
496    delegates: Option<Vec<String>>,
497    quota_project_id: Option<String>,
498}
499
500#[derive(Debug)]
501struct ImpersonatedServiceAccount<T>
502where
503    T: CachedTokenProvider,
504{
505    token_provider: T,
506    quota_project_id: Option<String>,
507}
508
509#[async_trait::async_trait]
510impl<T> CredentialsProvider for ImpersonatedServiceAccount<T>
511where
512    T: CachedTokenProvider,
513{
514    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
515        let token = self.token_provider.token(extensions).await?;
516        build_cacheable_headers(&token, &self.quota_project_id)
517    }
518}
519
520struct ImpersonatedTokenProvider {
521    source_credentials: Credentials,
522    service_account_impersonation_url: String,
523    delegates: Option<Vec<String>>,
524    scopes: Vec<String>,
525    lifetime: Duration,
526}
527
528impl Debug for ImpersonatedTokenProvider {
529    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530        f.debug_struct("ImpersonatedTokenProvider")
531            .field("source_credentials", &self.source_credentials)
532            .field(
533                "service_account_impersonation_url",
534                &self.service_account_impersonation_url,
535            )
536            .field("delegates", &self.delegates)
537            .field("scopes", &self.scopes)
538            .field("lifetime", &self.lifetime)
539            .finish()
540    }
541}
542
543#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
544struct GenerateAccessTokenRequest {
545    #[serde(skip_serializing_if = "Option::is_none")]
546    delegates: Option<Vec<String>>,
547    scope: Vec<String>,
548    lifetime: String,
549}
550
551pub(crate) async fn generate_access_token(
552    source_headers: HeaderMap,
553    delegates: Option<Vec<String>>,
554    scopes: Vec<String>,
555    lifetime: Duration,
556    service_account_impersonation_url: &str,
557) -> Result<Token> {
558    let client = Client::new();
559    let body = GenerateAccessTokenRequest {
560        delegates,
561        scope: scopes,
562        lifetime: format!("{}s", lifetime.as_secs_f64()),
563    };
564
565    let response = client
566        .post(service_account_impersonation_url)
567        .header("Content-Type", "application/json")
568        .header(
569            headers_util::X_GOOG_API_CLIENT,
570            metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
571        )
572        .headers(source_headers)
573        .json(&body)
574        .send()
575        .await
576        .map_err(|e| errors::from_http_error(e, MSG))?;
577
578    if !response.status().is_success() {
579        let err = errors::from_http_response(response, MSG).await;
580        return Err(err);
581    }
582
583    let token_response = response
584        .json::<GenerateAccessTokenResponse>()
585        .await
586        .map_err(|e| {
587            let retryable = !e.is_decode();
588            CredentialsError::from_source(retryable, e)
589        })?;
590
591    let parsed_dt = OffsetDateTime::parse(
592        &token_response.expire_time,
593        &time::format_description::well_known::Rfc3339,
594    )
595    .map_err(errors::non_retryable)?;
596
597    let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
598    let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
599
600    let token = Token {
601        token: token_response.access_token,
602        token_type: "Bearer".to_string(),
603        expires_at: Some(expires_at),
604        metadata: None,
605    };
606    Ok(token)
607}
608
609#[async_trait]
610impl TokenProvider for ImpersonatedTokenProvider {
611    async fn token(&self) -> Result<Token> {
612        let source_headers = self.source_credentials.headers(Extensions::new()).await?;
613        let source_headers = match source_headers {
614            CacheableResource::New { data, .. } => data,
615            CacheableResource::NotModified => {
616                unreachable!("requested source credentials without a caching etag")
617            }
618        };
619        generate_access_token(
620            source_headers,
621            self.delegates.clone(),
622            self.scopes.clone(),
623            self.lifetime,
624            &self.service_account_impersonation_url,
625        )
626        .await
627    }
628}
629
630#[derive(serde::Deserialize)]
631struct GenerateAccessTokenResponse {
632    #[serde(rename = "accessToken")]
633    access_token: String,
634    #[serde(rename = "expireTime")]
635    expire_time: String,
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use crate::credentials::tests::{
642        get_mock_auth_retry_policy, get_mock_backoff_policy, get_mock_retry_throttler,
643    };
644    use httptest::cycle;
645    use httptest::{Expectation, Server, matchers::*, responders::*};
646    use serde_json::json;
647
648    type TestResult = anyhow::Result<()>;
649
650    #[tokio::test]
651    async fn test_generate_access_token_success() -> TestResult {
652        let server = Server::run();
653        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
654            .format(&time::format_description::well_known::Rfc3339)
655            .unwrap();
656        server.expect(
657            Expectation::matching(all_of![
658                request::method_path(
659                    "POST",
660                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
661                ),
662                request::headers(contains(("authorization", "Bearer test-token"))),
663            ])
664            .respond_with(json_encoded(json!({
665                "accessToken": "test-impersonated-token",
666                "expireTime": expire_time
667            }))),
668        );
669
670        let mut headers = HeaderMap::new();
671        headers.insert("authorization", "Bearer test-token".parse().unwrap());
672        let token = generate_access_token(
673            headers,
674            None,
675            vec!["scope".to_string()],
676            DEFAULT_LIFETIME,
677            &server
678                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
679                .to_string(),
680        )
681        .await?;
682
683        assert_eq!(token.token, "test-impersonated-token");
684        Ok(())
685    }
686
687    #[tokio::test]
688    async fn test_generate_access_token_403() -> TestResult {
689        let server = Server::run();
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(status_code(403)),
699        );
700
701        let mut headers = HeaderMap::new();
702        headers.insert("authorization", "Bearer test-token".parse().unwrap());
703        let err = generate_access_token(
704            headers,
705            None,
706            vec!["scope".to_string()],
707            DEFAULT_LIFETIME,
708            &server
709                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
710                .to_string(),
711        )
712        .await
713        .unwrap_err();
714
715        assert!(!err.is_transient());
716        Ok(())
717    }
718
719    #[tokio::test]
720    async fn test_generate_access_token_no_auth_header() -> TestResult {
721        let server = Server::run();
722        server.expect(
723            Expectation::matching(request::method_path(
724                "POST",
725                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
726            ))
727            .respond_with(status_code(401)),
728        );
729
730        let err = generate_access_token(
731            HeaderMap::new(),
732            None,
733            vec!["scope".to_string()],
734            DEFAULT_LIFETIME,
735            &server
736                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
737                .to_string(),
738        )
739        .await
740        .unwrap_err();
741
742        assert!(!err.is_transient());
743        Ok(())
744    }
745
746    #[tokio::test]
747    async fn test_impersonated_service_account() -> TestResult {
748        let server = Server::run();
749        server.expect(
750            Expectation::matching(request::method_path("POST", "/token")).respond_with(
751                json_encoded(json!({
752                    "access_token": "test-user-account-token",
753                    "expires_in": 3600,
754                    "token_type": "Bearer",
755                })),
756            ),
757        );
758        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
759            .format(&time::format_description::well_known::Rfc3339)
760            .unwrap();
761        server.expect(
762            Expectation::matching(all_of![
763                request::method_path(
764                    "POST",
765                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
766                ),
767                request::headers(contains((
768                    "authorization",
769                    "Bearer test-user-account-token"
770                ))),
771                request::body(json_decoded(eq(json!({
772                    "scope": ["scope1", "scope2"],
773                    "lifetime": "3600s"
774                }))))
775            ])
776            .respond_with(json_encoded(json!({
777                "accessToken": "test-impersonated-token",
778                "expireTime": expire_time
779            }))),
780        );
781
782        let impersonated_credential = json!({
783            "type": "impersonated_service_account",
784            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
785            "source_credentials": {
786                "type": "authorized_user",
787                "client_id": "test-client-id",
788                "client_secret": "test-client-secret",
789                "refresh_token": "test-refresh-token",
790                "token_uri": server.url("/token").to_string()
791            }
792        });
793        let (token_provider, _) = Builder::new(impersonated_credential)
794            .with_scopes(vec!["scope1", "scope2"])
795            .build_components()?;
796
797        let token = token_provider.token().await?;
798        assert_eq!(token.token, "test-impersonated-token");
799        assert_eq!(token.token_type, "Bearer");
800
801        Ok(())
802    }
803
804    #[tokio::test]
805    async fn test_impersonated_service_account_default_scope() -> TestResult {
806        let server = Server::run();
807        server.expect(
808            Expectation::matching(request::method_path("POST", "/token")).respond_with(
809                json_encoded(json!({
810                    "access_token": "test-user-account-token",
811                    "expires_in": 3600,
812                    "token_type": "Bearer",
813                })),
814            ),
815        );
816        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
817            .format(&time::format_description::well_known::Rfc3339)
818            .unwrap();
819        server.expect(
820            Expectation::matching(all_of![
821                request::method_path(
822                    "POST",
823                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
824                ),
825                request::headers(contains((
826                    "authorization",
827                    "Bearer test-user-account-token"
828                ))),
829                request::body(json_decoded(eq(json!({
830                    "scope": [DEFAULT_SCOPE],
831                    "lifetime": "3600s"
832                }))))
833            ])
834            .respond_with(json_encoded(json!({
835                "accessToken": "test-impersonated-token",
836                "expireTime": expire_time
837            }))),
838        );
839
840        let impersonated_credential = json!({
841            "type": "impersonated_service_account",
842            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
843            "source_credentials": {
844                "type": "authorized_user",
845                "client_id": "test-client-id",
846                "client_secret": "test-client-secret",
847                "refresh_token": "test-refresh-token",
848                "token_uri": server.url("/token").to_string()
849            }
850        });
851        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
852
853        let token = token_provider.token().await?;
854        assert_eq!(token.token, "test-impersonated-token");
855        assert_eq!(token.token_type, "Bearer");
856
857        Ok(())
858    }
859
860    #[tokio::test]
861    async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
862        let server = Server::run();
863        server.expect(
864            Expectation::matching(request::method_path("POST", "/token")).respond_with(
865                json_encoded(json!({
866                    "access_token": "test-user-account-token",
867                    "expires_in": 3600,
868                    "token_type": "Bearer",
869                })),
870            ),
871        );
872        let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
873            .format(&time::format_description::well_known::Rfc3339)
874            .unwrap();
875        server.expect(
876            Expectation::matching(all_of![
877                request::method_path(
878                    "POST",
879                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
880                ),
881                request::headers(contains((
882                    "authorization",
883                    "Bearer test-user-account-token"
884                ))),
885                request::body(json_decoded(eq(json!({
886                    "scope": ["scope1", "scope2"],
887                    "lifetime": "3.5s"
888                }))))
889            ])
890            .respond_with(json_encoded(json!({
891                "accessToken": "test-impersonated-token",
892                "expireTime": expire_time
893            }))),
894        );
895
896        let impersonated_credential = json!({
897            "type": "impersonated_service_account",
898            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
899            "source_credentials": {
900                "type": "authorized_user",
901                "client_id": "test-client-id",
902                "client_secret": "test-client-secret",
903                "refresh_token": "test-refresh-token",
904                "token_uri": server.url("/token").to_string()
905            }
906        });
907        let (token_provider, _) = Builder::new(impersonated_credential)
908            .with_scopes(vec!["scope1", "scope2"])
909            .with_lifetime(Duration::from_secs_f32(3.5))
910            .build_components()?;
911
912        let token = token_provider.token().await?;
913        assert_eq!(token.token, "test-impersonated-token");
914
915        Ok(())
916    }
917
918    #[tokio::test]
919    async fn test_with_delegates() -> TestResult {
920        let server = Server::run();
921        server.expect(
922            Expectation::matching(request::method_path("POST", "/token")).respond_with(
923                json_encoded(json!({
924                    "access_token": "test-user-account-token",
925                    "expires_in": 3600,
926                    "token_type": "Bearer",
927                })),
928            ),
929        );
930        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
931            .format(&time::format_description::well_known::Rfc3339)
932            .unwrap();
933        server.expect(
934            Expectation::matching(all_of![
935                request::method_path(
936                    "POST",
937                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
938                ),
939                request::headers(contains((
940                    "authorization",
941                    "Bearer test-user-account-token"
942                ))),
943                request::body(json_decoded(eq(json!({
944                    "scope": [DEFAULT_SCOPE],
945                    "lifetime": "3600s",
946                    "delegates": ["delegate1", "delegate2"]
947                }))))
948            ])
949            .respond_with(json_encoded(json!({
950                "accessToken": "test-impersonated-token",
951                "expireTime": expire_time
952            }))),
953        );
954
955        let impersonated_credential = json!({
956            "type": "impersonated_service_account",
957            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
958            "source_credentials": {
959                "type": "authorized_user",
960                "client_id": "test-client-id",
961                "client_secret": "test-client-secret",
962                "refresh_token": "test-refresh-token",
963                "token_uri": server.url("/token").to_string()
964            }
965        });
966        let (token_provider, _) = Builder::new(impersonated_credential)
967            .with_delegates(vec!["delegate1", "delegate2"])
968            .build_components()?;
969
970        let token = token_provider.token().await?;
971        assert_eq!(token.token, "test-impersonated-token");
972        assert_eq!(token.token_type, "Bearer");
973
974        Ok(())
975    }
976
977    #[tokio::test]
978    async fn test_impersonated_service_account_fail() -> TestResult {
979        let server = Server::run();
980        server.expect(
981            Expectation::matching(request::method_path("POST", "/token")).respond_with(
982                json_encoded(json!({
983                    "access_token": "test-user-account-token",
984                    "expires_in": 3600,
985                    "token_type": "Bearer",
986                })),
987            ),
988        );
989        server.expect(
990            Expectation::matching(request::method_path(
991                "POST",
992                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
993            ))
994            .respond_with(status_code(500)),
995        );
996
997        let impersonated_credential = json!({
998            "type": "impersonated_service_account",
999            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1000            "source_credentials": {
1001                "type": "authorized_user",
1002                "client_id": "test-client-id",
1003                "client_secret": "test-client-secret",
1004                "refresh_token": "test-refresh-token",
1005                "token_uri": server.url("/token").to_string()
1006            }
1007        });
1008        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1009
1010        let err = token_provider.token().await.unwrap_err();
1011        assert!(err.is_transient());
1012
1013        Ok(())
1014    }
1015
1016    #[tokio::test]
1017    async fn debug_token_provider() {
1018        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1019            "type": "authorized_user",
1020            "client_id": "test-client-id",
1021            "client_secret": "test-client-secret",
1022            "refresh_token": "test-refresh-token"
1023        }))
1024        .build()
1025        .unwrap();
1026
1027        let expected = ImpersonatedTokenProvider {
1028            source_credentials,
1029            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1030            delegates: Some(vec!["delegate1".to_string()]),
1031            scopes: vec!["scope1".to_string()],
1032            lifetime: Duration::from_secs(3600),
1033        };
1034        let fmt = format!("{expected:?}");
1035        assert!(fmt.contains("UserCredentials"), "{fmt}");
1036        assert!(fmt.contains("test-client-id"), "{fmt}");
1037        assert!(!fmt.contains("test-client-secret"), "{fmt}");
1038        assert!(!fmt.contains("test-refresh-token"), "{fmt}");
1039        assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1040        assert!(fmt.contains("delegate1"), "{fmt}");
1041        assert!(fmt.contains("scope1"), "{fmt}");
1042        assert!(fmt.contains("3600s"), "{fmt}");
1043    }
1044
1045    #[test]
1046    fn impersonated_config_full_from_json_success() {
1047        let source_credentials_json = json!({
1048            "type": "authorized_user",
1049            "client_id": "test-client-id",
1050            "client_secret": "test-client-secret",
1051            "refresh_token": "test-refresh-token"
1052        });
1053        let json = json!({
1054            "type": "impersonated_service_account",
1055            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1056            "source_credentials": source_credentials_json,
1057            "delegates": ["delegate1"],
1058            "quota_project_id": "test-project-id",
1059        });
1060
1061        let expected = ImpersonatedConfig {
1062            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1063            source_credentials: source_credentials_json,
1064            delegates: Some(vec!["delegate1".to_string()]),
1065            quota_project_id: Some("test-project-id".to_string()),
1066        };
1067        let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1068        assert_eq!(actual, expected);
1069    }
1070
1071    #[test]
1072    fn impersonated_config_partial_from_json_success() {
1073        let source_credentials_json = json!({
1074            "type": "authorized_user",
1075            "client_id": "test-client-id",
1076            "client_secret": "test-client-secret",
1077            "refresh_token": "test-refresh-token"
1078        });
1079        let json = json!({
1080            "type": "impersonated_service_account",
1081            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1082            "source_credentials": source_credentials_json
1083        });
1084
1085        let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1086        assert_eq!(
1087            config.service_account_impersonation_url,
1088            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1089        );
1090        assert_eq!(config.source_credentials, source_credentials_json);
1091        assert_eq!(config.delegates, None);
1092        assert_eq!(config.quota_project_id, None);
1093    }
1094
1095    #[tokio::test]
1096    async fn test_impersonated_service_account_source_fail() -> TestResult {
1097        #[derive(Debug)]
1098        struct MockSourceCredentialsFail;
1099
1100        #[async_trait]
1101        impl CredentialsProvider for MockSourceCredentialsFail {
1102            async fn headers(
1103                &self,
1104                _extensions: Extensions,
1105            ) -> Result<CacheableResource<HeaderMap>> {
1106                Err(errors::non_retryable_from_str("source failed"))
1107            }
1108        }
1109
1110        let source_credentials = Credentials {
1111            inner: Arc::new(MockSourceCredentialsFail),
1112        };
1113
1114        let token_provider = ImpersonatedTokenProvider {
1115            source_credentials,
1116            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1117            delegates: Some(vec!["delegate1".to_string()]),
1118            scopes: vec!["scope1".to_string()],
1119            lifetime: DEFAULT_LIFETIME,
1120        };
1121
1122        let err = token_provider.token().await.unwrap_err();
1123        assert!(err.to_string().contains("source failed"));
1124
1125        Ok(())
1126    }
1127
1128    #[tokio::test]
1129    async fn test_missing_impersonation_url_fail() {
1130        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1131            "type": "authorized_user",
1132            "client_id": "test-client-id",
1133            "client_secret": "test-client-secret",
1134            "refresh_token": "test-refresh-token"
1135        }))
1136        .build()
1137        .unwrap();
1138
1139        let result = Builder::from_source_credentials(source_credentials).build();
1140        assert!(result.is_err());
1141        let err = result.unwrap_err();
1142        assert!(err.is_parsing());
1143        assert!(
1144            err.to_string()
1145                .contains("`service_account_impersonation_url` is required")
1146        );
1147    }
1148
1149    #[tokio::test]
1150    async fn test_nested_impersonated_credentials_fail() {
1151        let nested_impersonated = json!({
1152            "type": "impersonated_service_account",
1153            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1154            "source_credentials": {
1155                "type": "impersonated_service_account",
1156                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1157                "source_credentials": {
1158                    "type": "authorized_user",
1159                    "client_id": "test-client-id",
1160                    "client_secret": "test-client-secret",
1161                    "refresh_token": "test-refresh-token"
1162                }
1163            }
1164        });
1165
1166        let result = Builder::new(nested_impersonated).build();
1167        assert!(result.is_err());
1168        let err = result.unwrap_err();
1169        assert!(err.is_parsing());
1170        assert!(
1171            err.to_string().contains(
1172                "source credential of type `impersonated_service_account` is not supported"
1173            )
1174        );
1175    }
1176
1177    #[tokio::test]
1178    async fn test_malformed_impersonated_credentials_fail() {
1179        let malformed_impersonated = json!({
1180            "type": "impersonated_service_account",
1181            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1182        });
1183
1184        let result = Builder::new(malformed_impersonated).build();
1185        assert!(result.is_err());
1186        let err = result.unwrap_err();
1187        assert!(err.is_parsing());
1188        assert!(
1189            err.to_string()
1190                .contains("missing field `source_credentials`")
1191        );
1192    }
1193
1194    #[tokio::test]
1195    async fn test_invalid_source_credential_type_fail() {
1196        let invalid_source = json!({
1197            "type": "impersonated_service_account",
1198            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1199            "source_credentials": {
1200                "type": "invalid_type",
1201            }
1202        });
1203
1204        let result = Builder::new(invalid_source).build();
1205        assert!(result.is_err());
1206        let err = result.unwrap_err();
1207        assert!(err.is_unknown_type());
1208    }
1209
1210    #[tokio::test]
1211    async fn test_missing_expiry() -> TestResult {
1212        let server = Server::run();
1213        server.expect(
1214            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1215                json_encoded(json!({
1216                    "access_token": "test-user-account-token",
1217                    "expires_in": 3600,
1218                    "token_type": "Bearer",
1219                })),
1220            ),
1221        );
1222        server.expect(
1223            Expectation::matching(request::method_path(
1224                "POST",
1225                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1226            ))
1227            .respond_with(json_encoded(json!({
1228                "accessToken": "test-impersonated-token",
1229            }))),
1230        );
1231
1232        let impersonated_credential = json!({
1233            "type": "impersonated_service_account",
1234            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1235            "source_credentials": {
1236                "type": "authorized_user",
1237                "client_id": "test-client-id",
1238                "client_secret": "test-client-secret",
1239                "refresh_token": "test-refresh-token",
1240                "token_uri": server.url("/token").to_string()
1241            }
1242        });
1243        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1244
1245        let err = token_provider.token().await.unwrap_err();
1246        assert!(!err.is_transient());
1247
1248        Ok(())
1249    }
1250
1251    #[tokio::test]
1252    async fn test_invalid_expiry_format() -> TestResult {
1253        let server = Server::run();
1254        server.expect(
1255            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1256                json_encoded(json!({
1257                    "access_token": "test-user-account-token",
1258                    "expires_in": 3600,
1259                    "token_type": "Bearer",
1260                })),
1261            ),
1262        );
1263        server.expect(
1264            Expectation::matching(request::method_path(
1265                "POST",
1266                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1267            ))
1268            .respond_with(json_encoded(json!({
1269                "accessToken": "test-impersonated-token",
1270                "expireTime": "invalid-format"
1271            }))),
1272        );
1273
1274        let impersonated_credential = json!({
1275            "type": "impersonated_service_account",
1276            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1277            "source_credentials": {
1278                "type": "authorized_user",
1279                "client_id": "test-client-id",
1280                "client_secret": "test-client-secret",
1281                "refresh_token": "test-refresh-token",
1282                "token_uri": server.url("/token").to_string()
1283            }
1284        });
1285        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1286
1287        let err = token_provider.token().await.unwrap_err();
1288        assert!(!err.is_transient());
1289
1290        Ok(())
1291    }
1292
1293    #[tokio::test]
1294    async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1295        let server = Server::run();
1296        server.expect(
1297            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1298                json_encoded(json!({
1299                    "access_token": "test-user-account-token",
1300                    "expires_in": 3600,
1301                    "token_type": "Bearer",
1302                })),
1303            ),
1304        );
1305        server.expect(
1306            Expectation::matching(request::method_path(
1307                "POST",
1308                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1309            ))
1310            .respond_with(json_encoded(json!("bad json"))),
1311        );
1312
1313        let impersonated_credential = json!({
1314            "type": "impersonated_service_account",
1315            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1316            "source_credentials": {
1317                "type": "authorized_user",
1318                "client_id": "test-client-id",
1319                "client_secret": "test-client-secret",
1320                "refresh_token": "test-refresh-token",
1321                "token_uri": server.url("/token").to_string()
1322            }
1323        });
1324        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1325
1326        let e = token_provider.token().await.err().unwrap();
1327        assert!(!e.is_transient(), "{e}");
1328
1329        Ok(())
1330    }
1331
1332    #[tokio::test]
1333    async fn token_provider_nonretryable_error() -> TestResult {
1334        let server = Server::run();
1335        server.expect(
1336            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1337                json_encoded(json!({
1338                    "access_token": "test-user-account-token",
1339                    "expires_in": 3600,
1340                    "token_type": "Bearer",
1341                })),
1342            ),
1343        );
1344        server.expect(
1345            Expectation::matching(request::method_path(
1346                "POST",
1347                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1348            ))
1349            .respond_with(status_code(401)),
1350        );
1351
1352        let impersonated_credential = json!({
1353            "type": "impersonated_service_account",
1354            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1355            "source_credentials": {
1356                "type": "authorized_user",
1357                "client_id": "test-client-id",
1358                "client_secret": "test-client-secret",
1359                "refresh_token": "test-refresh-token",
1360                "token_uri": server.url("/token").to_string()
1361            }
1362        });
1363        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1364
1365        let err = token_provider.token().await.unwrap_err();
1366        assert!(!err.is_transient());
1367
1368        Ok(())
1369    }
1370
1371    #[tokio::test]
1372    async fn credential_full_with_quota_project_from_builder() -> TestResult {
1373        let server = Server::run();
1374        server.expect(
1375            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1376                json_encoded(json!({
1377                    "access_token": "test-user-account-token",
1378                    "expires_in": 3600,
1379                    "token_type": "Bearer",
1380                })),
1381            ),
1382        );
1383        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1384            .format(&time::format_description::well_known::Rfc3339)
1385            .unwrap();
1386        server.expect(
1387            Expectation::matching(request::method_path(
1388                "POST",
1389                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1390            ))
1391            .respond_with(json_encoded(json!({
1392                "accessToken": "test-impersonated-token",
1393                "expireTime": expire_time
1394            }))),
1395        );
1396
1397        let impersonated_credential = json!({
1398            "type": "impersonated_service_account",
1399            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1400            "source_credentials": {
1401                "type": "authorized_user",
1402                "client_id": "test-client-id",
1403                "client_secret": "test-client-secret",
1404                "refresh_token": "test-refresh-token",
1405                "token_uri": server.url("/token").to_string()
1406            }
1407        });
1408        let creds = Builder::new(impersonated_credential)
1409            .with_quota_project_id("test-project")
1410            .build()?;
1411
1412        let headers = creds.headers(Extensions::new()).await?;
1413        match headers {
1414            CacheableResource::New { data, .. } => {
1415                assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1416            }
1417            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1418        }
1419
1420        Ok(())
1421    }
1422
1423    #[tokio::test]
1424    async fn test_with_target_principal() {
1425        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1426            "type": "authorized_user",
1427            "client_id": "test-client-id",
1428            "client_secret": "test-client-secret",
1429            "refresh_token": "test-refresh-token"
1430        }))
1431        .build()
1432        .unwrap();
1433
1434        let (token_provider, _) = Builder::from_source_credentials(source_credentials)
1435            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
1436            .build_components()
1437            .unwrap();
1438
1439        assert_eq!(
1440            token_provider.inner.service_account_impersonation_url,
1441            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
1442        );
1443    }
1444
1445    #[tokio::test]
1446    async fn credential_full_with_quota_project_from_json() -> TestResult {
1447        let server = Server::run();
1448        server.expect(
1449            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1450                json_encoded(json!({
1451                    "access_token": "test-user-account-token",
1452                    "expires_in": 3600,
1453                    "token_type": "Bearer",
1454                })),
1455            ),
1456        );
1457        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1458            .format(&time::format_description::well_known::Rfc3339)
1459            .unwrap();
1460        server.expect(
1461            Expectation::matching(request::method_path(
1462                "POST",
1463                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1464            ))
1465            .respond_with(json_encoded(json!({
1466                "accessToken": "test-impersonated-token",
1467                "expireTime": expire_time
1468            }))),
1469        );
1470
1471        let impersonated_credential = json!({
1472            "type": "impersonated_service_account",
1473            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1474            "source_credentials": {
1475                "type": "authorized_user",
1476                "client_id": "test-client-id",
1477                "client_secret": "test-client-secret",
1478                "refresh_token": "test-refresh-token",
1479                "token_uri": server.url("/token").to_string()
1480            },
1481            "quota_project_id": "test-project-from-json",
1482        });
1483
1484        let creds = Builder::new(impersonated_credential).build()?;
1485
1486        let headers = creds.headers(Extensions::new()).await?;
1487        match headers {
1488            CacheableResource::New { data, .. } => {
1489                assert_eq!(
1490                    data.get("x-goog-user-project").unwrap(),
1491                    "test-project-from-json"
1492                );
1493            }
1494            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1495        }
1496
1497        Ok(())
1498    }
1499
1500    #[tokio::test]
1501    async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
1502        let server = Server::run();
1503
1504        // Expectation for the source credential token request.
1505        // It should NOT have any scopes in the body.
1506        server.expect(
1507            Expectation::matching(all_of![
1508                request::method_path("POST", "/source_token"),
1509                request::body(json_decoded(
1510                    |body: &serde_json::Value| body["scopes"].is_null()
1511                ))
1512            ])
1513            .respond_with(json_encoded(json!({
1514                "access_token": "source-token",
1515                "expires_in": 3600,
1516                "token_type": "Bearer",
1517            }))),
1518        );
1519
1520        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1521            .format(&time::format_description::well_known::Rfc3339)
1522            .unwrap();
1523
1524        // Expectation for the impersonation request.
1525        // It SHOULD have the scopes from the impersonated builder.
1526        server.expect(
1527            Expectation::matching(all_of![
1528                request::method_path(
1529                    "POST",
1530                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1531                ),
1532                request::headers(contains(("authorization", "Bearer source-token"))),
1533                request::body(json_decoded(eq(json!({
1534                    "scope": ["impersonated-scope"],
1535                    "lifetime": "3600s"
1536                }))))
1537            ])
1538            .respond_with(json_encoded(json!({
1539                "accessToken": "impersonated-token",
1540                "expireTime": expire_time
1541            }))),
1542        );
1543
1544        let impersonated_credential = json!({
1545            "type": "impersonated_service_account",
1546            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1547            "source_credentials": {
1548                "type": "authorized_user",
1549                "client_id": "test-client-id",
1550                "client_secret": "test-client-secret",
1551                "refresh_token": "test-refresh-token",
1552                "token_uri": server.url("/source_token").to_string()
1553            }
1554        });
1555
1556        let creds = Builder::new(impersonated_credential)
1557            .with_scopes(vec!["impersonated-scope"])
1558            .with_quota_project_id("impersonated-quota-project")
1559            .build()?;
1560
1561        // The quota project should be set on the final credentials object.
1562        let fmt = format!("{creds:?}");
1563        assert!(fmt.contains("impersonated-quota-project"));
1564
1565        // Fetching the token will trigger the mock server expectations.
1566        let _token = creds.headers(Extensions::new()).await?;
1567
1568        Ok(())
1569    }
1570
1571    #[tokio::test]
1572    async fn test_impersonated_metrics_header() -> TestResult {
1573        let server = Server::run();
1574        server.expect(
1575            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1576                json_encoded(json!({
1577                    "access_token": "test-user-account-token",
1578                    "expires_in": 3600,
1579                    "token_type": "Bearer",
1580                })),
1581            ),
1582        );
1583        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1584            .format(&time::format_description::well_known::Rfc3339)
1585            .unwrap();
1586        server.expect(
1587            Expectation::matching(all_of![
1588                request::method_path(
1589                    "POST",
1590                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1591                ),
1592                request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
1593                request::headers(contains((
1594                    "x-goog-api-client",
1595                    matches("auth-request-type/at")
1596                )))
1597            ])
1598            .respond_with(json_encoded(json!({
1599                "accessToken": "test-impersonated-token",
1600                "expireTime": expire_time
1601            }))),
1602        );
1603
1604        let impersonated_credential = json!({
1605            "type": "impersonated_service_account",
1606            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1607            "source_credentials": {
1608                "type": "authorized_user",
1609                "client_id": "test-client-id",
1610                "client_secret": "test-client-secret",
1611                "refresh_token": "test-refresh-token",
1612                "token_uri": server.url("/token").to_string()
1613            }
1614        });
1615        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1616
1617        let token = token_provider.token().await?;
1618        assert_eq!(token.token, "test-impersonated-token");
1619        assert_eq!(token.token_type, "Bearer");
1620
1621        Ok(())
1622    }
1623
1624    #[tokio::test]
1625    async fn test_impersonated_retries_for_success() -> TestResult {
1626        let mut server = Server::run();
1627        // Source credential token endpoint
1628        server.expect(
1629            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1630                json_encoded(json!({
1631                    "access_token": "test-user-account-token",
1632                    "expires_in": 3600,
1633                    "token_type": "Bearer",
1634                })),
1635            ),
1636        );
1637
1638        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1639            .format(&time::format_description::well_known::Rfc3339)
1640            .unwrap();
1641
1642        // Impersonation endpoint
1643        let impersonation_path =
1644            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1645        server.expect(
1646            Expectation::matching(request::method_path("POST", impersonation_path))
1647                .times(3)
1648                .respond_with(cycle![
1649                    status_code(503).body("try-again"),
1650                    status_code(503).body("try-again"),
1651                    status_code(200)
1652                        .append_header("Content-Type", "application/json")
1653                        .body(
1654                            json!({
1655                                "accessToken": "test-impersonated-token",
1656                                "expireTime": expire_time
1657                            })
1658                            .to_string()
1659                        ),
1660                ]),
1661        );
1662
1663        let impersonated_credential = json!({
1664            "type": "impersonated_service_account",
1665            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1666            "source_credentials": {
1667                "type": "authorized_user",
1668                "client_id": "test-client-id",
1669                "client_secret": "test-client-secret",
1670                "refresh_token": "test-refresh-token",
1671                "token_uri": server.url("/token").to_string()
1672            }
1673        });
1674
1675        let (token_provider, _) = Builder::new(impersonated_credential)
1676            .with_retry_policy(get_mock_auth_retry_policy(3))
1677            .with_backoff_policy(get_mock_backoff_policy())
1678            .with_retry_throttler(get_mock_retry_throttler())
1679            .build_components()?;
1680
1681        let token = token_provider.token().await?;
1682        assert_eq!(token.token, "test-impersonated-token");
1683
1684        server.verify_and_clear();
1685        Ok(())
1686    }
1687
1688    #[tokio::test]
1689    async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
1690        let mut server = Server::run();
1691        // Source credential token endpoint
1692        server.expect(
1693            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1694                json_encoded(json!({
1695                    "access_token": "test-user-account-token",
1696                    "expires_in": 3600,
1697                    "token_type": "Bearer",
1698                })),
1699            ),
1700        );
1701
1702        // Impersonation endpoint
1703        let impersonation_path =
1704            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1705        server.expect(
1706            Expectation::matching(request::method_path("POST", impersonation_path))
1707                .times(1)
1708                .respond_with(status_code(401)),
1709        );
1710
1711        let impersonated_credential = json!({
1712            "type": "impersonated_service_account",
1713            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1714            "source_credentials": {
1715                "type": "authorized_user",
1716                "client_id": "test-client-id",
1717                "client_secret": "test-client-secret",
1718                "refresh_token": "test-refresh-token",
1719                "token_uri": server.url("/token").to_string()
1720            }
1721        });
1722
1723        let (token_provider, _) = Builder::new(impersonated_credential)
1724            .with_retry_policy(get_mock_auth_retry_policy(3))
1725            .with_backoff_policy(get_mock_backoff_policy())
1726            .with_retry_throttler(get_mock_retry_throttler())
1727            .build_components()?;
1728
1729        let err = token_provider.token().await.unwrap_err();
1730        assert!(!err.is_transient());
1731
1732        server.verify_and_clear();
1733        Ok(())
1734    }
1735}