Skip to main content

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//! # async fn sample() -> anyhow::Result<()> {
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(()) }
53//! ```
54//!
55//! ## Example: Creating credentials with custom retry behavior
56//!
57//! ```
58//! # use google_cloud_auth::credentials::impersonated;
59//! # use serde_json::json;
60//! # use std::time::Duration;
61//! # use http::Extensions;
62//! # async fn sample() -> anyhow::Result<()> {
63//! use google_cloud_gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
64//! use google_cloud_gax::exponential_backoff::ExponentialBackoff;
65//! # let source_credentials = json!({
66//! #     "type": "authorized_user",
67//! #     "client_id": "test-client-id",
68//! #     "client_secret": "test-client-secret",
69//! #     "refresh_token": "test-refresh-token"
70//! # });
71//! #
72//! # let impersonated_credential = json!({
73//! #     "type": "impersonated_service_account",
74//! #     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
75//! #     "source_credentials": source_credentials,
76//! # });
77//! let backoff = ExponentialBackoff::default();
78//! let credentials = impersonated::Builder::new(impersonated_credential)
79//!     .with_retry_policy(AlwaysRetry.with_attempt_limit(3))
80//!     .with_backoff_policy(backoff)
81//!     .build()?;
82//! let headers = credentials.headers(Extensions::new()).await?;
83//! println!("Headers: {headers:?}");
84//! # Ok(()) }
85//! ```
86//!
87//! [Impersonated service account]: https://cloud.google.com/docs/authentication/use-service-account-impersonation
88//! [User Account]: https://cloud.google.com/docs/authentication#user-accounts
89//! [Service Account]: https://cloud.google.com/iam/docs/service-account-overview
90//! [Service Account Token Creator Role]: https://cloud.google.com/docs/authentication/use-service-account-impersonation#required-roles
91
92use crate::access_boundary::CredentialsWithAccessBoundary;
93use crate::build_errors::Error as BuilderError;
94use crate::constants::DEFAULT_SCOPE;
95use crate::credentials::dynamic::{AccessTokenCredentialsProvider, CredentialsProvider};
96use crate::credentials::{
97    AccessToken, AccessTokenCredentials, CacheableResource, Credentials, build_credentials,
98    extract_credential_type,
99};
100use crate::errors::{self, CredentialsError};
101use crate::headers_util::{
102    self, ACCESS_TOKEN_REQUEST_TYPE, AuthHeadersBuilder, 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 google_cloud_gax::Result as GaxResult;
110use google_cloud_gax::backoff_policy::{BackoffPolicy, BackoffPolicyArg};
111use google_cloud_gax::error::Error as GaxError;
112use google_cloud_gax::exponential_backoff::ExponentialBackoff;
113use google_cloud_gax::retry_loop_internal::retry_loop;
114use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicy, RetryPolicyArg, RetryPolicyExt};
115use google_cloud_gax::retry_throttler::{
116    AdaptiveThrottler, RetryThrottlerArg, SharedRetryThrottler,
117};
118use http::{Extensions, HeaderMap};
119use reqwest::Client;
120use serde_json::Value;
121use std::fmt::Debug;
122use std::sync::Arc;
123use std::time::Duration;
124use time::OffsetDateTime;
125use tokio::time::Instant;
126
127pub(crate) const IMPERSONATED_CREDENTIAL_TYPE: &str = "imp";
128pub(crate) const DEFAULT_LIFETIME: Duration = Duration::from_secs(3600);
129pub(crate) const MSG: &str = "failed to fetch token";
130
131#[derive(Debug, Clone)]
132pub(crate) enum BuilderSource {
133    FromJson(Value),
134    FromCredentials(Credentials),
135}
136
137/// A builder for constructing Impersonated Service Account [Credentials] instance.
138///
139/// # Example
140/// ```
141/// # use google_cloud_auth::credentials::impersonated::Builder;
142/// # async fn sample() -> anyhow::Result<()> {
143/// let impersonated_credential = serde_json::json!({
144///     "type": "impersonated_service_account",
145///     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
146///     "source_credentials": {
147///         "type": "authorized_user",
148///         "client_id": "test-client-id",
149///         "client_secret": "test-client-secret",
150///         "refresh_token": "test-refresh-token"
151///     }
152/// });
153/// let credentials = Builder::new(impersonated_credential).build();
154/// # Ok(()) }
155/// ```
156pub struct Builder {
157    source: BuilderSource,
158    service_account_impersonation_url: Option<ImpersonationUrl>,
159    delegates: Option<Vec<String>>,
160    scopes: Option<Vec<String>>,
161    quota_project_id: Option<String>,
162    universe_domain: Option<String>,
163    lifetime: Option<Duration>,
164    retry_builder: RetryTokenProviderBuilder,
165    iam_endpoint_override: Option<String>,
166    is_access_boundary_enabled: bool,
167}
168
169#[derive(Debug, Clone)]
170pub(crate) struct ImpersonationUrl {
171    // optional endpoint override for testing
172    pub(crate) endpoint: Option<String>,
173    kind: ImpersonationUrlKind,
174}
175
176#[derive(Debug, Clone)]
177pub(crate) enum ImpersonationUrlKind {
178    TargetPrincipal(String),
179    Exact(String),
180}
181
182impl ImpersonationUrl {
183    pub(crate) fn exact(url: String) -> Self {
184        Self {
185            endpoint: None,
186            kind: ImpersonationUrlKind::Exact(url),
187        }
188    }
189
190    pub(crate) fn target_principal(principal: String) -> Self {
191        Self {
192            endpoint: None,
193            kind: ImpersonationUrlKind::TargetPrincipal(principal),
194        }
195    }
196
197    pub(crate) async fn access_token_url(&self, creds: &Credentials) -> String {
198        match &self.kind {
199            ImpersonationUrlKind::TargetPrincipal(principal) => {
200                self.impersonation_url_for_method(creds, principal, "generateAccessToken")
201                    .await
202            }
203            ImpersonationUrlKind::Exact(url) => url.clone(),
204        }
205    }
206
207    #[cfg(feature = "idtoken")]
208    pub(crate) async fn id_token_url(&self, creds: &Credentials) -> String {
209        match &self.kind {
210            ImpersonationUrlKind::TargetPrincipal(principal) => {
211                self.impersonation_url_for_method(creds, principal, "generateIdToken")
212                    .await
213            }
214            ImpersonationUrlKind::Exact(url) => {
215                url.replace("generateAccessToken", "generateIdToken")
216            }
217        }
218    }
219
220    async fn impersonation_url_for_method(
221        &self,
222        creds: &Credentials,
223        principal: &str,
224        method: &str,
225    ) -> String {
226        let universe_domain = crate::universe_domain::resolve(creds).await;
227        let endpoint = match &self.endpoint {
228            Some(endpoint) => endpoint.to_string(),
229            None => format!("https://iamcredentials.{}", universe_domain),
230        };
231        format!(
232            "{}/v1/projects/-/serviceAccounts/{}:{}",
233            endpoint, principal, method
234        )
235    }
236
237    pub(crate) fn client_email(self) -> BuildResult<String> {
238        match self.kind {
239            ImpersonationUrlKind::TargetPrincipal(client_email) => Ok(client_email),
240            ImpersonationUrlKind::Exact(url) => extract_client_email(&url),
241        }
242    }
243}
244
245impl Builder {
246    /// Creates a new builder using `impersonated_service_account` JSON value.
247    ///
248    /// The `impersonated_service_account` JSON is typically generated using
249    /// a [gcloud command] for [application default login].
250    ///
251    /// [gcloud command]: https://cloud.google.com/docs/authentication/use-service-account-impersonation#adc
252    /// [application default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
253    pub fn new(impersonated_credential: Value) -> Self {
254        Self {
255            source: BuilderSource::FromJson(impersonated_credential),
256            service_account_impersonation_url: None,
257            delegates: None,
258            scopes: None,
259            quota_project_id: None,
260            universe_domain: None,
261            lifetime: None,
262            retry_builder: RetryTokenProviderBuilder::default(),
263            iam_endpoint_override: None,
264            is_access_boundary_enabled: true,
265        }
266    }
267
268    /// Creates a new builder with a source credentials object.
269    ///
270    /// # Example
271    /// ```
272    /// # use google_cloud_auth::credentials::impersonated;
273    /// # use google_cloud_auth::credentials::user_account;
274    /// # use serde_json::json;
275    /// #
276    /// # async fn sample() -> anyhow::Result<()> {
277    /// let source_credentials = user_account::Builder::new(json!({ /* add details here */ })).build()?;
278    ///
279    /// let creds = impersonated::Builder::from_source_credentials(source_credentials)
280    ///     .with_target_principal("test-principal")
281    ///     .build()?;
282    /// # Ok(()) }
283    /// ```
284    pub fn from_source_credentials(source_credentials: Credentials) -> Self {
285        Self {
286            source: BuilderSource::FromCredentials(source_credentials),
287            service_account_impersonation_url: None,
288            delegates: None,
289            scopes: None,
290            quota_project_id: None,
291            universe_domain: None,
292            lifetime: None,
293            retry_builder: RetryTokenProviderBuilder::default(),
294            iam_endpoint_override: None,
295            is_access_boundary_enabled: true,
296        }
297    }
298
299    /// Sets the target principal. This is required when using `from_source_credentials`.
300    /// Target principal is the email of the service account to impersonate.
301    ///
302    /// # Example
303    /// ```
304    /// # use google_cloud_auth::credentials::impersonated;
305    /// # use serde_json::json;
306    /// #
307    /// # async fn sample() -> anyhow::Result<()> {
308    /// let impersonated_credential = json!({ /* add details here */ });
309    ///
310    /// let creds = impersonated::Builder::new(impersonated_credential.into())
311    ///     .with_target_principal("test-principal")
312    ///     .build()?;
313    /// # Ok(()) }
314    /// ```
315    pub fn with_target_principal<S: Into<String>>(mut self, target_principal: S) -> Self {
316        self.service_account_impersonation_url =
317            Some(ImpersonationUrl::target_principal(target_principal.into()));
318        self
319    }
320
321    /// Sets the chain of delegates.
322    ///
323    /// # Example
324    /// ```
325    /// # use google_cloud_auth::credentials::impersonated;
326    /// # use serde_json::json;
327    /// #
328    /// # async fn sample() -> anyhow::Result<()> {
329    /// let impersonated_credential = json!({ /* add details here */ });
330    ///
331    /// let creds = impersonated::Builder::new(impersonated_credential.into())
332    ///     .with_delegates(["delegate1", "delegate2"])
333    ///     .build()?;
334    /// # Ok(()) }
335    /// ```
336    pub fn with_delegates<I, S>(mut self, delegates: I) -> Self
337    where
338        I: IntoIterator<Item = S>,
339        S: Into<String>,
340    {
341        self.delegates = Some(delegates.into_iter().map(|s| s.into()).collect());
342        self
343    }
344
345    /// Sets the [scopes] for these credentials.
346    ///
347    /// Any value set here overrides a `scopes` value from the
348    /// input `impersonated_service_account` JSON.
349    ///
350    /// By default `https://www.googleapis.com/auth/cloud-platform` scope is used.
351    ///
352    /// # Example
353    /// ```
354    /// # use google_cloud_auth::credentials::impersonated;
355    /// # use serde_json::json;
356    /// #
357    /// # async fn sample() -> anyhow::Result<()> {
358    /// let impersonated_credential = json!({ /* add details here */ });
359    ///
360    /// let creds = impersonated::Builder::new(impersonated_credential.into())
361    ///     .with_scopes(["https://www.googleapis.com/auth/pubsub"])
362    ///     .build()?;
363    /// # Ok(()) }
364    /// ```
365    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
366    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
367    where
368        I: IntoIterator<Item = S>,
369        S: Into<String>,
370    {
371        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
372        self
373    }
374
375    /// Sets the [quota project] for these credentials.
376    ///
377    /// For some services, you can use an account in
378    /// one project for authentication and authorization, and charge
379    /// the usage to a different project. This requires that the
380    /// target service account has `serviceusage.services.use`
381    /// permissions on the quota project.
382    ///
383    /// Any value set here overrides a `quota_project_id` value from the
384    /// input `impersonated_service_account` JSON.
385    ///
386    /// # Example
387    /// ```
388    /// # use google_cloud_auth::credentials::impersonated;
389    /// # use serde_json::json;
390    /// #
391    /// # async fn sample() -> anyhow::Result<()> {
392    /// let impersonated_credential = json!({ /* add details here */ });
393    ///
394    /// let creds = impersonated::Builder::new(impersonated_credential.into())
395    ///     .with_quota_project_id("my-project")
396    ///     .build()?;
397    /// # Ok(()) }
398    /// ```
399    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
400    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
401        self.quota_project_id = Some(quota_project_id.into());
402        self
403    }
404
405    /// Sets the Google Cloud universe domain for these credentials.
406    ///
407    /// The universe domain is the default service domain for a given Cloud universe.
408    ///
409    /// # Example
410    /// ```
411    /// # use google_cloud_auth::credentials::impersonated::Builder;
412    /// # use serde_json::json;
413    /// # async fn sample() -> anyhow::Result<()> {
414    /// # let config = json!({
415    /// #     "type": "impersonated_service_account",
416    /// #     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
417    /// #     "source_credentials": { "type": "authorized_user", "client_id": "id", "client_secret": "secret", "refresh_token": "token" }
418    /// # });
419    /// let credentials = Builder::new(config)
420    ///     .with_universe_domain("googleapis.com")
421    ///     .build()?;
422    /// # Ok(()) }
423    /// ```
424    ///
425    /// [universe domain]: https://cloud.google.com/docs/authentication/universe-domain
426    pub fn with_universe_domain<S: Into<String>>(mut self, universe_domain: S) -> Self {
427        self.universe_domain = Some(universe_domain.into());
428        self
429    }
430
431    /// Sets the lifetime for the impersonated credentials.
432    ///
433    /// # Example
434    /// ```
435    /// # use google_cloud_auth::credentials::impersonated;
436    /// # use serde_json::json;
437    /// # use std::time::Duration;
438    /// #
439    /// # async fn sample() -> anyhow::Result<()> {
440    /// let impersonated_credential = json!({ /* add details here */ });
441    ///
442    /// let creds = impersonated::Builder::new(impersonated_credential.into())
443    ///     .with_lifetime(Duration::from_secs(500))
444    ///     .build()?;
445    /// # Ok(()) }
446    /// ```
447    pub fn with_lifetime(mut self, lifetime: Duration) -> Self {
448        self.lifetime = Some(lifetime);
449        self
450    }
451
452    /// Configure the retry policy for fetching tokens.
453    ///
454    /// The retry policy controls how to handle retries, and sets limits on
455    /// the number of attempts or the total time spent retrying.
456    ///
457    /// ```
458    /// # use google_cloud_auth::credentials::impersonated::Builder;
459    /// # use serde_json::json;
460    /// # async fn sample() -> anyhow::Result<()> {
461    /// use google_cloud_gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
462    /// let impersonated_credential = json!({ /* add details here */ });
463    /// let credentials = Builder::new(impersonated_credential.into())
464    ///     .with_retry_policy(AlwaysRetry.with_attempt_limit(3))
465    ///     .build()?;
466    /// # Ok(()) }
467    /// ```
468    pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
469        self.retry_builder = self.retry_builder.with_retry_policy(v.into());
470        self
471    }
472
473    /// Configure the retry backoff policy.
474    ///
475    /// The backoff policy controls how long to wait in between retry attempts.
476    ///
477    /// ```
478    /// # use google_cloud_auth::credentials::impersonated::Builder;
479    /// # use serde_json::json;
480    /// # use std::time::Duration;
481    /// # async fn sample() -> anyhow::Result<()> {
482    /// use google_cloud_gax::exponential_backoff::ExponentialBackoff;
483    /// let policy = ExponentialBackoff::default();
484    /// let impersonated_credential = json!({ /* add details here */ });
485    /// let credentials = Builder::new(impersonated_credential.into())
486    ///     .with_backoff_policy(policy)
487    ///     .build()?;
488    /// # Ok(()) }
489    /// ```
490    pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
491        self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
492        self
493    }
494
495    /// Configure the retry throttler.
496    ///
497    /// Advanced applications may want to configure a retry throttler to
498    /// [Address Cascading Failures] and when [Handling Overload] conditions.
499    /// The authentication library throttles its retry loop, using a policy to
500    /// control the throttling algorithm. Use this method to fine tune or
501    /// customize the default retry throttler.
502    ///
503    /// [Handling Overload]: https://sre.google/sre-book/handling-overload/
504    /// [Address Cascading Failures]: https://sre.google/sre-book/addressing-cascading-failures/
505    ///
506    /// ```
507    /// # use google_cloud_auth::credentials::impersonated::Builder;
508    /// # use serde_json::json;
509    /// # async fn sample() -> anyhow::Result<()> {
510    /// use google_cloud_gax::retry_throttler::AdaptiveThrottler;
511    /// let impersonated_credential = json!({ /* add details here */ });
512    /// let credentials = Builder::new(impersonated_credential.into())
513    ///     .with_retry_throttler(AdaptiveThrottler::default())
514    ///     .build()?;
515    /// # Ok(()) }
516    /// ```
517    pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
518        self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
519        self
520    }
521
522    /// Returns a [Credentials] instance with the configured settings.
523    ///
524    /// # Errors
525    ///
526    /// Returns a [BuilderError] for one of the following cases:
527    /// - If the `impersonated_service_account` provided to [`Builder::new`] cannot
528    ///   be successfully deserialized into the expected format. This typically happens
529    ///   if the JSON value is malformed or missing required fields. For more information,
530    ///   on how to generate `impersonated_service_account` json, consult the relevant
531    ///   section in the [application-default credentials] guide.
532    /// - If the `impersonated_service_account` provided to [`Builder::new`] has a
533    ///   `source_credentials` of `impersonated_service_account` type.
534    /// - If `service_account_impersonation_url` is not provided after initializing
535    ///   the builder with [`Builder::from_source_credentials`].
536    ///
537    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
538    pub fn build(self) -> BuildResult<Credentials> {
539        Ok(self.build_credentials()?.into())
540    }
541
542    /// Returns an [AccessTokenCredentials] instance with the configured settings.
543    ///
544    /// # Example
545    ///
546    /// ```
547    /// # use google_cloud_auth::credentials::impersonated::Builder;
548    /// # use google_cloud_auth::credentials::{AccessTokenCredentials, AccessTokenCredentialsProvider};
549    /// # use serde_json::json;
550    /// # async fn sample() -> anyhow::Result<()> {
551    /// let impersonated_credential = json!({
552    ///     "type": "impersonated_service_account",
553    ///     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
554    ///     "source_credentials": {
555    ///         "type": "authorized_user",
556    ///         "client_id": "test-client-id",
557    ///         "client_secret": "test-client-secret",
558    ///         "refresh_token": "test-refresh-token"
559    ///     }
560    /// });
561    /// let credentials: AccessTokenCredentials = Builder::new(impersonated_credential.into())
562    ///     .build_access_token_credentials()?;
563    /// let access_token = credentials.access_token().await?;
564    /// println!("Token: {}", access_token.token);
565    /// # Ok(()) }
566    /// ```
567    ///
568    /// # Errors
569    ///
570    /// Returns a [BuilderError] for one of the following cases:
571    /// - If the `impersonated_service_account` provided to [`Builder::new`] cannot
572    ///   be successfully deserialized into the expected format. This typically happens
573    ///   if the JSON value is malformed or missing required fields. For more information,
574    ///   on how to generate `impersonated_service_account` json, consult the relevant
575    ///   section in the [application-default credentials] guide.
576    /// - If the `impersonated_service_account` provided to [`Builder::new`] has a
577    ///   `source_credentials` of `impersonated_service_account` type.
578    /// - If `service_account_impersonation_url` is not provided after initializing
579    ///   the builder with [`Builder::from_source_credentials`].
580    ///
581    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
582    pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
583        Ok(self.build_credentials()?.into())
584    }
585
586    fn build_credentials(
587        self,
588    ) -> BuildResult<CredentialsWithAccessBoundary<ImpersonatedServiceAccount<TokenCache>>> {
589        let is_access_boundary_enabled = self.is_access_boundary_enabled;
590        let impersonation_url = self.resolve_impersonation_url()?;
591        let client_email = impersonation_url.client_email()?;
592        let iam_endpoint_override = self.iam_endpoint_override.clone();
593        let universe_domain_override = self.universe_domain.clone();
594        let (token_provider, quota_project_id, source_credentials) = self.build_components()?;
595        let access_boundary_url = crate::access_boundary::service_account_lookup_url(
596            &client_email,
597            iam_endpoint_override.as_deref(),
598        );
599        let creds = ImpersonatedServiceAccount {
600            token_provider: TokenCache::new(token_provider),
601            quota_project_id,
602            universe_domain_override,
603            source_credentials,
604        };
605
606        if !is_access_boundary_enabled {
607            return Ok(CredentialsWithAccessBoundary::new_no_op(creds));
608        }
609
610        Ok(CredentialsWithAccessBoundary::new(
611            creds,
612            Some(access_boundary_url),
613        ))
614    }
615
616    /// Returns a [crate::signer::Signer] instance with the configured settings.
617    ///
618    /// The returned [crate::signer::Signer] uses the [IAM signBlob API] to sign content. This API
619    /// requires a network request for each signing operation.
620    ///
621    /// Note that if the source credentials are a service account, the signer
622    /// might perform signing locally. For more details on local signing, see
623    /// [crate::credentials::service_account::Builder::build_signer].
624    ///
625    /// # Example
626    ///
627    /// ```
628    /// # use google_cloud_auth::credentials::impersonated::Builder;
629    /// # use google_cloud_auth::signer::Signer;
630    /// # use serde_json::json;
631    /// # async fn sample() -> anyhow::Result<()> {
632    /// let impersonated_credential = json!({
633    ///     "type": "impersonated_service_account",
634    ///     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
635    ///     "source_credentials": {
636    ///         "type": "authorized_user",
637    ///         "client_id": "test-client-id",
638    ///         "client_secret": "test-client-secret",
639    ///         "refresh_token": "test-refresh-token"
640    ///     }
641    /// });
642    ///
643    /// let signer: Signer = Builder::new(impersonated_credential).build_signer()?;
644    /// # Ok(()) }
645    /// ```
646    ///
647    /// [IAM signBlob API]: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob
648    pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
649        let iam_endpoint = self.iam_endpoint_override.clone();
650        let source = self.source.clone();
651        if let BuilderSource::FromJson(json) = source {
652            // try to build service account signer from json
653            let signer = build_signer_from_json(json.clone())?;
654            if let Some(signer) = signer {
655                return Ok(signer);
656            }
657        }
658        let impersonation_url = self.resolve_impersonation_url()?;
659        let client_email = impersonation_url.client_email()?;
660        let creds = self.build()?;
661        let signer = crate::signer::iam::IamSigner::new(client_email, creds, iam_endpoint);
662        Ok(crate::signer::Signer {
663            inner: Arc::new(signer),
664        })
665    }
666
667    fn build_components(
668        self,
669    ) -> BuildResult<(
670        TokenProviderWithRetry<ImpersonatedTokenProvider>,
671        Option<String>,
672        Credentials,
673    )> {
674        let components = match self.source {
675            BuilderSource::FromJson(json) => build_components_from_json(json)?,
676            BuilderSource::FromCredentials(source_credentials) => {
677                build_components_from_credentials(
678                    source_credentials,
679                    self.service_account_impersonation_url,
680                )?
681            }
682        };
683
684        let scopes = self
685            .scopes
686            .or(components.scopes)
687            .unwrap_or_else(|| vec![DEFAULT_SCOPE.to_string()]);
688
689        let quota_project_id = self.quota_project_id.or(components.quota_project_id);
690        let delegates = self.delegates.or(components.delegates);
691
692        let source_credentials = components.source_credentials;
693        let token_provider = ImpersonatedTokenProvider {
694            source_credentials: source_credentials.clone(),
695            service_account_impersonation_url: components.service_account_impersonation_url,
696            delegates,
697            scopes,
698            lifetime: self.lifetime.unwrap_or(DEFAULT_LIFETIME),
699        };
700        let token_provider = self.retry_builder.build(token_provider);
701        Ok((token_provider, quota_project_id, source_credentials))
702    }
703
704    fn resolve_impersonation_url(&self) -> BuildResult<ImpersonationUrl> {
705        match self.source.clone() {
706            BuilderSource::FromJson(json) => {
707                let config = config_from_json(json)?;
708                Ok(ImpersonationUrl::exact(config.service_account_impersonation_url))
709            }
710            BuilderSource::FromCredentials(_) => {
711                self.service_account_impersonation_url.clone().ok_or_else(|| {
712                    BuilderError::parsing(
713                        "`service_account_impersonation_url` is required when building from source credentials",
714                    )
715                })
716            }
717        }
718    }
719}
720
721pub(crate) struct ImpersonatedCredentialComponents {
722    pub(crate) source_credentials: Credentials,
723    pub(crate) service_account_impersonation_url: ImpersonationUrl,
724    pub(crate) delegates: Option<Vec<String>>,
725    pub(crate) quota_project_id: Option<String>,
726    pub(crate) scopes: Option<Vec<String>>,
727}
728
729fn config_from_json(json: Value) -> BuildResult<ImpersonatedConfig> {
730    serde_json::from_value::<ImpersonatedConfig>(json).map_err(BuilderError::parsing)
731}
732
733pub(crate) fn build_components_from_json(
734    json: Value,
735) -> BuildResult<ImpersonatedCredentialComponents> {
736    let config = config_from_json(json)?;
737
738    let source_credential_type = extract_credential_type(&config.source_credentials)?;
739    if source_credential_type == "impersonated_service_account" {
740        return Err(BuilderError::parsing(
741            "source credential of type `impersonated_service_account` is not supported. \
742                        Use the `delegates` field to specify a delegation chain.",
743        ));
744    }
745
746    // Do not pass along scopes and quota project to the source credentials.
747    // It is not necessary that the source and target credentials have same permissions on
748    // the quota project and they typically need different scopes.
749    // If user does want some specific scopes or quota, they can build using the
750    // from_source_credentials method.
751    let source_credentials =
752        build_credentials(Some(config.source_credentials), None, None, None)?.into();
753
754    Ok(ImpersonatedCredentialComponents {
755        source_credentials,
756        service_account_impersonation_url: ImpersonationUrl::exact(
757            config.service_account_impersonation_url,
758        ),
759        delegates: config.delegates,
760        quota_project_id: config.quota_project_id,
761        scopes: config.scopes,
762    })
763}
764
765// Check if the source credentials is a service account and if so, build a signer from it.
766// Service account signers can do signing locally, which is more efficient than using the
767// remote/API based signer.
768fn build_signer_from_json(json: Value) -> BuildResult<Option<crate::signer::Signer>> {
769    use crate::credentials::service_account::ServiceAccountKey;
770    use crate::signer::service_account::ServiceAccountSigner;
771
772    let config = config_from_json(json)?;
773
774    let client_email = extract_client_email(&config.service_account_impersonation_url)?;
775    let source_credential_type = extract_credential_type(&config.source_credentials)?;
776    if source_credential_type == "service_account" {
777        let service_account_key =
778            serde_json::from_value::<ServiceAccountKey>(config.source_credentials)
779                .map_err(BuilderError::parsing)?;
780        let signing_provider = ServiceAccountSigner::from_impersonated_service_account(
781            service_account_key,
782            client_email,
783        );
784        let signer = crate::signer::Signer {
785            inner: Arc::new(signing_provider),
786        };
787        return Ok(Some(signer));
788    }
789    Ok(None)
790}
791
792fn extract_client_email(service_account_impersonation_url: &str) -> BuildResult<String> {
793    let mut parts = service_account_impersonation_url.split("/serviceAccounts/");
794    match (parts.nth(1), parts.next()) {
795        (Some(email), None) => Ok(email.trim_end_matches(":generateAccessToken").to_string()),
796        _ => Err(BuilderError::parsing(
797            "invalid service account impersonation URL",
798        )),
799    }
800}
801
802pub(crate) fn build_components_from_credentials(
803    source_credentials: Credentials,
804    impersonation_url: Option<ImpersonationUrl>,
805) -> BuildResult<ImpersonatedCredentialComponents> {
806    let url = impersonation_url.ok_or_else(|| {
807        BuilderError::parsing(
808            "`target_principal` is required when building from source credentials",
809        )
810    })?;
811    Ok(ImpersonatedCredentialComponents {
812        source_credentials,
813        service_account_impersonation_url: url,
814        delegates: None,
815        quota_project_id: None,
816        scopes: None,
817    })
818}
819
820#[derive(serde::Deserialize, Debug, PartialEq)]
821struct ImpersonatedConfig {
822    service_account_impersonation_url: String,
823    source_credentials: Value,
824    delegates: Option<Vec<String>>,
825    quota_project_id: Option<String>,
826    scopes: Option<Vec<String>>,
827    universe_domain: Option<String>,
828}
829
830#[derive(Debug)]
831struct ImpersonatedServiceAccount<T>
832where
833    T: CachedTokenProvider,
834{
835    token_provider: T,
836    quota_project_id: Option<String>,
837    universe_domain_override: Option<String>,
838    source_credentials: Credentials,
839}
840
841#[async_trait::async_trait]
842impl<T> CredentialsProvider for ImpersonatedServiceAccount<T>
843where
844    T: CachedTokenProvider,
845{
846    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
847        let token = self.token_provider.token(extensions).await?;
848
849        AuthHeadersBuilder::new(&token)
850            .maybe_quota_project_id(self.quota_project_id.as_deref())
851            .build()
852    }
853
854    async fn universe_domain(&self) -> Option<String> {
855        if let Some(universe_domain) = &self.universe_domain_override {
856            return Some(universe_domain.clone());
857        }
858        self.source_credentials.universe_domain().await
859    }
860}
861
862#[async_trait::async_trait]
863impl<T> AccessTokenCredentialsProvider for ImpersonatedServiceAccount<T>
864where
865    T: CachedTokenProvider,
866{
867    async fn access_token(&self) -> Result<AccessToken> {
868        let token = self.token_provider.token(Extensions::new()).await?;
869        token.into()
870    }
871}
872
873struct ImpersonatedTokenProvider {
874    source_credentials: Credentials,
875    service_account_impersonation_url: ImpersonationUrl,
876    delegates: Option<Vec<String>>,
877    scopes: Vec<String>,
878    lifetime: Duration,
879}
880
881impl Debug for ImpersonatedTokenProvider {
882    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
883        f.debug_struct("ImpersonatedTokenProvider")
884            .field("source_credentials", &self.source_credentials)
885            .field(
886                "service_account_impersonation_url",
887                &self.service_account_impersonation_url,
888            )
889            .field("delegates", &self.delegates)
890            .field("scopes", &self.scopes)
891            .field("lifetime", &self.lifetime)
892            .finish()
893    }
894}
895
896#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
897struct GenerateAccessTokenRequest {
898    #[serde(skip_serializing_if = "Option::is_none")]
899    delegates: Option<Vec<String>>,
900    scope: Vec<String>,
901    lifetime: String,
902}
903
904pub(crate) async fn generate_access_token(
905    source_headers: HeaderMap,
906    delegates: Option<Vec<String>>,
907    scopes: Vec<String>,
908    lifetime: Duration,
909    service_account_impersonation_url: &str,
910) -> Result<Token> {
911    GenerateAccessTokenClient::new(
912        source_headers,
913        delegates,
914        scopes,
915        lifetime,
916        service_account_impersonation_url.to_string(),
917    )
918    .fetch()
919    .await
920}
921
922async fn generate_access_token_call(
923    client: &Client,
924    url: &str,
925    source_headers: HeaderMap,
926    body: &GenerateAccessTokenRequest,
927) -> GaxResult<reqwest::Response> {
928    let response = client
929        .post(url)
930        .header("Content-Type", "application/json")
931        .header(
932            headers_util::X_GOOG_API_CLIENT,
933            metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
934        )
935        .headers(source_headers)
936        .json(body)
937        .send()
938        .await
939        .map_err(GaxError::io)?;
940
941    let status = response.status();
942    if !status.is_success() {
943        let err_headers = response.headers().clone();
944        let err_payload = response
945            .bytes()
946            .await
947            .map_err(|e| GaxError::transport(err_headers.clone(), e))?;
948        return Err(GaxError::http(status.as_u16(), err_headers, err_payload));
949    }
950
951    Ok(response)
952}
953
954#[derive(Debug)]
955struct GenerateAccessTokenClient {
956    source_headers: HeaderMap,
957    delegates: Option<Vec<String>>,
958    scopes: Vec<String>,
959    lifetime: Duration,
960    url: String,
961    retry_policy: Arc<dyn RetryPolicy>,
962    backoff_policy: Arc<dyn BackoffPolicy>,
963}
964
965impl GenerateAccessTokenClient {
966    fn new(
967        source_headers: HeaderMap,
968        delegates: Option<Vec<String>>,
969        scopes: Vec<String>,
970        lifetime: Duration,
971        url: String,
972    ) -> Self {
973        let retry_policy = Aip194Strict
974            .continue_on_too_many_requests()
975            .continue_on_client_timeout()
976            .with_time_limit(Duration::from_secs(60));
977        let backoff_policy = ExponentialBackoff::default();
978
979        Self {
980            source_headers,
981            delegates,
982            scopes,
983            lifetime,
984            url,
985            retry_policy: Arc::new(retry_policy),
986            backoff_policy: Arc::new(backoff_policy),
987        }
988    }
989
990    async fn fetch(self) -> Result<Token> {
991        let client = Client::new();
992        let body = GenerateAccessTokenRequest {
993            delegates: self.delegates,
994            scope: self.scopes,
995            lifetime: format!("{}s", self.lifetime.as_secs_f64()),
996        };
997
998        let sleep = async |d| tokio::time::sleep(d).await;
999        let retry_throttler: RetryThrottlerArg = AdaptiveThrottler::default().into();
1000        let retry_throttler: SharedRetryThrottler = retry_throttler.into();
1001        let url = self.url;
1002        let source_headers = self.source_headers;
1003
1004        let response = retry_loop(
1005            async move |d| {
1006                let attempt =
1007                    generate_access_token_call(&client, &url, source_headers.clone(), &body);
1008                let max_time_limit = Duration::from_secs(10);
1009                let time_limit = d.map_or(max_time_limit, |d| d.min(max_time_limit));
1010                match tokio::time::timeout(time_limit, attempt).await {
1011                    Ok(r) => r,
1012                    Err(e) => Err(GaxError::timeout(e)),
1013                }
1014            },
1015            sleep,
1016            true, // generate_access_token is idempotent
1017            retry_throttler,
1018            self.retry_policy.clone(),
1019            self.backoff_policy.clone(),
1020        )
1021        .await
1022        .map_err(|e| errors::from_gax_error(e, MSG))?;
1023
1024        let token_response = response
1025            .json::<GenerateAccessTokenResponse>()
1026            .await
1027            .map_err(|e| {
1028                let retryable = !e.is_decode();
1029                CredentialsError::from_source(retryable, e)
1030            })?;
1031
1032        let parsed_dt = OffsetDateTime::parse(
1033            &token_response.expire_time,
1034            &time::format_description::well_known::Rfc3339,
1035        )
1036        .map_err(errors::non_retryable)?;
1037
1038        let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
1039        let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
1040
1041        let token = Token {
1042            token: token_response.access_token,
1043            token_type: "Bearer".to_string(),
1044            expires_at: Some(expires_at),
1045            metadata: None,
1046        };
1047        Ok(token)
1048    }
1049}
1050
1051#[async_trait]
1052impl TokenProvider for ImpersonatedTokenProvider {
1053    async fn token(&self) -> Result<Token> {
1054        let source_headers = self.source_credentials.headers(Extensions::new()).await?;
1055        let source_headers = match source_headers {
1056            CacheableResource::New { data, .. } => data,
1057            CacheableResource::NotModified => {
1058                unreachable!("requested source credentials without a caching etag")
1059            }
1060        };
1061
1062        // We resolve the URL on every token call because fetching the universe domain
1063        // is async and must be done here rather than in the builder.
1064        // Since `token()` takes `&self`, we cannot mutate `self` to cache the URL
1065        // without using a lock for inner mutability, which is worse than just
1066        // building the URL string for each request.
1067        let url = self
1068            .service_account_impersonation_url
1069            .access_token_url(&self.source_credentials)
1070            .await;
1071
1072        generate_access_token(
1073            source_headers,
1074            self.delegates.clone(),
1075            self.scopes.clone(),
1076            self.lifetime,
1077            &url,
1078        )
1079        .await
1080    }
1081}
1082
1083#[derive(serde::Deserialize)]
1084struct GenerateAccessTokenResponse {
1085    #[serde(rename = "accessToken")]
1086    access_token: String,
1087    #[serde(rename = "expireTime")]
1088    expire_time: String,
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use super::*;
1094    use crate::credentials::service_account::ServiceAccountKey;
1095    use crate::credentials::tests::MockCredentials;
1096    use crate::credentials::tests::PKCS8_PK;
1097    use crate::credentials::tests::{
1098        find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
1099        get_mock_retry_throttler,
1100    };
1101    use crate::errors::CredentialsError;
1102    use crate::universe_domain::is_default_universe_domain;
1103    use base64::{Engine, prelude::BASE64_STANDARD};
1104    use httptest::cycle;
1105    use httptest::{Expectation, Server, matchers::*, responders::*};
1106    use serde_json::Value;
1107    use serde_json::json;
1108    use serial_test::parallel;
1109    use test_case::test_case;
1110
1111    type TestResult = anyhow::Result<()>;
1112
1113    impl Builder {
1114        fn maybe_iam_endpoint_override(mut self, iam_endpoint_override: Option<String>) -> Self {
1115            self.iam_endpoint_override = iam_endpoint_override;
1116            self
1117        }
1118
1119        fn without_access_boundary(mut self) -> Self {
1120            self.is_access_boundary_enabled = false;
1121            self
1122        }
1123
1124        fn with_impersonation_endpoint(mut self, endpoint: &str) -> Self {
1125            self.service_account_impersonation_url = self
1126                .service_account_impersonation_url
1127                .map(|u| u.with_endpoint(endpoint));
1128            self
1129        }
1130    }
1131
1132    impl ImpersonationUrl {
1133        pub(crate) fn with_endpoint(mut self, endpoint: &str) -> Self {
1134            self.endpoint = Some(endpoint.to_string());
1135            self
1136        }
1137    }
1138
1139    #[tokio::test]
1140    #[parallel]
1141    async fn test_generate_access_token_client_retry_success() -> TestResult {
1142        let server = Server::run();
1143        let impersonation_path =
1144            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1145        server.expect(
1146            Expectation::matching(request::method_path("POST", impersonation_path))
1147                .times(2)
1148                .respond_with(cycle![
1149                    status_code(503).body("try-again"),
1150                    json_encoded(json!({
1151                        "accessToken": "impersonated-token-success",
1152                        "expireTime": "2030-01-01T00:00:00Z",
1153                    })),
1154                ]),
1155        );
1156
1157        let url = server.url(impersonation_path).to_string();
1158        let mut client = GenerateAccessTokenClient::new(
1159            HeaderMap::new(),
1160            None,
1161            vec!["scope1".to_string()],
1162            Duration::from_secs(3600),
1163            url,
1164        );
1165        client.retry_policy = Arc::new(get_mock_auth_retry_policy(3));
1166        client.backoff_policy = Arc::new(get_mock_backoff_policy());
1167
1168        let token = client.fetch().await?;
1169        assert_eq!(token.token, "impersonated-token-success");
1170        Ok(())
1171    }
1172
1173    #[tokio::test]
1174    #[parallel]
1175    async fn test_generate_access_token_client_retry_exhausted() {
1176        let server = Server::run();
1177        let impersonation_path =
1178            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1179        server.expect(
1180            Expectation::matching(request::method_path("POST", impersonation_path))
1181                .times(3)
1182                .respond_with(status_code(503)),
1183        );
1184
1185        let url = server.url(impersonation_path).to_string();
1186        let mut client = GenerateAccessTokenClient::new(
1187            HeaderMap::new(),
1188            None,
1189            vec!["scope1".to_string()],
1190            Duration::from_secs(3600),
1191            url,
1192        );
1193        client.retry_policy = Arc::new(get_mock_auth_retry_policy(3));
1194        client.backoff_policy = Arc::new(get_mock_backoff_policy());
1195
1196        let err = client.fetch().await.unwrap_err();
1197        assert!(err.is_transient(), "{err:?}");
1198    }
1199
1200    #[tokio::test]
1201    #[parallel]
1202    async fn test_generate_access_token_success() -> TestResult {
1203        let server = Server::run();
1204        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1205            .format(&time::format_description::well_known::Rfc3339)
1206            .unwrap();
1207        server.expect(
1208            Expectation::matching(all_of![
1209                request::method_path(
1210                    "POST",
1211                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1212                ),
1213                request::headers(contains(("authorization", "Bearer test-token"))),
1214            ])
1215            .respond_with(json_encoded(json!({
1216                "accessToken": "test-impersonated-token",
1217                "expireTime": expire_time
1218            }))),
1219        );
1220
1221        let mut headers = HeaderMap::new();
1222        headers.insert("authorization", "Bearer test-token".parse().unwrap());
1223        let token = generate_access_token(
1224            headers,
1225            None,
1226            vec!["scope".to_string()],
1227            DEFAULT_LIFETIME,
1228            &server
1229                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1230                .to_string(),
1231        )
1232        .await?;
1233
1234        assert_eq!(token.token, "test-impersonated-token");
1235        Ok(())
1236    }
1237
1238    #[tokio::test]
1239    #[parallel]
1240    async fn test_generate_access_token_403() -> TestResult {
1241        let server = Server::run();
1242        server.expect(
1243            Expectation::matching(all_of![
1244                request::method_path(
1245                    "POST",
1246                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1247                ),
1248                request::headers(contains(("authorization", "Bearer test-token"))),
1249            ])
1250            .respond_with(status_code(403)),
1251        );
1252
1253        let mut headers = HeaderMap::new();
1254        headers.insert("authorization", "Bearer test-token".parse().unwrap());
1255        let err = generate_access_token(
1256            headers,
1257            None,
1258            vec!["scope".to_string()],
1259            DEFAULT_LIFETIME,
1260            &server
1261                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1262                .to_string(),
1263        )
1264        .await
1265        .unwrap_err();
1266
1267        assert!(!err.is_transient());
1268        Ok(())
1269    }
1270
1271    #[tokio::test]
1272    #[parallel]
1273    async fn test_generate_access_token_no_auth_header() -> TestResult {
1274        let server = Server::run();
1275        server.expect(
1276            Expectation::matching(request::method_path(
1277                "POST",
1278                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1279            ))
1280            .respond_with(status_code(401)),
1281        );
1282
1283        let err = generate_access_token(
1284            HeaderMap::new(),
1285            None,
1286            vec!["scope".to_string()],
1287            DEFAULT_LIFETIME,
1288            &server
1289                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1290                .to_string(),
1291        )
1292        .await
1293        .unwrap_err();
1294
1295        assert!(!err.is_transient());
1296        Ok(())
1297    }
1298
1299    #[tokio::test]
1300    #[parallel]
1301    async fn test_impersonated_service_account() -> TestResult {
1302        let server = Server::run();
1303        server.expect(
1304            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1305                json_encoded(json!({
1306                    "access_token": "test-user-account-token",
1307                    "expires_in": 3600,
1308                    "token_type": "Bearer",
1309                })),
1310            ),
1311        );
1312        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1313            .format(&time::format_description::well_known::Rfc3339)
1314            .unwrap();
1315        server.expect(
1316            Expectation::matching(all_of![
1317                request::method_path(
1318                    "POST",
1319                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1320                ),
1321                request::headers(contains((
1322                    "authorization",
1323                    "Bearer test-user-account-token"
1324                ))),
1325                request::body(json_decoded(eq(json!({
1326                    "scope": ["scope1", "scope2"],
1327                    "lifetime": "3600s"
1328                }))))
1329            ])
1330            .respond_with(json_encoded(json!({
1331                "accessToken": "test-impersonated-token",
1332                "expireTime": expire_time
1333            }))),
1334        );
1335
1336        let impersonated_credential = json!({
1337            "type": "impersonated_service_account",
1338            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1339            "source_credentials": {
1340                "type": "authorized_user",
1341                "client_id": "test-client-id",
1342                "client_secret": "test-client-secret",
1343                "refresh_token": "test-refresh-token",
1344                "token_uri": server.url("/token").to_string()
1345            }
1346        });
1347        let (token_provider, _, _) = Builder::new(impersonated_credential)
1348            .with_scopes(vec!["scope1", "scope2"])
1349            .build_components()?;
1350
1351        let token = token_provider.token().await?;
1352        assert_eq!(token.token, "test-impersonated-token");
1353        assert_eq!(token.token_type, "Bearer");
1354
1355        Ok(())
1356    }
1357
1358    #[tokio::test]
1359    #[parallel]
1360    async fn test_impersonated_service_account_default_scope() -> TestResult {
1361        let server = Server::run();
1362        server.expect(
1363            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1364                json_encoded(json!({
1365                    "access_token": "test-user-account-token",
1366                    "expires_in": 3600,
1367                    "token_type": "Bearer",
1368                })),
1369            ),
1370        );
1371        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1372            .format(&time::format_description::well_known::Rfc3339)
1373            .unwrap();
1374        server.expect(
1375            Expectation::matching(all_of![
1376                request::method_path(
1377                    "POST",
1378                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1379                ),
1380                request::headers(contains((
1381                    "authorization",
1382                    "Bearer test-user-account-token"
1383                ))),
1384                request::body(json_decoded(eq(json!({
1385                    "scope": [DEFAULT_SCOPE],
1386                    "lifetime": "3600s"
1387                }))))
1388            ])
1389            .respond_with(json_encoded(json!({
1390                "accessToken": "test-impersonated-token",
1391                "expireTime": expire_time
1392            }))),
1393        );
1394
1395        let impersonated_credential = json!({
1396            "type": "impersonated_service_account",
1397            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1398            "source_credentials": {
1399                "type": "authorized_user",
1400                "client_id": "test-client-id",
1401                "client_secret": "test-client-secret",
1402                "refresh_token": "test-refresh-token",
1403                "token_uri": server.url("/token").to_string()
1404            }
1405        });
1406        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1407
1408        let token = token_provider.token().await?;
1409        assert_eq!(token.token, "test-impersonated-token");
1410        assert_eq!(token.token_type, "Bearer");
1411
1412        Ok(())
1413    }
1414
1415    #[tokio::test]
1416    #[parallel]
1417    async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
1418        let server = Server::run();
1419        server.expect(
1420            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1421                json_encoded(json!({
1422                    "access_token": "test-user-account-token",
1423                    "expires_in": 3600,
1424                    "token_type": "Bearer",
1425                })),
1426            ),
1427        );
1428        let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
1429            .format(&time::format_description::well_known::Rfc3339)
1430            .unwrap();
1431        server.expect(
1432            Expectation::matching(all_of![
1433                request::method_path(
1434                    "POST",
1435                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1436                ),
1437                request::headers(contains((
1438                    "authorization",
1439                    "Bearer test-user-account-token"
1440                ))),
1441                request::body(json_decoded(eq(json!({
1442                    "scope": ["scope1", "scope2"],
1443                    "lifetime": "3.5s"
1444                }))))
1445            ])
1446            .respond_with(json_encoded(json!({
1447                "accessToken": "test-impersonated-token",
1448                "expireTime": expire_time
1449            }))),
1450        );
1451
1452        let impersonated_credential = json!({
1453            "type": "impersonated_service_account",
1454            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1455            "source_credentials": {
1456                "type": "authorized_user",
1457                "client_id": "test-client-id",
1458                "client_secret": "test-client-secret",
1459                "refresh_token": "test-refresh-token",
1460                "token_uri": server.url("/token").to_string()
1461            }
1462        });
1463        let (token_provider, _, _) = Builder::new(impersonated_credential)
1464            .with_scopes(vec!["scope1", "scope2"])
1465            .with_lifetime(Duration::from_secs_f32(3.5))
1466            .build_components()?;
1467
1468        let token = token_provider.token().await?;
1469        assert_eq!(token.token, "test-impersonated-token");
1470
1471        Ok(())
1472    }
1473
1474    #[tokio::test]
1475    #[parallel]
1476    async fn test_with_delegates() -> TestResult {
1477        let server = Server::run();
1478        server.expect(
1479            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1480                json_encoded(json!({
1481                    "access_token": "test-user-account-token",
1482                    "expires_in": 3600,
1483                    "token_type": "Bearer",
1484                })),
1485            ),
1486        );
1487        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1488            .format(&time::format_description::well_known::Rfc3339)
1489            .unwrap();
1490        server.expect(
1491            Expectation::matching(all_of![
1492                request::method_path(
1493                    "POST",
1494                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1495                ),
1496                request::headers(contains((
1497                    "authorization",
1498                    "Bearer test-user-account-token"
1499                ))),
1500                request::body(json_decoded(eq(json!({
1501                    "scope": [DEFAULT_SCOPE],
1502                    "lifetime": "3600s",
1503                    "delegates": ["delegate1", "delegate2"]
1504                }))))
1505            ])
1506            .respond_with(json_encoded(json!({
1507                "accessToken": "test-impersonated-token",
1508                "expireTime": expire_time
1509            }))),
1510        );
1511
1512        let impersonated_credential = json!({
1513            "type": "impersonated_service_account",
1514            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1515            "source_credentials": {
1516                "type": "authorized_user",
1517                "client_id": "test-client-id",
1518                "client_secret": "test-client-secret",
1519                "refresh_token": "test-refresh-token",
1520                "token_uri": server.url("/token").to_string()
1521            }
1522        });
1523        let (token_provider, _, _) = Builder::new(impersonated_credential)
1524            .with_delegates(vec!["delegate1", "delegate2"])
1525            .build_components()?;
1526
1527        let token = token_provider.token().await?;
1528        assert_eq!(token.token, "test-impersonated-token");
1529        assert_eq!(token.token_type, "Bearer");
1530
1531        Ok(())
1532    }
1533
1534    #[tokio::test]
1535    #[parallel]
1536    async fn test_impersonated_service_account_fail() -> TestResult {
1537        let server = Server::run();
1538        server.expect(
1539            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1540                json_encoded(json!({
1541                    "access_token": "test-user-account-token",
1542                    "expires_in": 3600,
1543                    "token_type": "Bearer",
1544                })),
1545            ),
1546        );
1547        server.expect(
1548            Expectation::matching(request::method_path(
1549                "POST",
1550                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1551            ))
1552            .respond_with(status_code(500)),
1553        );
1554
1555        let impersonated_credential = json!({
1556            "type": "impersonated_service_account",
1557            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1558            "source_credentials": {
1559                "type": "authorized_user",
1560                "client_id": "test-client-id",
1561                "client_secret": "test-client-secret",
1562                "refresh_token": "test-refresh-token",
1563                "token_uri": server.url("/token").to_string()
1564            }
1565        });
1566        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1567
1568        let err = token_provider.token().await.unwrap_err();
1569        let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1570        assert!(original_err.is_transient());
1571
1572        Ok(())
1573    }
1574
1575    #[tokio::test]
1576    #[parallel]
1577    async fn debug_token_provider() {
1578        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1579            "type": "authorized_user",
1580            "client_id": "test-client-id",
1581            "client_secret": "test-client-secret",
1582            "refresh_token": "test-refresh-token"
1583        }))
1584        .build()
1585        .unwrap();
1586
1587        let expected = ImpersonatedTokenProvider {
1588            source_credentials,
1589            service_account_impersonation_url: ImpersonationUrl::exact(
1590                "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1591            ),
1592            delegates: Some(vec!["delegate1".to_string()]),
1593            scopes: vec!["scope1".to_string()],
1594            lifetime: Duration::from_secs(3600),
1595        };
1596        let fmt = format!("{expected:?}");
1597        assert!(fmt.contains("UserCredentials"), "{fmt}");
1598        assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1599        assert!(fmt.contains("delegate1"), "{fmt}");
1600        assert!(fmt.contains("scope1"), "{fmt}");
1601        assert!(fmt.contains("3600s"), "{fmt}");
1602    }
1603
1604    #[test]
1605    fn impersonated_config_full_from_json_success() {
1606        let source_credentials_json = json!({
1607            "type": "authorized_user",
1608            "client_id": "test-client-id",
1609            "client_secret": "test-client-secret",
1610            "refresh_token": "test-refresh-token"
1611        });
1612        let json = json!({
1613            "type": "impersonated_service_account",
1614            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1615            "source_credentials": source_credentials_json,
1616            "delegates": ["delegate1"],
1617            "quota_project_id": "test-project-id",
1618            "scopes": ["scope1"],
1619        });
1620
1621        let expected = ImpersonatedConfig {
1622            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1623            source_credentials: source_credentials_json,
1624            delegates: Some(vec!["delegate1".to_string()]),
1625            quota_project_id: Some("test-project-id".to_string()),
1626            scopes: Some(vec!["scope1".to_string()]),
1627            universe_domain: None,
1628        };
1629        let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1630        assert_eq!(actual, expected);
1631    }
1632
1633    #[test]
1634    fn impersonated_config_partial_from_json_success() {
1635        let source_credentials_json = json!({
1636            "type": "authorized_user",
1637            "client_id": "test-client-id",
1638            "client_secret": "test-client-secret",
1639            "refresh_token": "test-refresh-token"
1640        });
1641        let json = json!({
1642            "type": "impersonated_service_account",
1643            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1644            "source_credentials": source_credentials_json
1645        });
1646
1647        let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1648        assert_eq!(
1649            config.service_account_impersonation_url,
1650            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1651        );
1652        assert_eq!(config.source_credentials, source_credentials_json);
1653        assert_eq!(config.delegates, None);
1654        assert_eq!(config.quota_project_id, None);
1655        assert_eq!(config.scopes, None);
1656    }
1657
1658    #[tokio::test]
1659    #[parallel]
1660    async fn test_impersonated_service_account_source_fail() -> TestResult {
1661        let mut mock = MockCredentials::new();
1662        mock.expect_headers()
1663            .returning(|_| Err(errors::non_retryable_from_str("source failed")));
1664        let source_credentials = Credentials::from(mock);
1665
1666        let token_provider = ImpersonatedTokenProvider {
1667            source_credentials,
1668            service_account_impersonation_url: ImpersonationUrl::exact(
1669                "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1670            ),
1671            delegates: Some(vec!["delegate1".to_string()]),
1672            scopes: vec!["scope1".to_string()],
1673            lifetime: DEFAULT_LIFETIME,
1674        };
1675
1676        let err = token_provider.token().await.unwrap_err();
1677        assert!(err.to_string().contains("source failed"));
1678
1679        Ok(())
1680    }
1681
1682    #[tokio::test]
1683    #[parallel]
1684    async fn test_missing_impersonation_url_fail() {
1685        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1686            "type": "authorized_user",
1687            "client_id": "test-client-id",
1688            "client_secret": "test-client-secret",
1689            "refresh_token": "test-refresh-token"
1690        }))
1691        .build()
1692        .unwrap();
1693
1694        let result = Builder::from_source_credentials(source_credentials).build();
1695        assert!(result.is_err(), "{result:?}");
1696        let err = result.unwrap_err();
1697        assert!(err.is_parsing());
1698        assert!(
1699            err.to_string()
1700                .contains("`service_account_impersonation_url` is required")
1701        );
1702    }
1703
1704    #[tokio::test]
1705    #[parallel]
1706    async fn test_nested_impersonated_credentials_fail() {
1707        let nested_impersonated = json!({
1708            "type": "impersonated_service_account",
1709            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1710            "source_credentials": {
1711                "type": "impersonated_service_account",
1712                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1713                "source_credentials": {
1714                    "type": "authorized_user",
1715                    "client_id": "test-client-id",
1716                    "client_secret": "test-client-secret",
1717                    "refresh_token": "test-refresh-token"
1718                }
1719            }
1720        });
1721
1722        let result = Builder::new(nested_impersonated).build();
1723        assert!(result.is_err(), "{result:?}");
1724        let err = result.unwrap_err();
1725        assert!(err.is_parsing());
1726        assert!(
1727            err.to_string().contains(
1728                "source credential of type `impersonated_service_account` is not supported"
1729            )
1730        );
1731    }
1732
1733    #[tokio::test]
1734    #[parallel]
1735    async fn test_malformed_impersonated_credentials_fail() {
1736        let malformed_impersonated = json!({
1737            "type": "impersonated_service_account",
1738            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1739        });
1740
1741        let result = Builder::new(malformed_impersonated).build();
1742        assert!(result.is_err(), "{result:?}");
1743        let err = result.unwrap_err();
1744        assert!(err.is_parsing());
1745        assert!(
1746            err.to_string()
1747                .contains("missing field `source_credentials`")
1748        );
1749    }
1750
1751    #[tokio::test]
1752    #[parallel]
1753    async fn test_invalid_source_credential_type_fail() {
1754        let invalid_source = json!({
1755            "type": "impersonated_service_account",
1756            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1757            "source_credentials": {
1758                "type": "invalid_type",
1759            }
1760        });
1761
1762        let result = Builder::new(invalid_source).build();
1763        assert!(result.is_err(), "{result:?}");
1764        let err = result.unwrap_err();
1765        assert!(err.is_unknown_type());
1766    }
1767
1768    #[tokio::test]
1769    #[parallel]
1770    async fn test_missing_expiry() -> TestResult {
1771        let server = Server::run();
1772        server.expect(
1773            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1774                json_encoded(json!({
1775                    "access_token": "test-user-account-token",
1776                    "expires_in": 3600,
1777                    "token_type": "Bearer",
1778                })),
1779            ),
1780        );
1781        server.expect(
1782            Expectation::matching(request::method_path(
1783                "POST",
1784                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1785            ))
1786            .respond_with(json_encoded(json!({
1787                "accessToken": "test-impersonated-token",
1788            }))),
1789        );
1790
1791        let impersonated_credential = json!({
1792            "type": "impersonated_service_account",
1793            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1794            "source_credentials": {
1795                "type": "authorized_user",
1796                "client_id": "test-client-id",
1797                "client_secret": "test-client-secret",
1798                "refresh_token": "test-refresh-token",
1799                "token_uri": server.url("/token").to_string()
1800            }
1801        });
1802        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1803
1804        let err = token_provider.token().await.unwrap_err();
1805        assert!(!err.is_transient());
1806
1807        Ok(())
1808    }
1809
1810    #[tokio::test]
1811    #[parallel]
1812    async fn test_invalid_expiry_format() -> TestResult {
1813        let server = Server::run();
1814        server.expect(
1815            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1816                json_encoded(json!({
1817                    "access_token": "test-user-account-token",
1818                    "expires_in": 3600,
1819                    "token_type": "Bearer",
1820                })),
1821            ),
1822        );
1823        server.expect(
1824            Expectation::matching(request::method_path(
1825                "POST",
1826                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1827            ))
1828            .respond_with(json_encoded(json!({
1829                "accessToken": "test-impersonated-token",
1830                "expireTime": "invalid-format"
1831            }))),
1832        );
1833
1834        let impersonated_credential = json!({
1835            "type": "impersonated_service_account",
1836            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1837            "source_credentials": {
1838                "type": "authorized_user",
1839                "client_id": "test-client-id",
1840                "client_secret": "test-client-secret",
1841                "refresh_token": "test-refresh-token",
1842                "token_uri": server.url("/token").to_string()
1843            }
1844        });
1845        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1846
1847        let err = token_provider.token().await.unwrap_err();
1848        assert!(!err.is_transient());
1849
1850        Ok(())
1851    }
1852
1853    #[tokio::test]
1854    #[parallel]
1855    async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1856        let server = Server::run();
1857        server.expect(
1858            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1859                json_encoded(json!({
1860                    "access_token": "test-user-account-token",
1861                    "expires_in": 3600,
1862                    "token_type": "Bearer",
1863                })),
1864            ),
1865        );
1866        server.expect(
1867            Expectation::matching(request::method_path(
1868                "POST",
1869                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1870            ))
1871            .respond_with(json_encoded(json!("bad json"))),
1872        );
1873
1874        let impersonated_credential = json!({
1875            "type": "impersonated_service_account",
1876            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1877            "source_credentials": {
1878                "type": "authorized_user",
1879                "client_id": "test-client-id",
1880                "client_secret": "test-client-secret",
1881                "refresh_token": "test-refresh-token",
1882                "token_uri": server.url("/token").to_string()
1883            }
1884        });
1885        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1886
1887        let e = token_provider.token().await.err().unwrap();
1888        assert!(!e.is_transient(), "{e}");
1889
1890        Ok(())
1891    }
1892
1893    #[tokio::test]
1894    #[parallel]
1895    async fn token_provider_nonretryable_error() -> TestResult {
1896        let server = Server::run();
1897        server.expect(
1898            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1899                json_encoded(json!({
1900                    "access_token": "test-user-account-token",
1901                    "expires_in": 3600,
1902                    "token_type": "Bearer",
1903                })),
1904            ),
1905        );
1906        server.expect(
1907            Expectation::matching(request::method_path(
1908                "POST",
1909                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1910            ))
1911            .respond_with(status_code(401)),
1912        );
1913
1914        let impersonated_credential = json!({
1915            "type": "impersonated_service_account",
1916            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1917            "source_credentials": {
1918                "type": "authorized_user",
1919                "client_id": "test-client-id",
1920                "client_secret": "test-client-secret",
1921                "refresh_token": "test-refresh-token",
1922                "token_uri": server.url("/token").to_string()
1923            }
1924        });
1925        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1926
1927        let err = token_provider.token().await.unwrap_err();
1928        assert!(!err.is_transient());
1929
1930        Ok(())
1931    }
1932
1933    #[tokio::test]
1934    #[parallel]
1935    async fn credential_full_with_quota_project_from_builder() -> TestResult {
1936        let server = Server::run();
1937        server.expect(
1938            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1939                json_encoded(json!({
1940                    "access_token": "test-user-account-token",
1941                    "expires_in": 3600,
1942                    "token_type": "Bearer",
1943                })),
1944            ),
1945        );
1946        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1947            .format(&time::format_description::well_known::Rfc3339)
1948            .unwrap();
1949        server.expect(
1950            Expectation::matching(request::method_path(
1951                "POST",
1952                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1953            ))
1954            .respond_with(json_encoded(json!({
1955                "accessToken": "test-impersonated-token",
1956                "expireTime": expire_time
1957            }))),
1958        );
1959
1960        let impersonated_credential = json!({
1961            "type": "impersonated_service_account",
1962            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1963            "source_credentials": {
1964                "type": "authorized_user",
1965                "client_id": "test-client-id",
1966                "client_secret": "test-client-secret",
1967                "refresh_token": "test-refresh-token",
1968                "token_uri": server.url("/token").to_string()
1969            }
1970        });
1971        let creds = Builder::new(impersonated_credential)
1972            .with_quota_project_id("test-project")
1973            .build()?;
1974
1975        let headers = creds.headers(Extensions::new()).await?;
1976        match headers {
1977            CacheableResource::New { data, .. } => {
1978                assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1979            }
1980            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1981        }
1982
1983        Ok(())
1984    }
1985
1986    #[tokio::test]
1987    #[parallel]
1988    async fn access_token_credentials_success() -> TestResult {
1989        let server = Server::run();
1990        server.expect(
1991            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1992                json_encoded(json!({
1993                    "access_token": "test-user-account-token",
1994                    "expires_in": 3600,
1995                    "token_type": "Bearer",
1996                })),
1997            ),
1998        );
1999        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2000            .format(&time::format_description::well_known::Rfc3339)
2001            .unwrap();
2002        server.expect(
2003            Expectation::matching(request::method_path(
2004                "POST",
2005                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2006            ))
2007            .respond_with(json_encoded(json!({
2008                "accessToken": "test-impersonated-token",
2009                "expireTime": expire_time
2010            }))),
2011        );
2012
2013        let impersonated_credential = json!({
2014            "type": "impersonated_service_account",
2015            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2016            "source_credentials": {
2017                "type": "authorized_user",
2018                "client_id": "test-client-id",
2019                "client_secret": "test-client-secret",
2020                "refresh_token": "test-refresh-token",
2021                "token_uri": server.url("/token").to_string()
2022            }
2023        });
2024        let creds = Builder::new(impersonated_credential).build_access_token_credentials()?;
2025
2026        let access_token = creds.access_token().await?;
2027        assert_eq!(access_token.token, "test-impersonated-token");
2028
2029        Ok(())
2030    }
2031
2032    #[tokio::test]
2033    #[parallel]
2034    async fn test_with_target_principal() {
2035        let source_credentials = crate::credentials::user_account::Builder::new(json!({
2036            "type": "authorized_user",
2037            "client_id": "test-client-id",
2038            "client_secret": "test-client-secret",
2039            "refresh_token": "test-refresh-token"
2040        }))
2041        .build()
2042        .unwrap();
2043
2044        let (token_provider, _, _) = Builder::from_source_credentials(source_credentials.clone())
2045            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2046            .build_components()
2047            .unwrap();
2048
2049        let url = token_provider
2050            .inner
2051            .service_account_impersonation_url
2052            .access_token_url(&source_credentials)
2053            .await;
2054
2055        assert_eq!(
2056            url,
2057            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
2058        );
2059    }
2060
2061    fn source_creds_with_universe_domain(source_universe_domain: &str) -> Value {
2062        serde_json::json!({
2063            "type": "service_account",
2064            "client_email": "test-client-email",
2065            "private_key_id": "test-private-key-id",
2066            "private_key": Value::from(PKCS8_PK.clone()),
2067            "project_id": "test-project-id",
2068            "universe_domain": source_universe_domain,
2069        })
2070    }
2071
2072    fn service_account_builder_with_universe(source_universe_domain: &str) -> Builder {
2073        let source_creds = source_creds_with_universe_domain(source_universe_domain);
2074        let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2075            .build()
2076            .expect("Failed to build service account credentials");
2077        Builder::from_source_credentials(source_credentials)
2078            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2079    }
2080
2081    fn mds_builder() -> Builder {
2082        let source_credentials = crate::credentials::mds::Builder::default()
2083            .build()
2084            .expect("Failed to build MDS credentials");
2085        Builder::from_source_credentials(source_credentials)
2086            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2087    }
2088
2089    fn json_builder_with_universe(source_universe_domain: &str) -> Builder {
2090        let source_creds = source_creds_with_universe_domain(source_universe_domain);
2091        let impersonated_credential = serde_json::json!({
2092            "type": "impersonated_service_account",
2093            "service_account_impersonation_url": "https://iamcredentials.my-custom-universe.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2094            "source_credentials": source_creds,
2095        });
2096        Builder::new(impersonated_credential)
2097    }
2098
2099    #[test_case(service_account_builder_with_universe("my-custom-universe.com"); "service account as source")]
2100    #[test_case(json_builder_with_universe("my-custom-universe.com"); "credentials from json")]
2101    #[parallel]
2102    #[tokio::test]
2103    async fn universe_domain_from_source(builder: Builder) -> TestResult {
2104        let creds = builder.build()?;
2105        let universe_domain = creds.universe_domain().await;
2106
2107        assert_eq!(universe_domain.as_deref(), Some("my-custom-universe.com"));
2108
2109        Ok(())
2110    }
2111
2112    #[tokio::test]
2113    #[parallel]
2114    async fn universe_domain_mds_source() -> TestResult {
2115        let builder = mds_builder();
2116        let creds = builder.build()?;
2117        let universe_domain = creds.universe_domain().await;
2118
2119        assert!(is_default_universe_domain(universe_domain.as_deref()));
2120
2121        Ok(())
2122    }
2123
2124    #[test_case(service_account_builder_with_universe("my-custom-universe.com"); "service account as source")]
2125    #[test_case(json_builder_with_universe("my-custom-universe.com"); "credentials from json")]
2126    #[test_case(mds_builder(); "mds as source")]
2127    #[tokio::test]
2128    #[parallel]
2129    async fn universe_domain_override(builder: Builder) -> TestResult {
2130        let creds = builder
2131            .with_universe_domain("another-universe.com")
2132            .build()?;
2133
2134        let universe_domain = creds.universe_domain().await;
2135
2136        assert_eq!(universe_domain.as_deref(), Some("another-universe.com"));
2137
2138        Ok(())
2139    }
2140
2141    #[tokio::test]
2142    #[parallel]
2143    async fn credential_full_with_quota_project_from_json() -> TestResult {
2144        let server = Server::run();
2145        server.expect(
2146            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2147                json_encoded(json!({
2148                    "access_token": "test-user-account-token",
2149                    "expires_in": 3600,
2150                    "token_type": "Bearer",
2151                })),
2152            ),
2153        );
2154        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2155            .format(&time::format_description::well_known::Rfc3339)
2156            .unwrap();
2157        server.expect(
2158            Expectation::matching(request::method_path(
2159                "POST",
2160                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2161            ))
2162            .respond_with(json_encoded(json!({
2163                "accessToken": "test-impersonated-token",
2164                "expireTime": expire_time
2165            }))),
2166        );
2167
2168        let impersonated_credential = json!({
2169            "type": "impersonated_service_account",
2170            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2171            "source_credentials": {
2172                "type": "authorized_user",
2173                "client_id": "test-client-id",
2174                "client_secret": "test-client-secret",
2175                "refresh_token": "test-refresh-token",
2176                "token_uri": server.url("/token").to_string()
2177            },
2178            "quota_project_id": "test-project-from-json",
2179        });
2180
2181        let creds = Builder::new(impersonated_credential).build()?;
2182
2183        let headers = creds.headers(Extensions::new()).await?;
2184        println!("headers: {:#?}", headers);
2185        match headers {
2186            CacheableResource::New { data, .. } => {
2187                assert_eq!(
2188                    data.get("x-goog-user-project").unwrap(),
2189                    "test-project-from-json"
2190                );
2191            }
2192            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
2193        }
2194
2195        Ok(())
2196    }
2197
2198    #[tokio::test]
2199    #[parallel]
2200    async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
2201        let server = Server::run();
2202
2203        // Expectation for the source credential token request.
2204        // It should NOT have any scopes in the body.
2205        server.expect(
2206            Expectation::matching(all_of![
2207                request::method_path("POST", "/source_token"),
2208                request::body(json_decoded(
2209                    |body: &serde_json::Value| body["scopes"].is_null()
2210                ))
2211            ])
2212            .respond_with(json_encoded(json!({
2213                "access_token": "source-token",
2214                "expires_in": 3600,
2215                "token_type": "Bearer",
2216            }))),
2217        );
2218
2219        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2220            .format(&time::format_description::well_known::Rfc3339)
2221            .unwrap();
2222
2223        // Expectation for the impersonation request.
2224        // It SHOULD have the scopes from the impersonated builder.
2225        server.expect(
2226            Expectation::matching(all_of![
2227                request::method_path(
2228                    "POST",
2229                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2230                ),
2231                request::headers(contains(("authorization", "Bearer source-token"))),
2232                request::body(json_decoded(eq(json!({
2233                    "scope": ["impersonated-scope"],
2234                    "lifetime": "3600s"
2235                }))))
2236            ])
2237            .respond_with(json_encoded(json!({
2238                "accessToken": "impersonated-token",
2239                "expireTime": expire_time
2240            }))),
2241        );
2242
2243        let impersonated_credential = json!({
2244            "type": "impersonated_service_account",
2245            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2246            "source_credentials": {
2247                "type": "authorized_user",
2248                "client_id": "test-client-id",
2249                "client_secret": "test-client-secret",
2250                "refresh_token": "test-refresh-token",
2251                "token_uri": server.url("/source_token").to_string()
2252            }
2253        });
2254
2255        let creds = Builder::new(impersonated_credential)
2256            .with_scopes(vec!["impersonated-scope"])
2257            .with_quota_project_id("impersonated-quota-project")
2258            .build()?;
2259
2260        // The quota project should be set on the final credentials object.
2261        let fmt = format!("{creds:?}");
2262        assert!(fmt.contains("impersonated-quota-project"));
2263
2264        // Fetching the token will trigger the mock server expectations.
2265        let _token = creds.headers(Extensions::new()).await?;
2266
2267        Ok(())
2268    }
2269
2270    #[tokio::test]
2271    #[parallel]
2272    async fn test_impersonated_metrics_header() -> TestResult {
2273        let server = Server::run();
2274        server.expect(
2275            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2276                json_encoded(json!({
2277                    "access_token": "test-user-account-token",
2278                    "expires_in": 3600,
2279                    "token_type": "Bearer",
2280                })),
2281            ),
2282        );
2283        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2284            .format(&time::format_description::well_known::Rfc3339)
2285            .unwrap();
2286        server.expect(
2287            Expectation::matching(all_of![
2288                request::method_path(
2289                    "POST",
2290                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2291                ),
2292                request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
2293                request::headers(contains((
2294                    "x-goog-api-client",
2295                    matches("auth-request-type/at")
2296                )))
2297            ])
2298            .respond_with(json_encoded(json!({
2299                "accessToken": "test-impersonated-token",
2300                "expireTime": expire_time
2301            }))),
2302        );
2303
2304        let impersonated_credential = json!({
2305            "type": "impersonated_service_account",
2306            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2307            "source_credentials": {
2308                "type": "authorized_user",
2309                "client_id": "test-client-id",
2310                "client_secret": "test-client-secret",
2311                "refresh_token": "test-refresh-token",
2312                "token_uri": server.url("/token").to_string()
2313            }
2314        });
2315        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
2316
2317        let token = token_provider.token().await?;
2318        assert_eq!(token.token, "test-impersonated-token");
2319        assert_eq!(token.token_type, "Bearer");
2320
2321        Ok(())
2322    }
2323
2324    #[tokio::test]
2325    #[parallel]
2326    async fn test_impersonated_retries_for_success() -> TestResult {
2327        let mut server = Server::run();
2328        // Source credential token endpoint
2329        server.expect(
2330            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2331                json_encoded(json!({
2332                    "access_token": "test-user-account-token",
2333                    "expires_in": 3600,
2334                    "token_type": "Bearer",
2335                })),
2336            ),
2337        );
2338
2339        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2340            .format(&time::format_description::well_known::Rfc3339)
2341            .unwrap();
2342
2343        // Impersonation endpoint
2344        let impersonation_path =
2345            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2346        server.expect(
2347            Expectation::matching(request::method_path("POST", impersonation_path))
2348                .times(3)
2349                .respond_with(cycle![
2350                    status_code(503).body("try-again"),
2351                    status_code(503).body("try-again"),
2352                    status_code(200)
2353                        .append_header("Content-Type", "application/json")
2354                        .body(
2355                            json!({
2356                                "accessToken": "test-impersonated-token",
2357                                "expireTime": expire_time
2358                            })
2359                            .to_string()
2360                        ),
2361                ]),
2362        );
2363
2364        let impersonated_credential = json!({
2365            "type": "impersonated_service_account",
2366            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2367            "source_credentials": {
2368                "type": "authorized_user",
2369                "client_id": "test-client-id",
2370                "client_secret": "test-client-secret",
2371                "refresh_token": "test-refresh-token",
2372                "token_uri": server.url("/token").to_string()
2373            }
2374        });
2375
2376        let (token_provider, _, _) = Builder::new(impersonated_credential)
2377            .with_retry_policy(get_mock_auth_retry_policy(3))
2378            .with_backoff_policy(get_mock_backoff_policy())
2379            .with_retry_throttler(get_mock_retry_throttler())
2380            .build_components()?;
2381
2382        let token = token_provider.token().await?;
2383        assert_eq!(token.token, "test-impersonated-token");
2384
2385        server.verify_and_clear();
2386        Ok(())
2387    }
2388
2389    #[tokio::test]
2390    #[parallel]
2391    async fn test_scopes_from_json() -> TestResult {
2392        let server = Server::run();
2393        server.expect(
2394            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2395                json_encoded(json!({
2396                    "access_token": "test-user-account-token",
2397                    "expires_in": 3600,
2398                    "token_type": "Bearer",
2399                })),
2400            ),
2401        );
2402        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2403            .format(&time::format_description::well_known::Rfc3339)
2404            .unwrap();
2405        server.expect(
2406            Expectation::matching(all_of![
2407                request::method_path(
2408                    "POST",
2409                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2410                ),
2411                request::body(json_decoded(eq(json!({
2412                    "scope": ["scope-from-json"],
2413                    "lifetime": "3600s"
2414                }))))
2415            ])
2416            .respond_with(json_encoded(json!({
2417                "accessToken": "test-impersonated-token",
2418                "expireTime": expire_time
2419            }))),
2420        );
2421
2422        let impersonated_credential = json!({
2423            "type": "impersonated_service_account",
2424            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2425            "scopes": ["scope-from-json"],
2426            "source_credentials": {
2427                "type": "authorized_user",
2428                "client_id": "test-client-id",
2429                "client_secret": "test-client-secret",
2430                "refresh_token": "test-refresh-token",
2431                "token_uri": server.url("/token").to_string()
2432            }
2433        });
2434        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
2435
2436        let token = token_provider.token().await?;
2437        assert_eq!(token.token, "test-impersonated-token");
2438
2439        Ok(())
2440    }
2441
2442    #[tokio::test]
2443    #[parallel]
2444    async fn test_with_scopes_overrides_json_scopes() -> TestResult {
2445        let server = Server::run();
2446        server.expect(
2447            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2448                json_encoded(json!({
2449                    "access_token": "test-user-account-token",
2450                    "expires_in": 3600,
2451                    "token_type": "Bearer",
2452                })),
2453            ),
2454        );
2455        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2456            .format(&time::format_description::well_known::Rfc3339)
2457            .unwrap();
2458        server.expect(
2459            Expectation::matching(all_of![
2460                request::method_path(
2461                    "POST",
2462                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2463                ),
2464                request::body(json_decoded(eq(json!({
2465                    "scope": ["scope-from-with-scopes"],
2466                    "lifetime": "3600s"
2467                }))))
2468            ])
2469            .respond_with(json_encoded(json!({
2470                "accessToken": "test-impersonated-token",
2471                "expireTime": expire_time
2472            }))),
2473        );
2474
2475        let impersonated_credential = json!({
2476            "type": "impersonated_service_account",
2477            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2478            "scopes": ["scope-from-json"],
2479            "source_credentials": {
2480                "type": "authorized_user",
2481                "client_id": "test-client-id",
2482                "client_secret": "test-client-secret",
2483                "refresh_token": "test-refresh-token",
2484                "token_uri": server.url("/token").to_string()
2485            }
2486        });
2487        let (token_provider, _, _) = Builder::new(impersonated_credential)
2488            .with_scopes(vec!["scope-from-with-scopes"])
2489            .build_components()?;
2490
2491        let token = token_provider.token().await?;
2492        assert_eq!(token.token, "test-impersonated-token");
2493
2494        Ok(())
2495    }
2496
2497    #[tokio::test]
2498    #[parallel]
2499    async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
2500        let mut server = Server::run();
2501        // Source credential token endpoint
2502        server.expect(
2503            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2504                json_encoded(json!({
2505                    "access_token": "test-user-account-token",
2506                    "expires_in": 3600,
2507                    "token_type": "Bearer",
2508                })),
2509            ),
2510        );
2511
2512        // Impersonation endpoint
2513        let impersonation_path =
2514            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2515        server.expect(
2516            Expectation::matching(request::method_path("POST", impersonation_path))
2517                .times(1)
2518                .respond_with(status_code(401)),
2519        );
2520
2521        let impersonated_credential = json!({
2522            "type": "impersonated_service_account",
2523            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2524            "source_credentials": {
2525                "type": "authorized_user",
2526                "client_id": "test-client-id",
2527                "client_secret": "test-client-secret",
2528                "refresh_token": "test-refresh-token",
2529                "token_uri": server.url("/token").to_string()
2530            }
2531        });
2532
2533        let (token_provider, _, _) = Builder::new(impersonated_credential)
2534            .with_retry_policy(get_mock_auth_retry_policy(3))
2535            .with_backoff_policy(get_mock_backoff_policy())
2536            .with_retry_throttler(get_mock_retry_throttler())
2537            .build_components()?;
2538
2539        let err = token_provider.token().await.unwrap_err();
2540        assert!(!err.is_transient());
2541
2542        server.verify_and_clear();
2543        Ok(())
2544    }
2545
2546    #[tokio::test]
2547    #[parallel]
2548    async fn test_impersonated_remote_signer() -> TestResult {
2549        let server = Server::run();
2550        server.expect(
2551            Expectation::matching(request::method_path("POST", "/token"))
2552                .times(2..)
2553                .respond_with(json_encoded(json!({
2554                    "access_token": "test-user-account-token",
2555                    "expires_in": 3600,
2556                    "token_type": "Bearer",
2557                }))),
2558        );
2559        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2560            .format(&time::format_description::well_known::Rfc3339)
2561            .unwrap();
2562        server.expect(
2563            Expectation::matching(request::method_path(
2564                "POST",
2565                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2566            ))
2567            .times(2)
2568            .respond_with(json_encoded(json!({
2569                "accessToken": "test-impersonated-token",
2570                "expireTime": expire_time
2571            }))),
2572        );
2573
2574        server.expect(
2575            Expectation::matching(all_of![
2576                request::method_path(
2577                    "POST",
2578                    "/v1/projects/-/serviceAccounts/test-principal:signBlob"
2579                ),
2580                request::headers(contains((
2581                    "authorization",
2582                    "Bearer test-impersonated-token"
2583                ))),
2584            ])
2585            .times(2)
2586            .respond_with(json_encoded(json!({
2587                "signedBlob": BASE64_STANDARD.encode("signed_blob"),
2588            }))),
2589        );
2590
2591        let endpoint = server.url("/").to_string();
2592        let endpoint = endpoint.trim_end_matches('/');
2593
2594        // Test builder from source credentials
2595        let user_credential = json!({
2596            "type": "authorized_user",
2597            "client_id": "test-client-id",
2598            "client_secret": "test-client-secret",
2599            "refresh_token": "test-refresh-token",
2600            "token_uri": server.url("/token").to_string()
2601        });
2602        let source_credential =
2603            crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2604
2605        let builder_from_source = Builder::from_source_credentials(source_credential)
2606            .with_target_principal("test-principal")
2607            .with_impersonation_endpoint(endpoint);
2608
2609        let impersonation_url = server
2610            .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2611            .to_string();
2612        // Test builder from JSON
2613        let impersonated_credential = json!({
2614            "type": "impersonated_service_account",
2615            "service_account_impersonation_url": impersonation_url,
2616            "source_credentials": user_credential,
2617        });
2618        let builder_from_json = Builder::new(impersonated_credential);
2619
2620        for builder in [builder_from_source, builder_from_json] {
2621            let iam_endpoint = server
2622                .url("/")
2623                .to_string()
2624                .trim_end_matches('/')
2625                .to_string();
2626            let signer = builder
2627                .maybe_iam_endpoint_override(Some(iam_endpoint))
2628                .without_access_boundary()
2629                .build_signer()?;
2630
2631            let client_email = signer.client_email().await?;
2632            assert_eq!(client_email, "test-principal");
2633
2634            let result = signer.sign(b"test").await?;
2635            assert_eq!(result.as_ref(), b"signed_blob");
2636        }
2637
2638        Ok(())
2639    }
2640
2641    #[tokio::test]
2642    #[parallel]
2643    async fn test_impersonated_sa_signer() -> TestResult {
2644        let service_account = json!({
2645            "type": "service_account",
2646            "client_email": "test-client-email",
2647            "private_key_id": "test-private-key-id",
2648            "private_key": Value::from(PKCS8_PK.clone()),
2649            "project_id": "test-project-id",
2650        });
2651        let impersonated_credential = json!({
2652            "type": "impersonated_service_account",
2653            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2654            "source_credentials": service_account.clone(),
2655        });
2656
2657        let signer = Builder::new(impersonated_credential).build_signer()?;
2658
2659        let client_email = signer.client_email().await?;
2660        assert_eq!(client_email, "test-principal");
2661
2662        let result = signer.sign(b"test").await?;
2663
2664        let service_account_key = serde_json::from_value::<ServiceAccountKey>(service_account)?;
2665        let inner_signer = service_account_key.signer().unwrap();
2666        let inner_result = inner_signer.sign(b"test")?;
2667        assert_eq!(result.as_ref(), inner_result);
2668
2669        Ok(())
2670    }
2671
2672    #[tokio::test]
2673    #[parallel]
2674    async fn test_impersonated_signer_with_invalid_email() -> TestResult {
2675        let impersonated_credential = json!({
2676            "type": "impersonated_service_account",
2677            "service_account_impersonation_url": "http://example.com/test-principal:generateIdToken",
2678            "source_credentials": json!({
2679                "type": "service_account",
2680                "client_email": "test-client-email",
2681                "private_key_id": "test-private-key-id",
2682                "private_key": "test-private-key",
2683                "project_id": "test-project-id",
2684            }),
2685        });
2686
2687        let error = Builder::new(impersonated_credential)
2688            .build_signer()
2689            .unwrap_err();
2690
2691        assert!(error.is_parsing());
2692        assert!(
2693            error
2694                .to_string()
2695                .contains("invalid service account impersonation URL"),
2696            "error: {}",
2697            error
2698        );
2699
2700        Ok(())
2701    }
2702
2703    #[tokio::test]
2704    #[parallel]
2705    #[cfg(google_cloud_unstable_trust_boundaries)]
2706    async fn e2e_access_boundary() -> TestResult {
2707        use crate::credentials::tests::{get_access_boundary_from_headers, get_token_from_headers};
2708        let server = Server::run();
2709        server.expect(
2710            Expectation::matching(request::method_path("POST", "/token"))
2711                .times(2..)
2712                .respond_with(json_encoded(json!({
2713                    "access_token": "test-user-account-token",
2714                    "expires_in": 3600,
2715                    "token_type": "Bearer",
2716                }))),
2717        );
2718        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2719            .format(&time::format_description::well_known::Rfc3339)
2720            .unwrap();
2721        server.expect(
2722            Expectation::matching(request::method_path(
2723                "POST",
2724                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2725            ))
2726            .times(2)
2727            .respond_with(json_encoded(json!({
2728                "accessToken": "test-impersonated-token",
2729                "expireTime": expire_time
2730            }))),
2731        );
2732
2733        server.expect(
2734            Expectation::matching(all_of![
2735                request::method_path(
2736                    "GET",
2737                    "/v1/projects/-/serviceAccounts/test-principal/allowedLocations"
2738                ),
2739                request::headers(contains((
2740                    "authorization",
2741                    "Bearer test-impersonated-token"
2742                ))),
2743            ])
2744            .times(2)
2745            .respond_with(json_encoded(json!({
2746                "locations": ["us-central1", "us-east1"],
2747                "encodedLocations": "0x1234"
2748            }))),
2749        );
2750        let impersonation_url = server
2751            .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2752            .to_string();
2753        let endpoint = server.url("/").to_string();
2754        let endpoint = endpoint.trim_end_matches('/');
2755
2756        // Test builder from source credentials
2757        let user_credential = json!({
2758            "type": "authorized_user",
2759            "client_id": "test-client-id",
2760            "client_secret": "test-client-secret",
2761            "refresh_token": "test-refresh-token",
2762            "token_uri": server.url("/token").to_string()
2763        });
2764        let source_credential =
2765            crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2766        let builder_from_source = Builder::from_source_credentials(source_credential)
2767            .with_target_principal("test-principal")
2768            .with_impersonation_endpoint(endpoint);
2769
2770        // Test builder from JSON
2771        let impersonated_credential = json!({
2772            "type": "impersonated_service_account",
2773            "service_account_impersonation_url": impersonation_url,
2774            "source_credentials": user_credential,
2775        });
2776        let builder_from_json = Builder::new(impersonated_credential);
2777
2778        for builder in [builder_from_source, builder_from_json] {
2779            let iam_endpoint = server
2780                .url("/")
2781                .to_string()
2782                .trim_end_matches('/')
2783                .to_string();
2784            let creds = builder
2785                .maybe_iam_endpoint_override(Some(iam_endpoint))
2786                .build_credentials()?;
2787
2788            // let the access boundary background thread update
2789            creds.wait_for_boundary().await;
2790
2791            let headers = creds.headers(Extensions::new()).await?;
2792            let token = get_token_from_headers(headers.clone());
2793            let access_boundary = get_access_boundary_from_headers(headers);
2794            assert!(token.is_some(), "should have some token: {token:?}");
2795            assert_eq!(
2796                access_boundary.as_deref(),
2797                Some("0x1234"),
2798                "should be 0x1234 but found: {access_boundary:?}"
2799            );
2800        }
2801
2802        Ok(())
2803    }
2804
2805    #[tokio::test]
2806    #[parallel]
2807    async fn test_impersonated_access_token_custom_universe_domain() -> TestResult {
2808        let server = Server::run();
2809        let universe_domain = "my-custom-universe.com".to_string();
2810        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2811            .format(&time::format_description::well_known::Rfc3339)
2812            .unwrap();
2813
2814        server.expect(
2815            Expectation::matching(all_of![
2816                request::method_path(
2817                    "POST",
2818                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2819                ),
2820                request::headers(contains((
2821                    "authorization",
2822                    "Bearer test-user-account-token"
2823                ))),
2824            ])
2825            .respond_with(json_encoded(json!({
2826                "accessToken": "test-impersonated-token",
2827                "expireTime": expire_time
2828            }))),
2829        );
2830
2831        let universe_domain_clone = universe_domain.clone();
2832        let mut mock = MockCredentials::new();
2833        mock.expect_universe_domain()
2834            .returning(move || Some(universe_domain_clone.clone()));
2835        mock.expect_headers().returning(move |_| {
2836            let mut headers = HeaderMap::new();
2837            headers.insert(
2838                "authorization",
2839                "Bearer test-user-account-token".parse().unwrap(),
2840            );
2841            Ok(CacheableResource::New {
2842                entity_tag: Default::default(),
2843                data: headers,
2844            })
2845        });
2846        let source_credentials = Credentials::from(mock);
2847
2848        let builder = Builder::from_source_credentials(source_credentials.clone())
2849            .with_target_principal("test-principal");
2850
2851        // resolve url with universe domain
2852        let url = builder
2853            .service_account_impersonation_url
2854            .as_ref()
2855            .expect("url should be set from the with_target_principal call")
2856            .access_token_url(&source_credentials)
2857            .await;
2858
2859        assert_eq!(
2860            url,
2861            format!(
2862                "https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2863            )
2864        );
2865
2866        let endpoint = server.url("/").to_string();
2867        let endpoint = endpoint.trim_end_matches('/');
2868
2869        let creds = builder
2870            .with_impersonation_endpoint(endpoint)
2871            .build_access_token_credentials()?;
2872
2873        let token = creds.access_token().await?;
2874        assert_eq!(token.token, "test-impersonated-token");
2875
2876        Ok(())
2877    }
2878
2879    #[test_case(ImpersonationUrlKind::TargetPrincipal("test@example.com".to_string()), "test@example.com" ; "target principal")]
2880    #[test_case(ImpersonationUrlKind::Exact("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@example.com:generateAccessToken".to_string()), "test@example.com" ; "exact url")]
2881    fn impersonation_url_client_email(kind: ImpersonationUrlKind, expected: &str) {
2882        let impersonation_url = ImpersonationUrl {
2883            endpoint: None,
2884            kind,
2885        };
2886        assert_eq!(impersonation_url.client_email().unwrap(), expected);
2887    }
2888
2889    #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "googleapis.com", "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/user@example.com:generateAccessToken" ; "target principal default universe")]
2890    #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "my-custom-universe.com", "https://iamcredentials.my-custom-universe.com/v1/projects/-/serviceAccounts/user@example.com:generateAccessToken" ; "target principal custom universe")]
2891    #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()).with_endpoint("https://iam.example.com"), "googleapis.com", "https://iam.example.com/v1/projects/-/serviceAccounts/user@example.com:generateAccessToken" ; "target principal custom endpoint override")]
2892    #[test_case(ImpersonationUrl::exact("https://iam.example.com/v1/user@example.com:generateAccessToken".to_string()), "googleapis.com", "https://iam.example.com/v1/user@example.com:generateAccessToken" ; "exact url")]
2893    #[tokio::test]
2894    async fn impersonation_url_access_token_url(
2895        impersonation_url: ImpersonationUrl,
2896        universe: &str,
2897        expected_url: &str,
2898    ) -> TestResult {
2899        let source_creds = source_creds_with_universe_domain(universe);
2900        let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2901            .build()
2902            .expect("Failed to build service account credentials");
2903
2904        let url = impersonation_url
2905            .access_token_url(&source_credentials)
2906            .await;
2907        assert_eq!(url, expected_url);
2908        Ok(())
2909    }
2910
2911    #[cfg(feature = "idtoken")]
2912    #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "googleapis.com", "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/user@example.com:generateIdToken" ; "target principal default universe")]
2913    #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()), "my-custom-universe.com", "https://iamcredentials.my-custom-universe.com/v1/projects/-/serviceAccounts/user@example.com:generateIdToken" ; "target principal custom universe")]
2914    #[test_case(ImpersonationUrl::target_principal("user@example.com".to_string()).with_endpoint("https://iam.example.com"), "googleapis.com", "https://iam.example.com/v1/projects/-/serviceAccounts/user@example.com:generateIdToken" ; "target principal custom endpoint override")]
2915    #[test_case(ImpersonationUrl::exact("https://iam.example.com/v1/user@example.com:generateAccessToken".to_string()), "googleapis.com", "https://iam.example.com/v1/user@example.com:generateIdToken" ; "exact url id token")]
2916    #[tokio::test]
2917    async fn impersonation_url_id_token_url(
2918        impersonation_url: ImpersonationUrl,
2919        universe: &str,
2920        expected_url: &str,
2921    ) -> TestResult {
2922        let source_creds = source_creds_with_universe_domain(universe);
2923        let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2924            .build()
2925            .expect("Failed to build service account credentials");
2926
2927        let url = impersonation_url.id_token_url(&source_credentials).await;
2928        assert_eq!(url, expected_url);
2929        Ok(())
2930    }
2931}