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            && let Some(signer) = build_signer_from_json(json.clone())?
653        {
654            return Ok(signer);
655        }
656        let impersonation_url = self.resolve_impersonation_url()?;
657        let client_email = impersonation_url.client_email()?;
658        let creds = self.build()?;
659        let signer = crate::signer::iam::IamSigner::new(client_email, creds, iam_endpoint);
660        Ok(crate::signer::Signer {
661            inner: Arc::new(signer),
662        })
663    }
664
665    fn build_components(
666        self,
667    ) -> BuildResult<(
668        TokenProviderWithRetry<ImpersonatedTokenProvider>,
669        Option<String>,
670        Credentials,
671    )> {
672        let components = match self.source {
673            BuilderSource::FromJson(json) => build_components_from_json(json)?,
674            BuilderSource::FromCredentials(source_credentials) => {
675                build_components_from_credentials(
676                    source_credentials,
677                    self.service_account_impersonation_url,
678                )?
679            }
680        };
681
682        let scopes = self
683            .scopes
684            .or(components.scopes)
685            .unwrap_or_else(|| vec![DEFAULT_SCOPE.to_string()]);
686
687        let quota_project_id = self.quota_project_id.or(components.quota_project_id);
688        let delegates = self.delegates.or(components.delegates);
689
690        let source_credentials = components.source_credentials;
691        let token_provider = ImpersonatedTokenProvider {
692            source_credentials: source_credentials.clone(),
693            service_account_impersonation_url: components.service_account_impersonation_url,
694            delegates,
695            scopes,
696            lifetime: self.lifetime.unwrap_or(DEFAULT_LIFETIME),
697        };
698        let token_provider = self.retry_builder.build(token_provider);
699        Ok((token_provider, quota_project_id, source_credentials))
700    }
701
702    fn resolve_impersonation_url(&self) -> BuildResult<ImpersonationUrl> {
703        match self.source.clone() {
704            BuilderSource::FromJson(json) => {
705                let config = config_from_json(json)?;
706                Ok(ImpersonationUrl::exact(config.service_account_impersonation_url))
707            }
708            BuilderSource::FromCredentials(_) => {
709                self.service_account_impersonation_url.clone().ok_or_else(|| {
710                    BuilderError::parsing(
711                        "`service_account_impersonation_url` is required when building from source credentials",
712                    )
713                })
714            }
715        }
716    }
717}
718
719pub(crate) struct ImpersonatedCredentialComponents {
720    pub(crate) source_credentials: Credentials,
721    pub(crate) service_account_impersonation_url: ImpersonationUrl,
722    pub(crate) delegates: Option<Vec<String>>,
723    pub(crate) quota_project_id: Option<String>,
724    pub(crate) scopes: Option<Vec<String>>,
725}
726
727fn config_from_json(json: Value) -> BuildResult<ImpersonatedConfig> {
728    serde_json::from_value::<ImpersonatedConfig>(json).map_err(BuilderError::parsing)
729}
730
731pub(crate) fn build_components_from_json(
732    json: Value,
733) -> BuildResult<ImpersonatedCredentialComponents> {
734    let config = config_from_json(json)?;
735
736    let source_credential_type = extract_credential_type(&config.source_credentials)?;
737    if source_credential_type == "impersonated_service_account" {
738        return Err(BuilderError::parsing(
739            "source credential of type `impersonated_service_account` is not supported. \
740                        Use the `delegates` field to specify a delegation chain.",
741        ));
742    }
743
744    // Do not pass along scopes and quota project to the source credentials.
745    // It is not necessary that the source and target credentials have same permissions on
746    // the quota project and they typically need different scopes.
747    // If user does want some specific scopes or quota, they can build using the
748    // from_source_credentials method.
749    let source_credentials =
750        build_credentials(Some(config.source_credentials), None, None, None)?.into();
751
752    Ok(ImpersonatedCredentialComponents {
753        source_credentials,
754        service_account_impersonation_url: ImpersonationUrl::exact(
755            config.service_account_impersonation_url,
756        ),
757        delegates: config.delegates,
758        quota_project_id: config.quota_project_id,
759        scopes: config.scopes,
760    })
761}
762
763// Check if the source credentials is a service account and if so, build a signer from it.
764// Service account signers can do signing locally, which is more efficient than using the
765// remote/API based signer.
766fn build_signer_from_json(json: Value) -> BuildResult<Option<crate::signer::Signer>> {
767    use crate::credentials::service_account::ServiceAccountKey;
768    use crate::signer::service_account::ServiceAccountSigner;
769
770    let config = config_from_json(json)?;
771
772    let client_email = extract_client_email(&config.service_account_impersonation_url)?;
773    let source_credential_type = extract_credential_type(&config.source_credentials)?;
774    if source_credential_type == "service_account" {
775        let service_account_key =
776            serde_json::from_value::<ServiceAccountKey>(config.source_credentials)
777                .map_err(BuilderError::parsing)?;
778        let signing_provider = ServiceAccountSigner::from_impersonated_service_account(
779            service_account_key,
780            client_email,
781        );
782        let signer = crate::signer::Signer {
783            inner: Arc::new(signing_provider),
784        };
785        return Ok(Some(signer));
786    }
787    Ok(None)
788}
789
790fn extract_client_email(service_account_impersonation_url: &str) -> BuildResult<String> {
791    let mut parts = service_account_impersonation_url.split("/serviceAccounts/");
792    match (parts.nth(1), parts.next()) {
793        (Some(email), None) => Ok(email.trim_end_matches(":generateAccessToken").to_string()),
794        _ => Err(BuilderError::parsing(
795            "invalid service account impersonation URL",
796        )),
797    }
798}
799
800pub(crate) fn build_components_from_credentials(
801    source_credentials: Credentials,
802    impersonation_url: Option<ImpersonationUrl>,
803) -> BuildResult<ImpersonatedCredentialComponents> {
804    let url = impersonation_url.ok_or_else(|| {
805        BuilderError::parsing(
806            "`target_principal` is required when building from source credentials",
807        )
808    })?;
809    Ok(ImpersonatedCredentialComponents {
810        source_credentials,
811        service_account_impersonation_url: url,
812        delegates: None,
813        quota_project_id: None,
814        scopes: None,
815    })
816}
817
818#[derive(serde::Deserialize, Debug, PartialEq)]
819struct ImpersonatedConfig {
820    service_account_impersonation_url: String,
821    source_credentials: Value,
822    delegates: Option<Vec<String>>,
823    quota_project_id: Option<String>,
824    scopes: Option<Vec<String>>,
825    universe_domain: Option<String>,
826}
827
828#[derive(Debug)]
829struct ImpersonatedServiceAccount<T>
830where
831    T: CachedTokenProvider,
832{
833    token_provider: T,
834    quota_project_id: Option<String>,
835    universe_domain_override: Option<String>,
836    source_credentials: Credentials,
837}
838
839#[async_trait::async_trait]
840impl<T> CredentialsProvider for ImpersonatedServiceAccount<T>
841where
842    T: CachedTokenProvider,
843{
844    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
845        let token = self.token_provider.token(extensions).await?;
846
847        AuthHeadersBuilder::new(&token)
848            .maybe_quota_project_id(self.quota_project_id.as_deref())
849            .build()
850    }
851
852    async fn universe_domain(&self) -> Option<String> {
853        if let Some(universe_domain) = &self.universe_domain_override {
854            return Some(universe_domain.clone());
855        }
856        self.source_credentials.universe_domain().await
857    }
858}
859
860#[async_trait::async_trait]
861impl<T> AccessTokenCredentialsProvider for ImpersonatedServiceAccount<T>
862where
863    T: CachedTokenProvider,
864{
865    async fn access_token(&self) -> Result<AccessToken> {
866        let token = self.token_provider.token(Extensions::new()).await?;
867        token.into()
868    }
869}
870
871struct ImpersonatedTokenProvider {
872    source_credentials: Credentials,
873    service_account_impersonation_url: ImpersonationUrl,
874    delegates: Option<Vec<String>>,
875    scopes: Vec<String>,
876    lifetime: Duration,
877}
878
879impl Debug for ImpersonatedTokenProvider {
880    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
881        f.debug_struct("ImpersonatedTokenProvider")
882            .field("source_credentials", &self.source_credentials)
883            .field(
884                "service_account_impersonation_url",
885                &self.service_account_impersonation_url,
886            )
887            .field("delegates", &self.delegates)
888            .field("scopes", &self.scopes)
889            .field("lifetime", &self.lifetime)
890            .finish()
891    }
892}
893
894#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
895struct GenerateAccessTokenRequest {
896    #[serde(skip_serializing_if = "Option::is_none")]
897    delegates: Option<Vec<String>>,
898    scope: Vec<String>,
899    lifetime: String,
900}
901
902pub(crate) async fn generate_access_token(
903    source_headers: HeaderMap,
904    delegates: Option<Vec<String>>,
905    scopes: Vec<String>,
906    lifetime: Duration,
907    service_account_impersonation_url: &str,
908) -> Result<Token> {
909    GenerateAccessTokenClient::new(
910        source_headers,
911        delegates,
912        scopes,
913        lifetime,
914        service_account_impersonation_url.to_string(),
915    )
916    .fetch()
917    .await
918}
919
920async fn generate_access_token_call(
921    client: &Client,
922    url: &str,
923    source_headers: HeaderMap,
924    body: &GenerateAccessTokenRequest,
925) -> GaxResult<reqwest::Response> {
926    let response = client
927        .post(url)
928        .header("Content-Type", "application/json")
929        .header(
930            headers_util::X_GOOG_API_CLIENT,
931            metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
932        )
933        .headers(source_headers)
934        .json(body)
935        .send()
936        .await
937        .map_err(GaxError::io)?;
938
939    let status = response.status();
940    if !status.is_success() {
941        let err_headers = response.headers().clone();
942        let err_payload = response
943            .bytes()
944            .await
945            .map_err(|e| GaxError::transport(err_headers.clone(), e))?;
946        return Err(GaxError::http(status.as_u16(), err_headers, err_payload));
947    }
948
949    Ok(response)
950}
951
952#[derive(Debug)]
953struct GenerateAccessTokenClient {
954    source_headers: HeaderMap,
955    delegates: Option<Vec<String>>,
956    scopes: Vec<String>,
957    lifetime: Duration,
958    url: String,
959    retry_policy: Arc<dyn RetryPolicy>,
960    backoff_policy: Arc<dyn BackoffPolicy>,
961}
962
963impl GenerateAccessTokenClient {
964    fn new(
965        source_headers: HeaderMap,
966        delegates: Option<Vec<String>>,
967        scopes: Vec<String>,
968        lifetime: Duration,
969        url: String,
970    ) -> Self {
971        let retry_policy = Aip194Strict
972            .continue_on_too_many_requests()
973            .continue_on_client_timeout()
974            .with_time_limit(Duration::from_secs(60));
975        let backoff_policy = ExponentialBackoff::default();
976
977        Self {
978            source_headers,
979            delegates,
980            scopes,
981            lifetime,
982            url,
983            retry_policy: Arc::new(retry_policy),
984            backoff_policy: Arc::new(backoff_policy),
985        }
986    }
987
988    async fn fetch(self) -> Result<Token> {
989        let client = Client::new();
990        let body = GenerateAccessTokenRequest {
991            delegates: self.delegates,
992            scope: self.scopes,
993            lifetime: format!("{}s", self.lifetime.as_secs_f64()),
994        };
995
996        let sleep = async |d| tokio::time::sleep(d).await;
997        let retry_throttler: RetryThrottlerArg = AdaptiveThrottler::default().into();
998        let retry_throttler: SharedRetryThrottler = retry_throttler.into();
999        let url = self.url;
1000        let source_headers = self.source_headers;
1001
1002        let response = retry_loop(
1003            async move |d| {
1004                let attempt =
1005                    generate_access_token_call(&client, &url, source_headers.clone(), &body);
1006                let max_time_limit = Duration::from_secs(10);
1007                let time_limit = d.map_or(max_time_limit, |d| d.min(max_time_limit));
1008                match tokio::time::timeout(time_limit, attempt).await {
1009                    Ok(r) => r,
1010                    Err(e) => Err(GaxError::timeout(e)),
1011                }
1012            },
1013            sleep,
1014            true, // generate_access_token is idempotent
1015            retry_throttler,
1016            self.retry_policy.clone(),
1017            self.backoff_policy.clone(),
1018        )
1019        .await
1020        .map_err(|e| errors::from_gax_error(e, MSG))?;
1021
1022        let token_response = response
1023            .json::<GenerateAccessTokenResponse>()
1024            .await
1025            .map_err(|e| {
1026                let retryable = !e.is_decode();
1027                CredentialsError::from_source(retryable, e)
1028            })?;
1029
1030        let parsed_dt = OffsetDateTime::parse(
1031            &token_response.expire_time,
1032            &time::format_description::well_known::Rfc3339,
1033        )
1034        .map_err(errors::non_retryable)?;
1035
1036        let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
1037        let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
1038
1039        let token = Token {
1040            token: token_response.access_token,
1041            token_type: "Bearer".to_string(),
1042            expires_at: Some(expires_at),
1043            metadata: None,
1044        };
1045        Ok(token)
1046    }
1047}
1048
1049#[async_trait]
1050impl TokenProvider for ImpersonatedTokenProvider {
1051    async fn token(&self) -> Result<Token> {
1052        let source_headers = self.source_credentials.headers(Extensions::new()).await?;
1053        let source_headers = match source_headers {
1054            CacheableResource::New { data, .. } => data,
1055            CacheableResource::NotModified => {
1056                unreachable!("requested source credentials without a caching etag")
1057            }
1058        };
1059
1060        // We resolve the URL on every token call because fetching the universe domain
1061        // is async and must be done here rather than in the builder.
1062        // Since `token()` takes `&self`, we cannot mutate `self` to cache the URL
1063        // without using a lock for inner mutability, which is worse than just
1064        // building the URL string for each request.
1065        let url = self
1066            .service_account_impersonation_url
1067            .access_token_url(&self.source_credentials)
1068            .await;
1069
1070        generate_access_token(
1071            source_headers,
1072            self.delegates.clone(),
1073            self.scopes.clone(),
1074            self.lifetime,
1075            &url,
1076        )
1077        .await
1078    }
1079}
1080
1081#[derive(serde::Deserialize)]
1082struct GenerateAccessTokenResponse {
1083    #[serde(rename = "accessToken")]
1084    access_token: String,
1085    #[serde(rename = "expireTime")]
1086    expire_time: String,
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use super::*;
1092    use crate::credentials::service_account::ServiceAccountKey;
1093    use crate::credentials::tests::MockCredentials;
1094    use crate::credentials::tests::PKCS8_PK;
1095    use crate::credentials::tests::{
1096        find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
1097        get_mock_retry_throttler,
1098    };
1099    use crate::errors::CredentialsError;
1100    use crate::universe_domain::is_default_universe_domain;
1101    use base64::{Engine, prelude::BASE64_STANDARD};
1102    use httptest::cycle;
1103    use httptest::{Expectation, Server, matchers::*, responders::*};
1104    use serde_json::Value;
1105    use serde_json::json;
1106    use serial_test::parallel;
1107    use test_case::test_case;
1108
1109    type TestResult = anyhow::Result<()>;
1110
1111    impl Builder {
1112        fn maybe_iam_endpoint_override(mut self, iam_endpoint_override: Option<String>) -> Self {
1113            self.iam_endpoint_override = iam_endpoint_override;
1114            self
1115        }
1116
1117        fn without_access_boundary(mut self) -> Self {
1118            self.is_access_boundary_enabled = false;
1119            self
1120        }
1121
1122        fn with_impersonation_endpoint(mut self, endpoint: &str) -> Self {
1123            self.service_account_impersonation_url = self
1124                .service_account_impersonation_url
1125                .map(|u| u.with_endpoint(endpoint));
1126            self
1127        }
1128    }
1129
1130    impl ImpersonationUrl {
1131        pub(crate) fn with_endpoint(mut self, endpoint: &str) -> Self {
1132            self.endpoint = Some(endpoint.to_string());
1133            self
1134        }
1135    }
1136
1137    #[tokio::test]
1138    #[parallel]
1139    async fn test_generate_access_token_client_retry_success() -> TestResult {
1140        let server = Server::run();
1141        let impersonation_path =
1142            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1143        server.expect(
1144            Expectation::matching(request::method_path("POST", impersonation_path))
1145                .times(2)
1146                .respond_with(cycle![
1147                    status_code(503).body("try-again"),
1148                    json_encoded(json!({
1149                        "accessToken": "impersonated-token-success",
1150                        "expireTime": "2030-01-01T00:00:00Z",
1151                    })),
1152                ]),
1153        );
1154
1155        let url = server.url(impersonation_path).to_string();
1156        let mut client = GenerateAccessTokenClient::new(
1157            HeaderMap::new(),
1158            None,
1159            vec!["scope1".to_string()],
1160            Duration::from_secs(3600),
1161            url,
1162        );
1163        client.retry_policy = Arc::new(get_mock_auth_retry_policy(3));
1164        client.backoff_policy = Arc::new(get_mock_backoff_policy());
1165
1166        let token = client.fetch().await?;
1167        assert_eq!(token.token, "impersonated-token-success");
1168        Ok(())
1169    }
1170
1171    #[tokio::test]
1172    #[parallel]
1173    async fn test_generate_access_token_client_retry_exhausted() {
1174        let server = Server::run();
1175        let impersonation_path =
1176            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1177        server.expect(
1178            Expectation::matching(request::method_path("POST", impersonation_path))
1179                .times(3)
1180                .respond_with(status_code(503)),
1181        );
1182
1183        let url = server.url(impersonation_path).to_string();
1184        let mut client = GenerateAccessTokenClient::new(
1185            HeaderMap::new(),
1186            None,
1187            vec!["scope1".to_string()],
1188            Duration::from_secs(3600),
1189            url,
1190        );
1191        client.retry_policy = Arc::new(get_mock_auth_retry_policy(3));
1192        client.backoff_policy = Arc::new(get_mock_backoff_policy());
1193
1194        let err = client.fetch().await.unwrap_err();
1195        assert!(err.is_transient(), "{err:?}");
1196    }
1197
1198    #[tokio::test]
1199    #[parallel]
1200    async fn test_generate_access_token_success() -> TestResult {
1201        let server = Server::run();
1202        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1203            .format(&time::format_description::well_known::Rfc3339)
1204            .unwrap();
1205        server.expect(
1206            Expectation::matching(all_of![
1207                request::method_path(
1208                    "POST",
1209                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1210                ),
1211                request::headers(contains(("authorization", "Bearer test-token"))),
1212            ])
1213            .respond_with(json_encoded(json!({
1214                "accessToken": "test-impersonated-token",
1215                "expireTime": expire_time
1216            }))),
1217        );
1218
1219        let mut headers = HeaderMap::new();
1220        headers.insert("authorization", "Bearer test-token".parse().unwrap());
1221        let token = generate_access_token(
1222            headers,
1223            None,
1224            vec!["scope".to_string()],
1225            DEFAULT_LIFETIME,
1226            &server
1227                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1228                .to_string(),
1229        )
1230        .await?;
1231
1232        assert_eq!(token.token, "test-impersonated-token");
1233        Ok(())
1234    }
1235
1236    #[tokio::test]
1237    #[parallel]
1238    async fn test_generate_access_token_403() -> TestResult {
1239        let server = Server::run();
1240        server.expect(
1241            Expectation::matching(all_of![
1242                request::method_path(
1243                    "POST",
1244                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1245                ),
1246                request::headers(contains(("authorization", "Bearer test-token"))),
1247            ])
1248            .respond_with(status_code(403)),
1249        );
1250
1251        let mut headers = HeaderMap::new();
1252        headers.insert("authorization", "Bearer test-token".parse().unwrap());
1253        let err = generate_access_token(
1254            headers,
1255            None,
1256            vec!["scope".to_string()],
1257            DEFAULT_LIFETIME,
1258            &server
1259                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1260                .to_string(),
1261        )
1262        .await
1263        .unwrap_err();
1264
1265        assert!(!err.is_transient());
1266        Ok(())
1267    }
1268
1269    #[tokio::test]
1270    #[parallel]
1271    async fn test_generate_access_token_no_auth_header() -> TestResult {
1272        let server = Server::run();
1273        server.expect(
1274            Expectation::matching(request::method_path(
1275                "POST",
1276                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1277            ))
1278            .respond_with(status_code(401)),
1279        );
1280
1281        let err = generate_access_token(
1282            HeaderMap::new(),
1283            None,
1284            vec!["scope".to_string()],
1285            DEFAULT_LIFETIME,
1286            &server
1287                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
1288                .to_string(),
1289        )
1290        .await
1291        .unwrap_err();
1292
1293        assert!(!err.is_transient());
1294        Ok(())
1295    }
1296
1297    #[tokio::test]
1298    #[parallel]
1299    async fn test_impersonated_service_account() -> TestResult {
1300        let server = Server::run();
1301        server.expect(
1302            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1303                json_encoded(json!({
1304                    "access_token": "test-user-account-token",
1305                    "expires_in": 3600,
1306                    "token_type": "Bearer",
1307                })),
1308            ),
1309        );
1310        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1311            .format(&time::format_description::well_known::Rfc3339)
1312            .unwrap();
1313        server.expect(
1314            Expectation::matching(all_of![
1315                request::method_path(
1316                    "POST",
1317                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1318                ),
1319                request::headers(contains((
1320                    "authorization",
1321                    "Bearer test-user-account-token"
1322                ))),
1323                request::body(json_decoded(eq(json!({
1324                    "scope": ["scope1", "scope2"],
1325                    "lifetime": "3600s"
1326                }))))
1327            ])
1328            .respond_with(json_encoded(json!({
1329                "accessToken": "test-impersonated-token",
1330                "expireTime": expire_time
1331            }))),
1332        );
1333
1334        let impersonated_credential = json!({
1335            "type": "impersonated_service_account",
1336            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1337            "source_credentials": {
1338                "type": "authorized_user",
1339                "client_id": "test-client-id",
1340                "client_secret": "test-client-secret",
1341                "refresh_token": "test-refresh-token",
1342                "token_uri": server.url("/token").to_string()
1343            }
1344        });
1345        let (token_provider, _, _) = Builder::new(impersonated_credential)
1346            .with_scopes(vec!["scope1", "scope2"])
1347            .build_components()?;
1348
1349        let token = token_provider.token().await?;
1350        assert_eq!(token.token, "test-impersonated-token");
1351        assert_eq!(token.token_type, "Bearer");
1352
1353        Ok(())
1354    }
1355
1356    #[tokio::test]
1357    #[parallel]
1358    async fn test_impersonated_service_account_default_scope() -> TestResult {
1359        let server = Server::run();
1360        server.expect(
1361            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1362                json_encoded(json!({
1363                    "access_token": "test-user-account-token",
1364                    "expires_in": 3600,
1365                    "token_type": "Bearer",
1366                })),
1367            ),
1368        );
1369        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1370            .format(&time::format_description::well_known::Rfc3339)
1371            .unwrap();
1372        server.expect(
1373            Expectation::matching(all_of![
1374                request::method_path(
1375                    "POST",
1376                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1377                ),
1378                request::headers(contains((
1379                    "authorization",
1380                    "Bearer test-user-account-token"
1381                ))),
1382                request::body(json_decoded(eq(json!({
1383                    "scope": [DEFAULT_SCOPE],
1384                    "lifetime": "3600s"
1385                }))))
1386            ])
1387            .respond_with(json_encoded(json!({
1388                "accessToken": "test-impersonated-token",
1389                "expireTime": expire_time
1390            }))),
1391        );
1392
1393        let impersonated_credential = json!({
1394            "type": "impersonated_service_account",
1395            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1396            "source_credentials": {
1397                "type": "authorized_user",
1398                "client_id": "test-client-id",
1399                "client_secret": "test-client-secret",
1400                "refresh_token": "test-refresh-token",
1401                "token_uri": server.url("/token").to_string()
1402            }
1403        });
1404        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1405
1406        let token = token_provider.token().await?;
1407        assert_eq!(token.token, "test-impersonated-token");
1408        assert_eq!(token.token_type, "Bearer");
1409
1410        Ok(())
1411    }
1412
1413    #[tokio::test]
1414    #[parallel]
1415    async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
1416        let server = Server::run();
1417        server.expect(
1418            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1419                json_encoded(json!({
1420                    "access_token": "test-user-account-token",
1421                    "expires_in": 3600,
1422                    "token_type": "Bearer",
1423                })),
1424            ),
1425        );
1426        let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
1427            .format(&time::format_description::well_known::Rfc3339)
1428            .unwrap();
1429        server.expect(
1430            Expectation::matching(all_of![
1431                request::method_path(
1432                    "POST",
1433                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1434                ),
1435                request::headers(contains((
1436                    "authorization",
1437                    "Bearer test-user-account-token"
1438                ))),
1439                request::body(json_decoded(eq(json!({
1440                    "scope": ["scope1", "scope2"],
1441                    "lifetime": "3.5s"
1442                }))))
1443            ])
1444            .respond_with(json_encoded(json!({
1445                "accessToken": "test-impersonated-token",
1446                "expireTime": expire_time
1447            }))),
1448        );
1449
1450        let impersonated_credential = json!({
1451            "type": "impersonated_service_account",
1452            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1453            "source_credentials": {
1454                "type": "authorized_user",
1455                "client_id": "test-client-id",
1456                "client_secret": "test-client-secret",
1457                "refresh_token": "test-refresh-token",
1458                "token_uri": server.url("/token").to_string()
1459            }
1460        });
1461        let (token_provider, _, _) = Builder::new(impersonated_credential)
1462            .with_scopes(vec!["scope1", "scope2"])
1463            .with_lifetime(Duration::from_secs_f32(3.5))
1464            .build_components()?;
1465
1466        let token = token_provider.token().await?;
1467        assert_eq!(token.token, "test-impersonated-token");
1468
1469        Ok(())
1470    }
1471
1472    #[tokio::test]
1473    #[parallel]
1474    async fn test_with_delegates() -> TestResult {
1475        let server = Server::run();
1476        server.expect(
1477            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1478                json_encoded(json!({
1479                    "access_token": "test-user-account-token",
1480                    "expires_in": 3600,
1481                    "token_type": "Bearer",
1482                })),
1483            ),
1484        );
1485        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1486            .format(&time::format_description::well_known::Rfc3339)
1487            .unwrap();
1488        server.expect(
1489            Expectation::matching(all_of![
1490                request::method_path(
1491                    "POST",
1492                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1493                ),
1494                request::headers(contains((
1495                    "authorization",
1496                    "Bearer test-user-account-token"
1497                ))),
1498                request::body(json_decoded(eq(json!({
1499                    "scope": [DEFAULT_SCOPE],
1500                    "lifetime": "3600s",
1501                    "delegates": ["delegate1", "delegate2"]
1502                }))))
1503            ])
1504            .respond_with(json_encoded(json!({
1505                "accessToken": "test-impersonated-token",
1506                "expireTime": expire_time
1507            }))),
1508        );
1509
1510        let impersonated_credential = json!({
1511            "type": "impersonated_service_account",
1512            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1513            "source_credentials": {
1514                "type": "authorized_user",
1515                "client_id": "test-client-id",
1516                "client_secret": "test-client-secret",
1517                "refresh_token": "test-refresh-token",
1518                "token_uri": server.url("/token").to_string()
1519            }
1520        });
1521        let (token_provider, _, _) = Builder::new(impersonated_credential)
1522            .with_delegates(vec!["delegate1", "delegate2"])
1523            .build_components()?;
1524
1525        let token = token_provider.token().await?;
1526        assert_eq!(token.token, "test-impersonated-token");
1527        assert_eq!(token.token_type, "Bearer");
1528
1529        Ok(())
1530    }
1531
1532    #[tokio::test]
1533    #[parallel]
1534    async fn test_impersonated_service_account_fail() -> TestResult {
1535        let server = Server::run();
1536        server.expect(
1537            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1538                json_encoded(json!({
1539                    "access_token": "test-user-account-token",
1540                    "expires_in": 3600,
1541                    "token_type": "Bearer",
1542                })),
1543            ),
1544        );
1545        server.expect(
1546            Expectation::matching(request::method_path(
1547                "POST",
1548                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1549            ))
1550            .respond_with(status_code(500)),
1551        );
1552
1553        let impersonated_credential = json!({
1554            "type": "impersonated_service_account",
1555            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1556            "source_credentials": {
1557                "type": "authorized_user",
1558                "client_id": "test-client-id",
1559                "client_secret": "test-client-secret",
1560                "refresh_token": "test-refresh-token",
1561                "token_uri": server.url("/token").to_string()
1562            }
1563        });
1564        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1565
1566        let err = token_provider.token().await.unwrap_err();
1567        let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1568        assert!(original_err.is_transient());
1569
1570        Ok(())
1571    }
1572
1573    #[tokio::test]
1574    #[parallel]
1575    async fn debug_token_provider() {
1576        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1577            "type": "authorized_user",
1578            "client_id": "test-client-id",
1579            "client_secret": "test-client-secret",
1580            "refresh_token": "test-refresh-token"
1581        }))
1582        .build()
1583        .unwrap();
1584
1585        let expected = ImpersonatedTokenProvider {
1586            source_credentials,
1587            service_account_impersonation_url: ImpersonationUrl::exact(
1588                "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1589            ),
1590            delegates: Some(vec!["delegate1".to_string()]),
1591            scopes: vec!["scope1".to_string()],
1592            lifetime: Duration::from_secs(3600),
1593        };
1594        let fmt = format!("{expected:?}");
1595        assert!(fmt.contains("UserCredentials"), "{fmt}");
1596        assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1597        assert!(fmt.contains("delegate1"), "{fmt}");
1598        assert!(fmt.contains("scope1"), "{fmt}");
1599        assert!(fmt.contains("3600s"), "{fmt}");
1600    }
1601
1602    #[test]
1603    fn impersonated_config_full_from_json_success() {
1604        let source_credentials_json = json!({
1605            "type": "authorized_user",
1606            "client_id": "test-client-id",
1607            "client_secret": "test-client-secret",
1608            "refresh_token": "test-refresh-token"
1609        });
1610        let json = json!({
1611            "type": "impersonated_service_account",
1612            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1613            "source_credentials": source_credentials_json,
1614            "delegates": ["delegate1"],
1615            "quota_project_id": "test-project-id",
1616            "scopes": ["scope1"],
1617        });
1618
1619        let expected = ImpersonatedConfig {
1620            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1621            source_credentials: source_credentials_json,
1622            delegates: Some(vec!["delegate1".to_string()]),
1623            quota_project_id: Some("test-project-id".to_string()),
1624            scopes: Some(vec!["scope1".to_string()]),
1625            universe_domain: None,
1626        };
1627        let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1628        assert_eq!(actual, expected);
1629    }
1630
1631    #[test]
1632    fn impersonated_config_partial_from_json_success() {
1633        let source_credentials_json = json!({
1634            "type": "authorized_user",
1635            "client_id": "test-client-id",
1636            "client_secret": "test-client-secret",
1637            "refresh_token": "test-refresh-token"
1638        });
1639        let json = json!({
1640            "type": "impersonated_service_account",
1641            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1642            "source_credentials": source_credentials_json
1643        });
1644
1645        let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1646        assert_eq!(
1647            config.service_account_impersonation_url,
1648            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1649        );
1650        assert_eq!(config.source_credentials, source_credentials_json);
1651        assert_eq!(config.delegates, None);
1652        assert_eq!(config.quota_project_id, None);
1653        assert_eq!(config.scopes, None);
1654    }
1655
1656    #[tokio::test]
1657    #[parallel]
1658    async fn test_impersonated_service_account_source_fail() -> TestResult {
1659        let mut mock = MockCredentials::new();
1660        mock.expect_headers()
1661            .returning(|_| Err(errors::non_retryable_from_str("source failed")));
1662        let source_credentials = Credentials::from(mock);
1663
1664        let token_provider = ImpersonatedTokenProvider {
1665            source_credentials,
1666            service_account_impersonation_url: ImpersonationUrl::exact(
1667                "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1668            ),
1669            delegates: Some(vec!["delegate1".to_string()]),
1670            scopes: vec!["scope1".to_string()],
1671            lifetime: DEFAULT_LIFETIME,
1672        };
1673
1674        let err = token_provider.token().await.unwrap_err();
1675        assert!(err.to_string().contains("source failed"));
1676
1677        Ok(())
1678    }
1679
1680    #[tokio::test]
1681    #[parallel]
1682    async fn test_missing_impersonation_url_fail() {
1683        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1684            "type": "authorized_user",
1685            "client_id": "test-client-id",
1686            "client_secret": "test-client-secret",
1687            "refresh_token": "test-refresh-token"
1688        }))
1689        .build()
1690        .unwrap();
1691
1692        let result = Builder::from_source_credentials(source_credentials).build();
1693        assert!(result.is_err(), "{result:?}");
1694        let err = result.unwrap_err();
1695        assert!(err.is_parsing());
1696        assert!(
1697            err.to_string()
1698                .contains("`service_account_impersonation_url` is required")
1699        );
1700    }
1701
1702    #[tokio::test]
1703    #[parallel]
1704    async fn test_nested_impersonated_credentials_fail() {
1705        let nested_impersonated = json!({
1706            "type": "impersonated_service_account",
1707            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1708            "source_credentials": {
1709                "type": "impersonated_service_account",
1710                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1711                "source_credentials": {
1712                    "type": "authorized_user",
1713                    "client_id": "test-client-id",
1714                    "client_secret": "test-client-secret",
1715                    "refresh_token": "test-refresh-token"
1716                }
1717            }
1718        });
1719
1720        let result = Builder::new(nested_impersonated).build();
1721        assert!(result.is_err(), "{result:?}");
1722        let err = result.unwrap_err();
1723        assert!(err.is_parsing());
1724        assert!(
1725            err.to_string().contains(
1726                "source credential of type `impersonated_service_account` is not supported"
1727            )
1728        );
1729    }
1730
1731    #[tokio::test]
1732    #[parallel]
1733    async fn test_malformed_impersonated_credentials_fail() {
1734        let malformed_impersonated = json!({
1735            "type": "impersonated_service_account",
1736            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1737        });
1738
1739        let result = Builder::new(malformed_impersonated).build();
1740        assert!(result.is_err(), "{result:?}");
1741        let err = result.unwrap_err();
1742        assert!(err.is_parsing());
1743        assert!(
1744            err.to_string()
1745                .contains("missing field `source_credentials`")
1746        );
1747    }
1748
1749    #[tokio::test]
1750    #[parallel]
1751    async fn test_invalid_source_credential_type_fail() {
1752        let invalid_source = json!({
1753            "type": "impersonated_service_account",
1754            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1755            "source_credentials": {
1756                "type": "invalid_type",
1757            }
1758        });
1759
1760        let result = Builder::new(invalid_source).build();
1761        assert!(result.is_err(), "{result:?}");
1762        let err = result.unwrap_err();
1763        assert!(err.is_unknown_type());
1764    }
1765
1766    #[tokio::test]
1767    #[parallel]
1768    async fn test_missing_expiry() -> TestResult {
1769        let server = Server::run();
1770        server.expect(
1771            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1772                json_encoded(json!({
1773                    "access_token": "test-user-account-token",
1774                    "expires_in": 3600,
1775                    "token_type": "Bearer",
1776                })),
1777            ),
1778        );
1779        server.expect(
1780            Expectation::matching(request::method_path(
1781                "POST",
1782                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1783            ))
1784            .respond_with(json_encoded(json!({
1785                "accessToken": "test-impersonated-token",
1786            }))),
1787        );
1788
1789        let impersonated_credential = json!({
1790            "type": "impersonated_service_account",
1791            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1792            "source_credentials": {
1793                "type": "authorized_user",
1794                "client_id": "test-client-id",
1795                "client_secret": "test-client-secret",
1796                "refresh_token": "test-refresh-token",
1797                "token_uri": server.url("/token").to_string()
1798            }
1799        });
1800        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1801
1802        let err = token_provider.token().await.unwrap_err();
1803        assert!(!err.is_transient());
1804
1805        Ok(())
1806    }
1807
1808    #[tokio::test]
1809    #[parallel]
1810    async fn test_invalid_expiry_format() -> TestResult {
1811        let server = Server::run();
1812        server.expect(
1813            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1814                json_encoded(json!({
1815                    "access_token": "test-user-account-token",
1816                    "expires_in": 3600,
1817                    "token_type": "Bearer",
1818                })),
1819            ),
1820        );
1821        server.expect(
1822            Expectation::matching(request::method_path(
1823                "POST",
1824                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1825            ))
1826            .respond_with(json_encoded(json!({
1827                "accessToken": "test-impersonated-token",
1828                "expireTime": "invalid-format"
1829            }))),
1830        );
1831
1832        let impersonated_credential = json!({
1833            "type": "impersonated_service_account",
1834            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1835            "source_credentials": {
1836                "type": "authorized_user",
1837                "client_id": "test-client-id",
1838                "client_secret": "test-client-secret",
1839                "refresh_token": "test-refresh-token",
1840                "token_uri": server.url("/token").to_string()
1841            }
1842        });
1843        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1844
1845        let err = token_provider.token().await.unwrap_err();
1846        assert!(!err.is_transient());
1847
1848        Ok(())
1849    }
1850
1851    #[tokio::test]
1852    #[parallel]
1853    async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1854        let server = Server::run();
1855        server.expect(
1856            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1857                json_encoded(json!({
1858                    "access_token": "test-user-account-token",
1859                    "expires_in": 3600,
1860                    "token_type": "Bearer",
1861                })),
1862            ),
1863        );
1864        server.expect(
1865            Expectation::matching(request::method_path(
1866                "POST",
1867                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1868            ))
1869            .respond_with(json_encoded(json!("bad json"))),
1870        );
1871
1872        let impersonated_credential = json!({
1873            "type": "impersonated_service_account",
1874            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1875            "source_credentials": {
1876                "type": "authorized_user",
1877                "client_id": "test-client-id",
1878                "client_secret": "test-client-secret",
1879                "refresh_token": "test-refresh-token",
1880                "token_uri": server.url("/token").to_string()
1881            }
1882        });
1883        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1884
1885        let e = token_provider.token().await.err().unwrap();
1886        assert!(!e.is_transient(), "{e}");
1887
1888        Ok(())
1889    }
1890
1891    #[tokio::test]
1892    #[parallel]
1893    async fn token_provider_nonretryable_error() -> TestResult {
1894        let server = Server::run();
1895        server.expect(
1896            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1897                json_encoded(json!({
1898                    "access_token": "test-user-account-token",
1899                    "expires_in": 3600,
1900                    "token_type": "Bearer",
1901                })),
1902            ),
1903        );
1904        server.expect(
1905            Expectation::matching(request::method_path(
1906                "POST",
1907                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1908            ))
1909            .respond_with(status_code(401)),
1910        );
1911
1912        let impersonated_credential = json!({
1913            "type": "impersonated_service_account",
1914            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1915            "source_credentials": {
1916                "type": "authorized_user",
1917                "client_id": "test-client-id",
1918                "client_secret": "test-client-secret",
1919                "refresh_token": "test-refresh-token",
1920                "token_uri": server.url("/token").to_string()
1921            }
1922        });
1923        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
1924
1925        let err = token_provider.token().await.unwrap_err();
1926        assert!(!err.is_transient());
1927
1928        Ok(())
1929    }
1930
1931    #[tokio::test]
1932    #[parallel]
1933    async fn credential_full_with_quota_project_from_builder() -> TestResult {
1934        let server = Server::run();
1935        server.expect(
1936            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1937                json_encoded(json!({
1938                    "access_token": "test-user-account-token",
1939                    "expires_in": 3600,
1940                    "token_type": "Bearer",
1941                })),
1942            ),
1943        );
1944        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1945            .format(&time::format_description::well_known::Rfc3339)
1946            .unwrap();
1947        server.expect(
1948            Expectation::matching(request::method_path(
1949                "POST",
1950                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1951            ))
1952            .respond_with(json_encoded(json!({
1953                "accessToken": "test-impersonated-token",
1954                "expireTime": expire_time
1955            }))),
1956        );
1957
1958        let impersonated_credential = json!({
1959            "type": "impersonated_service_account",
1960            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1961            "source_credentials": {
1962                "type": "authorized_user",
1963                "client_id": "test-client-id",
1964                "client_secret": "test-client-secret",
1965                "refresh_token": "test-refresh-token",
1966                "token_uri": server.url("/token").to_string()
1967            }
1968        });
1969        let creds = Builder::new(impersonated_credential)
1970            .with_quota_project_id("test-project")
1971            .build()?;
1972
1973        let headers = creds.headers(Extensions::new()).await?;
1974        match headers {
1975            CacheableResource::New { data, .. } => {
1976                assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1977            }
1978            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1979        }
1980
1981        Ok(())
1982    }
1983
1984    #[tokio::test]
1985    #[parallel]
1986    async fn access_token_credentials_success() -> TestResult {
1987        let server = Server::run();
1988        server.expect(
1989            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1990                json_encoded(json!({
1991                    "access_token": "test-user-account-token",
1992                    "expires_in": 3600,
1993                    "token_type": "Bearer",
1994                })),
1995            ),
1996        );
1997        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1998            .format(&time::format_description::well_known::Rfc3339)
1999            .unwrap();
2000        server.expect(
2001            Expectation::matching(request::method_path(
2002                "POST",
2003                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2004            ))
2005            .respond_with(json_encoded(json!({
2006                "accessToken": "test-impersonated-token",
2007                "expireTime": expire_time
2008            }))),
2009        );
2010
2011        let impersonated_credential = json!({
2012            "type": "impersonated_service_account",
2013            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2014            "source_credentials": {
2015                "type": "authorized_user",
2016                "client_id": "test-client-id",
2017                "client_secret": "test-client-secret",
2018                "refresh_token": "test-refresh-token",
2019                "token_uri": server.url("/token").to_string()
2020            }
2021        });
2022        let creds = Builder::new(impersonated_credential).build_access_token_credentials()?;
2023
2024        let access_token = creds.access_token().await?;
2025        assert_eq!(access_token.token, "test-impersonated-token");
2026
2027        Ok(())
2028    }
2029
2030    #[tokio::test]
2031    #[parallel]
2032    async fn test_with_target_principal() {
2033        let source_credentials = crate::credentials::user_account::Builder::new(json!({
2034            "type": "authorized_user",
2035            "client_id": "test-client-id",
2036            "client_secret": "test-client-secret",
2037            "refresh_token": "test-refresh-token"
2038        }))
2039        .build()
2040        .unwrap();
2041
2042        let (token_provider, _, _) = Builder::from_source_credentials(source_credentials.clone())
2043            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2044            .build_components()
2045            .unwrap();
2046
2047        let url = token_provider
2048            .inner
2049            .service_account_impersonation_url
2050            .access_token_url(&source_credentials)
2051            .await;
2052
2053        assert_eq!(
2054            url,
2055            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
2056        );
2057    }
2058
2059    fn source_creds_with_universe_domain(source_universe_domain: &str) -> Value {
2060        serde_json::json!({
2061            "type": "service_account",
2062            "client_email": "test-client-email",
2063            "private_key_id": "test-private-key-id",
2064            "private_key": Value::from(PKCS8_PK.clone()),
2065            "project_id": "test-project-id",
2066            "universe_domain": source_universe_domain,
2067        })
2068    }
2069
2070    fn service_account_builder_with_universe(source_universe_domain: &str) -> Builder {
2071        let source_creds = source_creds_with_universe_domain(source_universe_domain);
2072        let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2073            .build()
2074            .expect("Failed to build service account credentials");
2075        Builder::from_source_credentials(source_credentials)
2076            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2077    }
2078
2079    fn mds_builder() -> Builder {
2080        let source_credentials = crate::credentials::mds::Builder::default()
2081            .build()
2082            .expect("Failed to build MDS credentials");
2083        Builder::from_source_credentials(source_credentials)
2084            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
2085    }
2086
2087    fn json_builder_with_universe(source_universe_domain: &str) -> Builder {
2088        let source_creds = source_creds_with_universe_domain(source_universe_domain);
2089        let impersonated_credential = serde_json::json!({
2090            "type": "impersonated_service_account",
2091            "service_account_impersonation_url": "https://iamcredentials.my-custom-universe.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2092            "source_credentials": source_creds,
2093        });
2094        Builder::new(impersonated_credential)
2095    }
2096
2097    #[test_case(service_account_builder_with_universe("my-custom-universe.com"); "service account as source")]
2098    #[test_case(json_builder_with_universe("my-custom-universe.com"); "credentials from json")]
2099    #[parallel]
2100    #[tokio::test]
2101    async fn universe_domain_from_source(builder: Builder) -> TestResult {
2102        let creds = builder.build()?;
2103        let universe_domain = creds.universe_domain().await;
2104
2105        assert_eq!(universe_domain.as_deref(), Some("my-custom-universe.com"));
2106
2107        Ok(())
2108    }
2109
2110    #[tokio::test]
2111    #[parallel]
2112    async fn universe_domain_mds_source() -> TestResult {
2113        let builder = mds_builder();
2114        let creds = builder.build()?;
2115        let universe_domain = creds.universe_domain().await;
2116
2117        assert!(is_default_universe_domain(universe_domain.as_deref()));
2118
2119        Ok(())
2120    }
2121
2122    #[test_case(service_account_builder_with_universe("my-custom-universe.com"); "service account as source")]
2123    #[test_case(json_builder_with_universe("my-custom-universe.com"); "credentials from json")]
2124    #[test_case(mds_builder(); "mds as source")]
2125    #[tokio::test]
2126    #[parallel]
2127    async fn universe_domain_override(builder: Builder) -> TestResult {
2128        let creds = builder
2129            .with_universe_domain("another-universe.com")
2130            .build()?;
2131
2132        let universe_domain = creds.universe_domain().await;
2133
2134        assert_eq!(universe_domain.as_deref(), Some("another-universe.com"));
2135
2136        Ok(())
2137    }
2138
2139    #[tokio::test]
2140    #[parallel]
2141    async fn credential_full_with_quota_project_from_json() -> TestResult {
2142        let server = Server::run();
2143        server.expect(
2144            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2145                json_encoded(json!({
2146                    "access_token": "test-user-account-token",
2147                    "expires_in": 3600,
2148                    "token_type": "Bearer",
2149                })),
2150            ),
2151        );
2152        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2153            .format(&time::format_description::well_known::Rfc3339)
2154            .unwrap();
2155        server.expect(
2156            Expectation::matching(request::method_path(
2157                "POST",
2158                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2159            ))
2160            .respond_with(json_encoded(json!({
2161                "accessToken": "test-impersonated-token",
2162                "expireTime": expire_time
2163            }))),
2164        );
2165
2166        let impersonated_credential = json!({
2167            "type": "impersonated_service_account",
2168            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2169            "source_credentials": {
2170                "type": "authorized_user",
2171                "client_id": "test-client-id",
2172                "client_secret": "test-client-secret",
2173                "refresh_token": "test-refresh-token",
2174                "token_uri": server.url("/token").to_string()
2175            },
2176            "quota_project_id": "test-project-from-json",
2177        });
2178
2179        let creds = Builder::new(impersonated_credential).build()?;
2180
2181        let headers = creds.headers(Extensions::new()).await?;
2182        println!("headers: {:#?}", headers);
2183        match headers {
2184            CacheableResource::New { data, .. } => {
2185                assert_eq!(
2186                    data.get("x-goog-user-project").unwrap(),
2187                    "test-project-from-json"
2188                );
2189            }
2190            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
2191        }
2192
2193        Ok(())
2194    }
2195
2196    #[tokio::test]
2197    #[parallel]
2198    async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
2199        let server = Server::run();
2200
2201        // Expectation for the source credential token request.
2202        // It should NOT have any scopes in the body.
2203        server.expect(
2204            Expectation::matching(all_of![
2205                request::method_path("POST", "/source_token"),
2206                request::body(json_decoded(
2207                    |body: &serde_json::Value| body["scopes"].is_null()
2208                ))
2209            ])
2210            .respond_with(json_encoded(json!({
2211                "access_token": "source-token",
2212                "expires_in": 3600,
2213                "token_type": "Bearer",
2214            }))),
2215        );
2216
2217        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2218            .format(&time::format_description::well_known::Rfc3339)
2219            .unwrap();
2220
2221        // Expectation for the impersonation request.
2222        // It SHOULD have the scopes from the impersonated builder.
2223        server.expect(
2224            Expectation::matching(all_of![
2225                request::method_path(
2226                    "POST",
2227                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2228                ),
2229                request::headers(contains(("authorization", "Bearer source-token"))),
2230                request::body(json_decoded(eq(json!({
2231                    "scope": ["impersonated-scope"],
2232                    "lifetime": "3600s"
2233                }))))
2234            ])
2235            .respond_with(json_encoded(json!({
2236                "accessToken": "impersonated-token",
2237                "expireTime": expire_time
2238            }))),
2239        );
2240
2241        let impersonated_credential = json!({
2242            "type": "impersonated_service_account",
2243            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2244            "source_credentials": {
2245                "type": "authorized_user",
2246                "client_id": "test-client-id",
2247                "client_secret": "test-client-secret",
2248                "refresh_token": "test-refresh-token",
2249                "token_uri": server.url("/source_token").to_string()
2250            }
2251        });
2252
2253        let creds = Builder::new(impersonated_credential)
2254            .with_scopes(vec!["impersonated-scope"])
2255            .with_quota_project_id("impersonated-quota-project")
2256            .build()?;
2257
2258        // The quota project should be set on the final credentials object.
2259        let fmt = format!("{creds:?}");
2260        assert!(fmt.contains("impersonated-quota-project"));
2261
2262        // Fetching the token will trigger the mock server expectations.
2263        let _token = creds.headers(Extensions::new()).await?;
2264
2265        Ok(())
2266    }
2267
2268    #[tokio::test]
2269    #[parallel]
2270    async fn test_impersonated_metrics_header() -> TestResult {
2271        let server = Server::run();
2272        server.expect(
2273            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2274                json_encoded(json!({
2275                    "access_token": "test-user-account-token",
2276                    "expires_in": 3600,
2277                    "token_type": "Bearer",
2278                })),
2279            ),
2280        );
2281        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2282            .format(&time::format_description::well_known::Rfc3339)
2283            .unwrap();
2284        server.expect(
2285            Expectation::matching(all_of![
2286                request::method_path(
2287                    "POST",
2288                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2289                ),
2290                request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
2291                request::headers(contains((
2292                    "x-goog-api-client",
2293                    matches("auth-request-type/at")
2294                )))
2295            ])
2296            .respond_with(json_encoded(json!({
2297                "accessToken": "test-impersonated-token",
2298                "expireTime": expire_time
2299            }))),
2300        );
2301
2302        let impersonated_credential = json!({
2303            "type": "impersonated_service_account",
2304            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2305            "source_credentials": {
2306                "type": "authorized_user",
2307                "client_id": "test-client-id",
2308                "client_secret": "test-client-secret",
2309                "refresh_token": "test-refresh-token",
2310                "token_uri": server.url("/token").to_string()
2311            }
2312        });
2313        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
2314
2315        let token = token_provider.token().await?;
2316        assert_eq!(token.token, "test-impersonated-token");
2317        assert_eq!(token.token_type, "Bearer");
2318
2319        Ok(())
2320    }
2321
2322    #[tokio::test]
2323    #[parallel]
2324    async fn test_impersonated_retries_for_success() -> TestResult {
2325        let mut server = Server::run();
2326        // Source credential token endpoint
2327        server.expect(
2328            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2329                json_encoded(json!({
2330                    "access_token": "test-user-account-token",
2331                    "expires_in": 3600,
2332                    "token_type": "Bearer",
2333                })),
2334            ),
2335        );
2336
2337        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2338            .format(&time::format_description::well_known::Rfc3339)
2339            .unwrap();
2340
2341        // Impersonation endpoint
2342        let impersonation_path =
2343            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2344        server.expect(
2345            Expectation::matching(request::method_path("POST", impersonation_path))
2346                .times(3)
2347                .respond_with(cycle![
2348                    status_code(503).body("try-again"),
2349                    status_code(503).body("try-again"),
2350                    status_code(200)
2351                        .append_header("Content-Type", "application/json")
2352                        .body(
2353                            json!({
2354                                "accessToken": "test-impersonated-token",
2355                                "expireTime": expire_time
2356                            })
2357                            .to_string()
2358                        ),
2359                ]),
2360        );
2361
2362        let impersonated_credential = json!({
2363            "type": "impersonated_service_account",
2364            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2365            "source_credentials": {
2366                "type": "authorized_user",
2367                "client_id": "test-client-id",
2368                "client_secret": "test-client-secret",
2369                "refresh_token": "test-refresh-token",
2370                "token_uri": server.url("/token").to_string()
2371            }
2372        });
2373
2374        let (token_provider, _, _) = Builder::new(impersonated_credential)
2375            .with_retry_policy(get_mock_auth_retry_policy(3))
2376            .with_backoff_policy(get_mock_backoff_policy())
2377            .with_retry_throttler(get_mock_retry_throttler())
2378            .build_components()?;
2379
2380        let token = token_provider.token().await?;
2381        assert_eq!(token.token, "test-impersonated-token");
2382
2383        server.verify_and_clear();
2384        Ok(())
2385    }
2386
2387    #[tokio::test]
2388    #[parallel]
2389    async fn test_scopes_from_json() -> TestResult {
2390        let server = Server::run();
2391        server.expect(
2392            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2393                json_encoded(json!({
2394                    "access_token": "test-user-account-token",
2395                    "expires_in": 3600,
2396                    "token_type": "Bearer",
2397                })),
2398            ),
2399        );
2400        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2401            .format(&time::format_description::well_known::Rfc3339)
2402            .unwrap();
2403        server.expect(
2404            Expectation::matching(all_of![
2405                request::method_path(
2406                    "POST",
2407                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2408                ),
2409                request::body(json_decoded(eq(json!({
2410                    "scope": ["scope-from-json"],
2411                    "lifetime": "3600s"
2412                }))))
2413            ])
2414            .respond_with(json_encoded(json!({
2415                "accessToken": "test-impersonated-token",
2416                "expireTime": expire_time
2417            }))),
2418        );
2419
2420        let impersonated_credential = json!({
2421            "type": "impersonated_service_account",
2422            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2423            "scopes": ["scope-from-json"],
2424            "source_credentials": {
2425                "type": "authorized_user",
2426                "client_id": "test-client-id",
2427                "client_secret": "test-client-secret",
2428                "refresh_token": "test-refresh-token",
2429                "token_uri": server.url("/token").to_string()
2430            }
2431        });
2432        let (token_provider, _, _) = Builder::new(impersonated_credential).build_components()?;
2433
2434        let token = token_provider.token().await?;
2435        assert_eq!(token.token, "test-impersonated-token");
2436
2437        Ok(())
2438    }
2439
2440    #[tokio::test]
2441    #[parallel]
2442    async fn test_with_scopes_overrides_json_scopes() -> TestResult {
2443        let server = Server::run();
2444        server.expect(
2445            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2446                json_encoded(json!({
2447                    "access_token": "test-user-account-token",
2448                    "expires_in": 3600,
2449                    "token_type": "Bearer",
2450                })),
2451            ),
2452        );
2453        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2454            .format(&time::format_description::well_known::Rfc3339)
2455            .unwrap();
2456        server.expect(
2457            Expectation::matching(all_of![
2458                request::method_path(
2459                    "POST",
2460                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2461                ),
2462                request::body(json_decoded(eq(json!({
2463                    "scope": ["scope-from-with-scopes"],
2464                    "lifetime": "3600s"
2465                }))))
2466            ])
2467            .respond_with(json_encoded(json!({
2468                "accessToken": "test-impersonated-token",
2469                "expireTime": expire_time
2470            }))),
2471        );
2472
2473        let impersonated_credential = json!({
2474            "type": "impersonated_service_account",
2475            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2476            "scopes": ["scope-from-json"],
2477            "source_credentials": {
2478                "type": "authorized_user",
2479                "client_id": "test-client-id",
2480                "client_secret": "test-client-secret",
2481                "refresh_token": "test-refresh-token",
2482                "token_uri": server.url("/token").to_string()
2483            }
2484        });
2485        let (token_provider, _, _) = Builder::new(impersonated_credential)
2486            .with_scopes(vec!["scope-from-with-scopes"])
2487            .build_components()?;
2488
2489        let token = token_provider.token().await?;
2490        assert_eq!(token.token, "test-impersonated-token");
2491
2492        Ok(())
2493    }
2494
2495    #[tokio::test]
2496    #[parallel]
2497    async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
2498        let mut server = Server::run();
2499        // Source credential token endpoint
2500        server.expect(
2501            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2502                json_encoded(json!({
2503                    "access_token": "test-user-account-token",
2504                    "expires_in": 3600,
2505                    "token_type": "Bearer",
2506                })),
2507            ),
2508        );
2509
2510        // Impersonation endpoint
2511        let impersonation_path =
2512            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2513        server.expect(
2514            Expectation::matching(request::method_path("POST", impersonation_path))
2515                .times(1)
2516                .respond_with(status_code(401)),
2517        );
2518
2519        let impersonated_credential = json!({
2520            "type": "impersonated_service_account",
2521            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2522            "source_credentials": {
2523                "type": "authorized_user",
2524                "client_id": "test-client-id",
2525                "client_secret": "test-client-secret",
2526                "refresh_token": "test-refresh-token",
2527                "token_uri": server.url("/token").to_string()
2528            }
2529        });
2530
2531        let (token_provider, _, _) = Builder::new(impersonated_credential)
2532            .with_retry_policy(get_mock_auth_retry_policy(3))
2533            .with_backoff_policy(get_mock_backoff_policy())
2534            .with_retry_throttler(get_mock_retry_throttler())
2535            .build_components()?;
2536
2537        let err = token_provider.token().await.unwrap_err();
2538        assert!(!err.is_transient());
2539
2540        server.verify_and_clear();
2541        Ok(())
2542    }
2543
2544    #[tokio::test]
2545    #[parallel]
2546    async fn test_impersonated_remote_signer() -> TestResult {
2547        let server = Server::run();
2548        server.expect(
2549            Expectation::matching(request::method_path("POST", "/token"))
2550                .times(2..)
2551                .respond_with(json_encoded(json!({
2552                    "access_token": "test-user-account-token",
2553                    "expires_in": 3600,
2554                    "token_type": "Bearer",
2555                }))),
2556        );
2557        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2558            .format(&time::format_description::well_known::Rfc3339)
2559            .unwrap();
2560        server.expect(
2561            Expectation::matching(request::method_path(
2562                "POST",
2563                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2564            ))
2565            .times(2)
2566            .respond_with(json_encoded(json!({
2567                "accessToken": "test-impersonated-token",
2568                "expireTime": expire_time
2569            }))),
2570        );
2571
2572        server.expect(
2573            Expectation::matching(all_of![
2574                request::method_path(
2575                    "POST",
2576                    "/v1/projects/-/serviceAccounts/test-principal:signBlob"
2577                ),
2578                request::headers(contains((
2579                    "authorization",
2580                    "Bearer test-impersonated-token"
2581                ))),
2582            ])
2583            .times(2)
2584            .respond_with(json_encoded(json!({
2585                "signedBlob": BASE64_STANDARD.encode("signed_blob"),
2586            }))),
2587        );
2588
2589        let endpoint = server.url("/").to_string();
2590        let endpoint = endpoint.trim_end_matches('/');
2591
2592        // Test builder from source credentials
2593        let user_credential = json!({
2594            "type": "authorized_user",
2595            "client_id": "test-client-id",
2596            "client_secret": "test-client-secret",
2597            "refresh_token": "test-refresh-token",
2598            "token_uri": server.url("/token").to_string()
2599        });
2600        let source_credential =
2601            crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2602
2603        let builder_from_source = Builder::from_source_credentials(source_credential)
2604            .with_target_principal("test-principal")
2605            .with_impersonation_endpoint(endpoint);
2606
2607        let impersonation_url = server
2608            .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2609            .to_string();
2610        // Test builder from JSON
2611        let impersonated_credential = json!({
2612            "type": "impersonated_service_account",
2613            "service_account_impersonation_url": impersonation_url,
2614            "source_credentials": user_credential,
2615        });
2616        let builder_from_json = Builder::new(impersonated_credential);
2617
2618        for builder in [builder_from_source, builder_from_json] {
2619            let iam_endpoint = server
2620                .url("/")
2621                .to_string()
2622                .trim_end_matches('/')
2623                .to_string();
2624            let signer = builder
2625                .maybe_iam_endpoint_override(Some(iam_endpoint))
2626                .without_access_boundary()
2627                .build_signer()?;
2628
2629            let client_email = signer.client_email().await?;
2630            assert_eq!(client_email, "test-principal");
2631
2632            let result = signer.sign(b"test").await?;
2633            assert_eq!(result.as_ref(), b"signed_blob");
2634        }
2635
2636        Ok(())
2637    }
2638
2639    #[tokio::test]
2640    #[parallel]
2641    async fn test_impersonated_sa_signer() -> TestResult {
2642        let service_account = json!({
2643            "type": "service_account",
2644            "client_email": "test-client-email",
2645            "private_key_id": "test-private-key-id",
2646            "private_key": Value::from(PKCS8_PK.clone()),
2647            "project_id": "test-project-id",
2648        });
2649        let impersonated_credential = json!({
2650            "type": "impersonated_service_account",
2651            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2652            "source_credentials": service_account.clone(),
2653        });
2654
2655        let signer = Builder::new(impersonated_credential).build_signer()?;
2656
2657        let client_email = signer.client_email().await?;
2658        assert_eq!(client_email, "test-principal");
2659
2660        let result = signer.sign(b"test").await?;
2661
2662        let service_account_key = serde_json::from_value::<ServiceAccountKey>(service_account)?;
2663        let inner_signer = service_account_key.signer().unwrap();
2664        let inner_result = inner_signer.sign(b"test")?;
2665        assert_eq!(result.as_ref(), inner_result);
2666
2667        Ok(())
2668    }
2669
2670    #[tokio::test]
2671    #[parallel]
2672    async fn test_impersonated_signer_with_invalid_email() -> TestResult {
2673        let impersonated_credential = json!({
2674            "type": "impersonated_service_account",
2675            "service_account_impersonation_url": "http://example.com/test-principal:generateIdToken",
2676            "source_credentials": json!({
2677                "type": "service_account",
2678                "client_email": "test-client-email",
2679                "private_key_id": "test-private-key-id",
2680                "private_key": "test-private-key",
2681                "project_id": "test-project-id",
2682            }),
2683        });
2684
2685        let error = Builder::new(impersonated_credential)
2686            .build_signer()
2687            .unwrap_err();
2688
2689        assert!(error.is_parsing());
2690        assert!(
2691            error
2692                .to_string()
2693                .contains("invalid service account impersonation URL"),
2694            "error: {}",
2695            error
2696        );
2697
2698        Ok(())
2699    }
2700
2701    #[tokio::test]
2702    #[parallel]
2703    #[cfg(google_cloud_unstable_trust_boundaries)]
2704    async fn e2e_access_boundary() -> TestResult {
2705        use crate::credentials::tests::{get_access_boundary_from_headers, get_token_from_headers};
2706        let server = Server::run();
2707        server.expect(
2708            Expectation::matching(request::method_path("POST", "/token"))
2709                .times(2..)
2710                .respond_with(json_encoded(json!({
2711                    "access_token": "test-user-account-token",
2712                    "expires_in": 3600,
2713                    "token_type": "Bearer",
2714                }))),
2715        );
2716        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2717            .format(&time::format_description::well_known::Rfc3339)
2718            .unwrap();
2719        server.expect(
2720            Expectation::matching(request::method_path(
2721                "POST",
2722                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2723            ))
2724            .times(2)
2725            .respond_with(json_encoded(json!({
2726                "accessToken": "test-impersonated-token",
2727                "expireTime": expire_time
2728            }))),
2729        );
2730
2731        server.expect(
2732            Expectation::matching(all_of![
2733                request::method_path(
2734                    "GET",
2735                    "/v1/projects/-/serviceAccounts/test-principal/allowedLocations"
2736                ),
2737                request::headers(contains((
2738                    "authorization",
2739                    "Bearer test-impersonated-token"
2740                ))),
2741            ])
2742            .times(2)
2743            .respond_with(json_encoded(json!({
2744                "locations": ["us-central1", "us-east1"],
2745                "encodedLocations": "0x1234"
2746            }))),
2747        );
2748        let impersonation_url = server
2749            .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2750            .to_string();
2751        let endpoint = server.url("/").to_string();
2752        let endpoint = endpoint.trim_end_matches('/');
2753
2754        // Test builder from source credentials
2755        let user_credential = json!({
2756            "type": "authorized_user",
2757            "client_id": "test-client-id",
2758            "client_secret": "test-client-secret",
2759            "refresh_token": "test-refresh-token",
2760            "token_uri": server.url("/token").to_string()
2761        });
2762        let source_credential =
2763            crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2764        let builder_from_source = Builder::from_source_credentials(source_credential)
2765            .with_target_principal("test-principal")
2766            .with_impersonation_endpoint(endpoint);
2767
2768        // Test builder from JSON
2769        let impersonated_credential = json!({
2770            "type": "impersonated_service_account",
2771            "service_account_impersonation_url": impersonation_url,
2772            "source_credentials": user_credential,
2773        });
2774        let builder_from_json = Builder::new(impersonated_credential);
2775
2776        for builder in [builder_from_source, builder_from_json] {
2777            let iam_endpoint = server
2778                .url("/")
2779                .to_string()
2780                .trim_end_matches('/')
2781                .to_string();
2782            let creds = builder
2783                .maybe_iam_endpoint_override(Some(iam_endpoint))
2784                .build_credentials()?;
2785
2786            // let the access boundary background thread update
2787            creds.wait_for_boundary().await;
2788
2789            let headers = creds.headers(Extensions::new()).await?;
2790            let token = get_token_from_headers(headers.clone());
2791            let access_boundary = get_access_boundary_from_headers(headers);
2792            assert!(token.is_some(), "should have some token: {token:?}");
2793            assert_eq!(
2794                access_boundary.as_deref(),
2795                Some("0x1234"),
2796                "should be 0x1234 but found: {access_boundary:?}"
2797            );
2798        }
2799
2800        Ok(())
2801    }
2802
2803    #[tokio::test]
2804    #[parallel]
2805    async fn test_impersonated_access_token_custom_universe_domain() -> TestResult {
2806        let server = Server::run();
2807        let universe_domain = "my-custom-universe.com".to_string();
2808        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2809            .format(&time::format_description::well_known::Rfc3339)
2810            .unwrap();
2811
2812        server.expect(
2813            Expectation::matching(all_of![
2814                request::method_path(
2815                    "POST",
2816                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2817                ),
2818                request::headers(contains((
2819                    "authorization",
2820                    "Bearer test-user-account-token"
2821                ))),
2822            ])
2823            .respond_with(json_encoded(json!({
2824                "accessToken": "test-impersonated-token",
2825                "expireTime": expire_time
2826            }))),
2827        );
2828
2829        let universe_domain_clone = universe_domain.clone();
2830        let mut mock = MockCredentials::new();
2831        mock.expect_universe_domain()
2832            .returning(move || Some(universe_domain_clone.clone()));
2833        mock.expect_headers().returning(move |_| {
2834            let mut headers = HeaderMap::new();
2835            headers.insert(
2836                "authorization",
2837                "Bearer test-user-account-token".parse().unwrap(),
2838            );
2839            Ok(CacheableResource::New {
2840                entity_tag: Default::default(),
2841                data: headers,
2842            })
2843        });
2844        let source_credentials = Credentials::from(mock);
2845
2846        let builder = Builder::from_source_credentials(source_credentials.clone())
2847            .with_target_principal("test-principal");
2848
2849        // resolve url with universe domain
2850        let url = builder
2851            .service_account_impersonation_url
2852            .as_ref()
2853            .expect("url should be set from the with_target_principal call")
2854            .access_token_url(&source_credentials)
2855            .await;
2856
2857        assert_eq!(
2858            url,
2859            format!(
2860                "https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2861            )
2862        );
2863
2864        let endpoint = server.url("/").to_string();
2865        let endpoint = endpoint.trim_end_matches('/');
2866
2867        let creds = builder
2868            .with_impersonation_endpoint(endpoint)
2869            .build_access_token_credentials()?;
2870
2871        let token = creds.access_token().await?;
2872        assert_eq!(token.token, "test-impersonated-token");
2873
2874        Ok(())
2875    }
2876
2877    #[test_case(ImpersonationUrlKind::TargetPrincipal("test@example.com".to_string()), "test@example.com" ; "target principal")]
2878    #[test_case(ImpersonationUrlKind::Exact("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@example.com:generateAccessToken".to_string()), "test@example.com" ; "exact url")]
2879    fn impersonation_url_client_email(kind: ImpersonationUrlKind, expected: &str) {
2880        let impersonation_url = ImpersonationUrl {
2881            endpoint: None,
2882            kind,
2883        };
2884        assert_eq!(impersonation_url.client_email().unwrap(), expected);
2885    }
2886
2887    #[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")]
2888    #[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")]
2889    #[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")]
2890    #[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")]
2891    #[tokio::test]
2892    async fn impersonation_url_access_token_url(
2893        impersonation_url: ImpersonationUrl,
2894        universe: &str,
2895        expected_url: &str,
2896    ) -> TestResult {
2897        let source_creds = source_creds_with_universe_domain(universe);
2898        let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2899            .build()
2900            .expect("Failed to build service account credentials");
2901
2902        let url = impersonation_url
2903            .access_token_url(&source_credentials)
2904            .await;
2905        assert_eq!(url, expected_url);
2906        Ok(())
2907    }
2908
2909    #[cfg(feature = "idtoken")]
2910    #[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")]
2911    #[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")]
2912    #[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")]
2913    #[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")]
2914    #[tokio::test]
2915    async fn impersonation_url_id_token_url(
2916        impersonation_url: ImpersonationUrl,
2917        universe: &str,
2918        expected_url: &str,
2919    ) -> TestResult {
2920        let source_creds = source_creds_with_universe_domain(universe);
2921        let source_credentials = crate::credentials::service_account::Builder::new(source_creds)
2922            .build()
2923            .expect("Failed to build service account credentials");
2924
2925        let url = impersonation_url.id_token_url(&source_credentials).await;
2926        assert_eq!(url, expected_url);
2927        Ok(())
2928    }
2929}