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