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