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        find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
643        get_mock_retry_throttler,
644    };
645    use crate::errors::CredentialsError;
646    use httptest::cycle;
647    use httptest::{Expectation, Server, matchers::*, responders::*};
648    use serde_json::json;
649
650    type TestResult = anyhow::Result<()>;
651
652    #[tokio::test]
653    async fn test_generate_access_token_success() -> TestResult {
654        let server = Server::run();
655        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
656            .format(&time::format_description::well_known::Rfc3339)
657            .unwrap();
658        server.expect(
659            Expectation::matching(all_of![
660                request::method_path(
661                    "POST",
662                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
663                ),
664                request::headers(contains(("authorization", "Bearer test-token"))),
665            ])
666            .respond_with(json_encoded(json!({
667                "accessToken": "test-impersonated-token",
668                "expireTime": expire_time
669            }))),
670        );
671
672        let mut headers = HeaderMap::new();
673        headers.insert("authorization", "Bearer test-token".parse().unwrap());
674        let token = generate_access_token(
675            headers,
676            None,
677            vec!["scope".to_string()],
678            DEFAULT_LIFETIME,
679            &server
680                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
681                .to_string(),
682        )
683        .await?;
684
685        assert_eq!(token.token, "test-impersonated-token");
686        Ok(())
687    }
688
689    #[tokio::test]
690    async fn test_generate_access_token_403() -> TestResult {
691        let server = Server::run();
692        server.expect(
693            Expectation::matching(all_of![
694                request::method_path(
695                    "POST",
696                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
697                ),
698                request::headers(contains(("authorization", "Bearer test-token"))),
699            ])
700            .respond_with(status_code(403)),
701        );
702
703        let mut headers = HeaderMap::new();
704        headers.insert("authorization", "Bearer test-token".parse().unwrap());
705        let err = generate_access_token(
706            headers,
707            None,
708            vec!["scope".to_string()],
709            DEFAULT_LIFETIME,
710            &server
711                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
712                .to_string(),
713        )
714        .await
715        .unwrap_err();
716
717        assert!(!err.is_transient());
718        Ok(())
719    }
720
721    #[tokio::test]
722    async fn test_generate_access_token_no_auth_header() -> TestResult {
723        let server = Server::run();
724        server.expect(
725            Expectation::matching(request::method_path(
726                "POST",
727                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
728            ))
729            .respond_with(status_code(401)),
730        );
731
732        let err = generate_access_token(
733            HeaderMap::new(),
734            None,
735            vec!["scope".to_string()],
736            DEFAULT_LIFETIME,
737            &server
738                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
739                .to_string(),
740        )
741        .await
742        .unwrap_err();
743
744        assert!(!err.is_transient());
745        Ok(())
746    }
747
748    #[tokio::test]
749    async fn test_impersonated_service_account() -> TestResult {
750        let server = Server::run();
751        server.expect(
752            Expectation::matching(request::method_path("POST", "/token")).respond_with(
753                json_encoded(json!({
754                    "access_token": "test-user-account-token",
755                    "expires_in": 3600,
756                    "token_type": "Bearer",
757                })),
758            ),
759        );
760        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
761            .format(&time::format_description::well_known::Rfc3339)
762            .unwrap();
763        server.expect(
764            Expectation::matching(all_of![
765                request::method_path(
766                    "POST",
767                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
768                ),
769                request::headers(contains((
770                    "authorization",
771                    "Bearer test-user-account-token"
772                ))),
773                request::body(json_decoded(eq(json!({
774                    "scope": ["scope1", "scope2"],
775                    "lifetime": "3600s"
776                }))))
777            ])
778            .respond_with(json_encoded(json!({
779                "accessToken": "test-impersonated-token",
780                "expireTime": expire_time
781            }))),
782        );
783
784        let impersonated_credential = json!({
785            "type": "impersonated_service_account",
786            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
787            "source_credentials": {
788                "type": "authorized_user",
789                "client_id": "test-client-id",
790                "client_secret": "test-client-secret",
791                "refresh_token": "test-refresh-token",
792                "token_uri": server.url("/token").to_string()
793            }
794        });
795        let (token_provider, _) = Builder::new(impersonated_credential)
796            .with_scopes(vec!["scope1", "scope2"])
797            .build_components()?;
798
799        let token = token_provider.token().await?;
800        assert_eq!(token.token, "test-impersonated-token");
801        assert_eq!(token.token_type, "Bearer");
802
803        Ok(())
804    }
805
806    #[tokio::test]
807    async fn test_impersonated_service_account_default_scope() -> TestResult {
808        let server = Server::run();
809        server.expect(
810            Expectation::matching(request::method_path("POST", "/token")).respond_with(
811                json_encoded(json!({
812                    "access_token": "test-user-account-token",
813                    "expires_in": 3600,
814                    "token_type": "Bearer",
815                })),
816            ),
817        );
818        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
819            .format(&time::format_description::well_known::Rfc3339)
820            .unwrap();
821        server.expect(
822            Expectation::matching(all_of![
823                request::method_path(
824                    "POST",
825                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
826                ),
827                request::headers(contains((
828                    "authorization",
829                    "Bearer test-user-account-token"
830                ))),
831                request::body(json_decoded(eq(json!({
832                    "scope": [DEFAULT_SCOPE],
833                    "lifetime": "3600s"
834                }))))
835            ])
836            .respond_with(json_encoded(json!({
837                "accessToken": "test-impersonated-token",
838                "expireTime": expire_time
839            }))),
840        );
841
842        let impersonated_credential = json!({
843            "type": "impersonated_service_account",
844            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
845            "source_credentials": {
846                "type": "authorized_user",
847                "client_id": "test-client-id",
848                "client_secret": "test-client-secret",
849                "refresh_token": "test-refresh-token",
850                "token_uri": server.url("/token").to_string()
851            }
852        });
853        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
854
855        let token = token_provider.token().await?;
856        assert_eq!(token.token, "test-impersonated-token");
857        assert_eq!(token.token_type, "Bearer");
858
859        Ok(())
860    }
861
862    #[tokio::test]
863    async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
864        let server = Server::run();
865        server.expect(
866            Expectation::matching(request::method_path("POST", "/token")).respond_with(
867                json_encoded(json!({
868                    "access_token": "test-user-account-token",
869                    "expires_in": 3600,
870                    "token_type": "Bearer",
871                })),
872            ),
873        );
874        let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
875            .format(&time::format_description::well_known::Rfc3339)
876            .unwrap();
877        server.expect(
878            Expectation::matching(all_of![
879                request::method_path(
880                    "POST",
881                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
882                ),
883                request::headers(contains((
884                    "authorization",
885                    "Bearer test-user-account-token"
886                ))),
887                request::body(json_decoded(eq(json!({
888                    "scope": ["scope1", "scope2"],
889                    "lifetime": "3.5s"
890                }))))
891            ])
892            .respond_with(json_encoded(json!({
893                "accessToken": "test-impersonated-token",
894                "expireTime": expire_time
895            }))),
896        );
897
898        let impersonated_credential = json!({
899            "type": "impersonated_service_account",
900            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
901            "source_credentials": {
902                "type": "authorized_user",
903                "client_id": "test-client-id",
904                "client_secret": "test-client-secret",
905                "refresh_token": "test-refresh-token",
906                "token_uri": server.url("/token").to_string()
907            }
908        });
909        let (token_provider, _) = Builder::new(impersonated_credential)
910            .with_scopes(vec!["scope1", "scope2"])
911            .with_lifetime(Duration::from_secs_f32(3.5))
912            .build_components()?;
913
914        let token = token_provider.token().await?;
915        assert_eq!(token.token, "test-impersonated-token");
916
917        Ok(())
918    }
919
920    #[tokio::test]
921    async fn test_with_delegates() -> TestResult {
922        let server = Server::run();
923        server.expect(
924            Expectation::matching(request::method_path("POST", "/token")).respond_with(
925                json_encoded(json!({
926                    "access_token": "test-user-account-token",
927                    "expires_in": 3600,
928                    "token_type": "Bearer",
929                })),
930            ),
931        );
932        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
933            .format(&time::format_description::well_known::Rfc3339)
934            .unwrap();
935        server.expect(
936            Expectation::matching(all_of![
937                request::method_path(
938                    "POST",
939                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
940                ),
941                request::headers(contains((
942                    "authorization",
943                    "Bearer test-user-account-token"
944                ))),
945                request::body(json_decoded(eq(json!({
946                    "scope": [DEFAULT_SCOPE],
947                    "lifetime": "3600s",
948                    "delegates": ["delegate1", "delegate2"]
949                }))))
950            ])
951            .respond_with(json_encoded(json!({
952                "accessToken": "test-impersonated-token",
953                "expireTime": expire_time
954            }))),
955        );
956
957        let impersonated_credential = json!({
958            "type": "impersonated_service_account",
959            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
960            "source_credentials": {
961                "type": "authorized_user",
962                "client_id": "test-client-id",
963                "client_secret": "test-client-secret",
964                "refresh_token": "test-refresh-token",
965                "token_uri": server.url("/token").to_string()
966            }
967        });
968        let (token_provider, _) = Builder::new(impersonated_credential)
969            .with_delegates(vec!["delegate1", "delegate2"])
970            .build_components()?;
971
972        let token = token_provider.token().await?;
973        assert_eq!(token.token, "test-impersonated-token");
974        assert_eq!(token.token_type, "Bearer");
975
976        Ok(())
977    }
978
979    #[tokio::test]
980    async fn test_impersonated_service_account_fail() -> TestResult {
981        let server = Server::run();
982        server.expect(
983            Expectation::matching(request::method_path("POST", "/token")).respond_with(
984                json_encoded(json!({
985                    "access_token": "test-user-account-token",
986                    "expires_in": 3600,
987                    "token_type": "Bearer",
988                })),
989            ),
990        );
991        server.expect(
992            Expectation::matching(request::method_path(
993                "POST",
994                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
995            ))
996            .respond_with(status_code(500)),
997        );
998
999        let impersonated_credential = json!({
1000            "type": "impersonated_service_account",
1001            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1002            "source_credentials": {
1003                "type": "authorized_user",
1004                "client_id": "test-client-id",
1005                "client_secret": "test-client-secret",
1006                "refresh_token": "test-refresh-token",
1007                "token_uri": server.url("/token").to_string()
1008            }
1009        });
1010        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1011
1012        let err = token_provider.token().await.unwrap_err();
1013        let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1014        assert!(original_err.is_transient());
1015
1016        Ok(())
1017    }
1018
1019    #[tokio::test]
1020    async fn debug_token_provider() {
1021        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1022            "type": "authorized_user",
1023            "client_id": "test-client-id",
1024            "client_secret": "test-client-secret",
1025            "refresh_token": "test-refresh-token"
1026        }))
1027        .build()
1028        .unwrap();
1029
1030        let expected = ImpersonatedTokenProvider {
1031            source_credentials,
1032            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1033            delegates: Some(vec!["delegate1".to_string()]),
1034            scopes: vec!["scope1".to_string()],
1035            lifetime: Duration::from_secs(3600),
1036        };
1037        let fmt = format!("{expected:?}");
1038        assert!(fmt.contains("UserCredentials"), "{fmt}");
1039        assert!(fmt.contains("test-client-id"), "{fmt}");
1040        assert!(!fmt.contains("test-client-secret"), "{fmt}");
1041        assert!(!fmt.contains("test-refresh-token"), "{fmt}");
1042        assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1043        assert!(fmt.contains("delegate1"), "{fmt}");
1044        assert!(fmt.contains("scope1"), "{fmt}");
1045        assert!(fmt.contains("3600s"), "{fmt}");
1046    }
1047
1048    #[test]
1049    fn impersonated_config_full_from_json_success() {
1050        let source_credentials_json = json!({
1051            "type": "authorized_user",
1052            "client_id": "test-client-id",
1053            "client_secret": "test-client-secret",
1054            "refresh_token": "test-refresh-token"
1055        });
1056        let json = json!({
1057            "type": "impersonated_service_account",
1058            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1059            "source_credentials": source_credentials_json,
1060            "delegates": ["delegate1"],
1061            "quota_project_id": "test-project-id",
1062        });
1063
1064        let expected = ImpersonatedConfig {
1065            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1066            source_credentials: source_credentials_json,
1067            delegates: Some(vec!["delegate1".to_string()]),
1068            quota_project_id: Some("test-project-id".to_string()),
1069        };
1070        let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1071        assert_eq!(actual, expected);
1072    }
1073
1074    #[test]
1075    fn impersonated_config_partial_from_json_success() {
1076        let source_credentials_json = json!({
1077            "type": "authorized_user",
1078            "client_id": "test-client-id",
1079            "client_secret": "test-client-secret",
1080            "refresh_token": "test-refresh-token"
1081        });
1082        let json = json!({
1083            "type": "impersonated_service_account",
1084            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1085            "source_credentials": source_credentials_json
1086        });
1087
1088        let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1089        assert_eq!(
1090            config.service_account_impersonation_url,
1091            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1092        );
1093        assert_eq!(config.source_credentials, source_credentials_json);
1094        assert_eq!(config.delegates, None);
1095        assert_eq!(config.quota_project_id, None);
1096    }
1097
1098    #[tokio::test]
1099    async fn test_impersonated_service_account_source_fail() -> TestResult {
1100        #[derive(Debug)]
1101        struct MockSourceCredentialsFail;
1102
1103        #[async_trait]
1104        impl CredentialsProvider for MockSourceCredentialsFail {
1105            async fn headers(
1106                &self,
1107                _extensions: Extensions,
1108            ) -> Result<CacheableResource<HeaderMap>> {
1109                Err(errors::non_retryable_from_str("source failed"))
1110            }
1111        }
1112
1113        let source_credentials = Credentials {
1114            inner: Arc::new(MockSourceCredentialsFail),
1115        };
1116
1117        let token_provider = ImpersonatedTokenProvider {
1118            source_credentials,
1119            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1120            delegates: Some(vec!["delegate1".to_string()]),
1121            scopes: vec!["scope1".to_string()],
1122            lifetime: DEFAULT_LIFETIME,
1123        };
1124
1125        let err = token_provider.token().await.unwrap_err();
1126        assert!(err.to_string().contains("source failed"));
1127
1128        Ok(())
1129    }
1130
1131    #[tokio::test]
1132    async fn test_missing_impersonation_url_fail() {
1133        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1134            "type": "authorized_user",
1135            "client_id": "test-client-id",
1136            "client_secret": "test-client-secret",
1137            "refresh_token": "test-refresh-token"
1138        }))
1139        .build()
1140        .unwrap();
1141
1142        let result = Builder::from_source_credentials(source_credentials).build();
1143        assert!(result.is_err());
1144        let err = result.unwrap_err();
1145        assert!(err.is_parsing());
1146        assert!(
1147            err.to_string()
1148                .contains("`service_account_impersonation_url` is required")
1149        );
1150    }
1151
1152    #[tokio::test]
1153    async fn test_nested_impersonated_credentials_fail() {
1154        let nested_impersonated = json!({
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": "impersonated_service_account",
1159                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1160                "source_credentials": {
1161                    "type": "authorized_user",
1162                    "client_id": "test-client-id",
1163                    "client_secret": "test-client-secret",
1164                    "refresh_token": "test-refresh-token"
1165                }
1166            }
1167        });
1168
1169        let result = Builder::new(nested_impersonated).build();
1170        assert!(result.is_err());
1171        let err = result.unwrap_err();
1172        assert!(err.is_parsing());
1173        assert!(
1174            err.to_string().contains(
1175                "source credential of type `impersonated_service_account` is not supported"
1176            )
1177        );
1178    }
1179
1180    #[tokio::test]
1181    async fn test_malformed_impersonated_credentials_fail() {
1182        let malformed_impersonated = json!({
1183            "type": "impersonated_service_account",
1184            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1185        });
1186
1187        let result = Builder::new(malformed_impersonated).build();
1188        assert!(result.is_err());
1189        let err = result.unwrap_err();
1190        assert!(err.is_parsing());
1191        assert!(
1192            err.to_string()
1193                .contains("missing field `source_credentials`")
1194        );
1195    }
1196
1197    #[tokio::test]
1198    async fn test_invalid_source_credential_type_fail() {
1199        let invalid_source = json!({
1200            "type": "impersonated_service_account",
1201            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1202            "source_credentials": {
1203                "type": "invalid_type",
1204            }
1205        });
1206
1207        let result = Builder::new(invalid_source).build();
1208        assert!(result.is_err());
1209        let err = result.unwrap_err();
1210        assert!(err.is_unknown_type());
1211    }
1212
1213    #[tokio::test]
1214    async fn test_missing_expiry() -> TestResult {
1215        let server = Server::run();
1216        server.expect(
1217            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1218                json_encoded(json!({
1219                    "access_token": "test-user-account-token",
1220                    "expires_in": 3600,
1221                    "token_type": "Bearer",
1222                })),
1223            ),
1224        );
1225        server.expect(
1226            Expectation::matching(request::method_path(
1227                "POST",
1228                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1229            ))
1230            .respond_with(json_encoded(json!({
1231                "accessToken": "test-impersonated-token",
1232            }))),
1233        );
1234
1235        let impersonated_credential = json!({
1236            "type": "impersonated_service_account",
1237            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1238            "source_credentials": {
1239                "type": "authorized_user",
1240                "client_id": "test-client-id",
1241                "client_secret": "test-client-secret",
1242                "refresh_token": "test-refresh-token",
1243                "token_uri": server.url("/token").to_string()
1244            }
1245        });
1246        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1247
1248        let err = token_provider.token().await.unwrap_err();
1249        assert!(!err.is_transient());
1250
1251        Ok(())
1252    }
1253
1254    #[tokio::test]
1255    async fn test_invalid_expiry_format() -> TestResult {
1256        let server = Server::run();
1257        server.expect(
1258            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1259                json_encoded(json!({
1260                    "access_token": "test-user-account-token",
1261                    "expires_in": 3600,
1262                    "token_type": "Bearer",
1263                })),
1264            ),
1265        );
1266        server.expect(
1267            Expectation::matching(request::method_path(
1268                "POST",
1269                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1270            ))
1271            .respond_with(json_encoded(json!({
1272                "accessToken": "test-impersonated-token",
1273                "expireTime": "invalid-format"
1274            }))),
1275        );
1276
1277        let impersonated_credential = json!({
1278            "type": "impersonated_service_account",
1279            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1280            "source_credentials": {
1281                "type": "authorized_user",
1282                "client_id": "test-client-id",
1283                "client_secret": "test-client-secret",
1284                "refresh_token": "test-refresh-token",
1285                "token_uri": server.url("/token").to_string()
1286            }
1287        });
1288        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1289
1290        let err = token_provider.token().await.unwrap_err();
1291        assert!(!err.is_transient());
1292
1293        Ok(())
1294    }
1295
1296    #[tokio::test]
1297    async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1298        let server = Server::run();
1299        server.expect(
1300            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1301                json_encoded(json!({
1302                    "access_token": "test-user-account-token",
1303                    "expires_in": 3600,
1304                    "token_type": "Bearer",
1305                })),
1306            ),
1307        );
1308        server.expect(
1309            Expectation::matching(request::method_path(
1310                "POST",
1311                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1312            ))
1313            .respond_with(json_encoded(json!("bad json"))),
1314        );
1315
1316        let impersonated_credential = json!({
1317            "type": "impersonated_service_account",
1318            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1319            "source_credentials": {
1320                "type": "authorized_user",
1321                "client_id": "test-client-id",
1322                "client_secret": "test-client-secret",
1323                "refresh_token": "test-refresh-token",
1324                "token_uri": server.url("/token").to_string()
1325            }
1326        });
1327        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1328
1329        let e = token_provider.token().await.err().unwrap();
1330        assert!(!e.is_transient(), "{e}");
1331
1332        Ok(())
1333    }
1334
1335    #[tokio::test]
1336    async fn token_provider_nonretryable_error() -> TestResult {
1337        let server = Server::run();
1338        server.expect(
1339            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1340                json_encoded(json!({
1341                    "access_token": "test-user-account-token",
1342                    "expires_in": 3600,
1343                    "token_type": "Bearer",
1344                })),
1345            ),
1346        );
1347        server.expect(
1348            Expectation::matching(request::method_path(
1349                "POST",
1350                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1351            ))
1352            .respond_with(status_code(401)),
1353        );
1354
1355        let impersonated_credential = json!({
1356            "type": "impersonated_service_account",
1357            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1358            "source_credentials": {
1359                "type": "authorized_user",
1360                "client_id": "test-client-id",
1361                "client_secret": "test-client-secret",
1362                "refresh_token": "test-refresh-token",
1363                "token_uri": server.url("/token").to_string()
1364            }
1365        });
1366        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1367
1368        let err = token_provider.token().await.unwrap_err();
1369        assert!(!err.is_transient());
1370
1371        Ok(())
1372    }
1373
1374    #[tokio::test]
1375    async fn credential_full_with_quota_project_from_builder() -> TestResult {
1376        let server = Server::run();
1377        server.expect(
1378            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1379                json_encoded(json!({
1380                    "access_token": "test-user-account-token",
1381                    "expires_in": 3600,
1382                    "token_type": "Bearer",
1383                })),
1384            ),
1385        );
1386        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1387            .format(&time::format_description::well_known::Rfc3339)
1388            .unwrap();
1389        server.expect(
1390            Expectation::matching(request::method_path(
1391                "POST",
1392                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1393            ))
1394            .respond_with(json_encoded(json!({
1395                "accessToken": "test-impersonated-token",
1396                "expireTime": expire_time
1397            }))),
1398        );
1399
1400        let impersonated_credential = json!({
1401            "type": "impersonated_service_account",
1402            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1403            "source_credentials": {
1404                "type": "authorized_user",
1405                "client_id": "test-client-id",
1406                "client_secret": "test-client-secret",
1407                "refresh_token": "test-refresh-token",
1408                "token_uri": server.url("/token").to_string()
1409            }
1410        });
1411        let creds = Builder::new(impersonated_credential)
1412            .with_quota_project_id("test-project")
1413            .build()?;
1414
1415        let headers = creds.headers(Extensions::new()).await?;
1416        match headers {
1417            CacheableResource::New { data, .. } => {
1418                assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1419            }
1420            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1421        }
1422
1423        Ok(())
1424    }
1425
1426    #[tokio::test]
1427    async fn test_with_target_principal() {
1428        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1429            "type": "authorized_user",
1430            "client_id": "test-client-id",
1431            "client_secret": "test-client-secret",
1432            "refresh_token": "test-refresh-token"
1433        }))
1434        .build()
1435        .unwrap();
1436
1437        let (token_provider, _) = Builder::from_source_credentials(source_credentials)
1438            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
1439            .build_components()
1440            .unwrap();
1441
1442        assert_eq!(
1443            token_provider.inner.service_account_impersonation_url,
1444            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
1445        );
1446    }
1447
1448    #[tokio::test]
1449    async fn credential_full_with_quota_project_from_json() -> TestResult {
1450        let server = Server::run();
1451        server.expect(
1452            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1453                json_encoded(json!({
1454                    "access_token": "test-user-account-token",
1455                    "expires_in": 3600,
1456                    "token_type": "Bearer",
1457                })),
1458            ),
1459        );
1460        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1461            .format(&time::format_description::well_known::Rfc3339)
1462            .unwrap();
1463        server.expect(
1464            Expectation::matching(request::method_path(
1465                "POST",
1466                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1467            ))
1468            .respond_with(json_encoded(json!({
1469                "accessToken": "test-impersonated-token",
1470                "expireTime": expire_time
1471            }))),
1472        );
1473
1474        let impersonated_credential = json!({
1475            "type": "impersonated_service_account",
1476            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1477            "source_credentials": {
1478                "type": "authorized_user",
1479                "client_id": "test-client-id",
1480                "client_secret": "test-client-secret",
1481                "refresh_token": "test-refresh-token",
1482                "token_uri": server.url("/token").to_string()
1483            },
1484            "quota_project_id": "test-project-from-json",
1485        });
1486
1487        let creds = Builder::new(impersonated_credential).build()?;
1488
1489        let headers = creds.headers(Extensions::new()).await?;
1490        match headers {
1491            CacheableResource::New { data, .. } => {
1492                assert_eq!(
1493                    data.get("x-goog-user-project").unwrap(),
1494                    "test-project-from-json"
1495                );
1496            }
1497            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1498        }
1499
1500        Ok(())
1501    }
1502
1503    #[tokio::test]
1504    async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
1505        let server = Server::run();
1506
1507        // Expectation for the source credential token request.
1508        // It should NOT have any scopes in the body.
1509        server.expect(
1510            Expectation::matching(all_of![
1511                request::method_path("POST", "/source_token"),
1512                request::body(json_decoded(
1513                    |body: &serde_json::Value| body["scopes"].is_null()
1514                ))
1515            ])
1516            .respond_with(json_encoded(json!({
1517                "access_token": "source-token",
1518                "expires_in": 3600,
1519                "token_type": "Bearer",
1520            }))),
1521        );
1522
1523        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1524            .format(&time::format_description::well_known::Rfc3339)
1525            .unwrap();
1526
1527        // Expectation for the impersonation request.
1528        // It SHOULD have the scopes from the impersonated builder.
1529        server.expect(
1530            Expectation::matching(all_of![
1531                request::method_path(
1532                    "POST",
1533                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1534                ),
1535                request::headers(contains(("authorization", "Bearer source-token"))),
1536                request::body(json_decoded(eq(json!({
1537                    "scope": ["impersonated-scope"],
1538                    "lifetime": "3600s"
1539                }))))
1540            ])
1541            .respond_with(json_encoded(json!({
1542                "accessToken": "impersonated-token",
1543                "expireTime": expire_time
1544            }))),
1545        );
1546
1547        let impersonated_credential = json!({
1548            "type": "impersonated_service_account",
1549            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1550            "source_credentials": {
1551                "type": "authorized_user",
1552                "client_id": "test-client-id",
1553                "client_secret": "test-client-secret",
1554                "refresh_token": "test-refresh-token",
1555                "token_uri": server.url("/source_token").to_string()
1556            }
1557        });
1558
1559        let creds = Builder::new(impersonated_credential)
1560            .with_scopes(vec!["impersonated-scope"])
1561            .with_quota_project_id("impersonated-quota-project")
1562            .build()?;
1563
1564        // The quota project should be set on the final credentials object.
1565        let fmt = format!("{creds:?}");
1566        assert!(fmt.contains("impersonated-quota-project"));
1567
1568        // Fetching the token will trigger the mock server expectations.
1569        let _token = creds.headers(Extensions::new()).await?;
1570
1571        Ok(())
1572    }
1573
1574    #[tokio::test]
1575    async fn test_impersonated_metrics_header() -> TestResult {
1576        let server = Server::run();
1577        server.expect(
1578            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1579                json_encoded(json!({
1580                    "access_token": "test-user-account-token",
1581                    "expires_in": 3600,
1582                    "token_type": "Bearer",
1583                })),
1584            ),
1585        );
1586        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1587            .format(&time::format_description::well_known::Rfc3339)
1588            .unwrap();
1589        server.expect(
1590            Expectation::matching(all_of![
1591                request::method_path(
1592                    "POST",
1593                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1594                ),
1595                request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
1596                request::headers(contains((
1597                    "x-goog-api-client",
1598                    matches("auth-request-type/at")
1599                )))
1600            ])
1601            .respond_with(json_encoded(json!({
1602                "accessToken": "test-impersonated-token",
1603                "expireTime": expire_time
1604            }))),
1605        );
1606
1607        let impersonated_credential = json!({
1608            "type": "impersonated_service_account",
1609            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1610            "source_credentials": {
1611                "type": "authorized_user",
1612                "client_id": "test-client-id",
1613                "client_secret": "test-client-secret",
1614                "refresh_token": "test-refresh-token",
1615                "token_uri": server.url("/token").to_string()
1616            }
1617        });
1618        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1619
1620        let token = token_provider.token().await?;
1621        assert_eq!(token.token, "test-impersonated-token");
1622        assert_eq!(token.token_type, "Bearer");
1623
1624        Ok(())
1625    }
1626
1627    #[tokio::test]
1628    async fn test_impersonated_retries_for_success() -> TestResult {
1629        let mut server = Server::run();
1630        // Source credential token endpoint
1631        server.expect(
1632            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1633                json_encoded(json!({
1634                    "access_token": "test-user-account-token",
1635                    "expires_in": 3600,
1636                    "token_type": "Bearer",
1637                })),
1638            ),
1639        );
1640
1641        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1642            .format(&time::format_description::well_known::Rfc3339)
1643            .unwrap();
1644
1645        // Impersonation endpoint
1646        let impersonation_path =
1647            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1648        server.expect(
1649            Expectation::matching(request::method_path("POST", impersonation_path))
1650                .times(3)
1651                .respond_with(cycle![
1652                    status_code(503).body("try-again"),
1653                    status_code(503).body("try-again"),
1654                    status_code(200)
1655                        .append_header("Content-Type", "application/json")
1656                        .body(
1657                            json!({
1658                                "accessToken": "test-impersonated-token",
1659                                "expireTime": expire_time
1660                            })
1661                            .to_string()
1662                        ),
1663                ]),
1664        );
1665
1666        let impersonated_credential = json!({
1667            "type": "impersonated_service_account",
1668            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1669            "source_credentials": {
1670                "type": "authorized_user",
1671                "client_id": "test-client-id",
1672                "client_secret": "test-client-secret",
1673                "refresh_token": "test-refresh-token",
1674                "token_uri": server.url("/token").to_string()
1675            }
1676        });
1677
1678        let (token_provider, _) = Builder::new(impersonated_credential)
1679            .with_retry_policy(get_mock_auth_retry_policy(3))
1680            .with_backoff_policy(get_mock_backoff_policy())
1681            .with_retry_throttler(get_mock_retry_throttler())
1682            .build_components()?;
1683
1684        let token = token_provider.token().await?;
1685        assert_eq!(token.token, "test-impersonated-token");
1686
1687        server.verify_and_clear();
1688        Ok(())
1689    }
1690
1691    #[tokio::test]
1692    async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
1693        let mut server = Server::run();
1694        // Source credential token endpoint
1695        server.expect(
1696            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1697                json_encoded(json!({
1698                    "access_token": "test-user-account-token",
1699                    "expires_in": 3600,
1700                    "token_type": "Bearer",
1701                })),
1702            ),
1703        );
1704
1705        // Impersonation endpoint
1706        let impersonation_path =
1707            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1708        server.expect(
1709            Expectation::matching(request::method_path("POST", impersonation_path))
1710                .times(1)
1711                .respond_with(status_code(401)),
1712        );
1713
1714        let impersonated_credential = json!({
1715            "type": "impersonated_service_account",
1716            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1717            "source_credentials": {
1718                "type": "authorized_user",
1719                "client_id": "test-client-id",
1720                "client_secret": "test-client-secret",
1721                "refresh_token": "test-refresh-token",
1722                "token_uri": server.url("/token").to_string()
1723            }
1724        });
1725
1726        let (token_provider, _) = Builder::new(impersonated_credential)
1727            .with_retry_policy(get_mock_auth_retry_policy(3))
1728            .with_backoff_policy(get_mock_backoff_policy())
1729            .with_retry_throttler(get_mock_retry_throttler())
1730            .build_components()?;
1731
1732        let err = token_provider.token().await.unwrap_err();
1733        assert!(!err.is_transient());
1734
1735        server.verify_and_clear();
1736        Ok(())
1737    }
1738}