1use super::dynamic::CredentialsProvider;
111use super::external_account_sources::executable_sourced::ExecutableSourcedCredentials;
112use super::external_account_sources::file_sourced::FileSourcedCredentials;
113use super::external_account_sources::url_sourced::UrlSourcedCredentials;
114use super::impersonated;
115use super::internal::sts_exchange::{ClientAuthentication, ExchangeTokenRequest, STSHandler};
116use super::{CacheableResource, Credentials};
117use crate::build_errors::Error as BuilderError;
118use crate::constants::{DEFAULT_SCOPE, STS_TOKEN_URL};
119use crate::credentials::dynamic::AccessTokenCredentialsProvider;
120use crate::credentials::external_account_sources::programmatic_sourced::ProgrammaticSourcedCredentials;
121use crate::credentials::subject_token::dynamic;
122use crate::credentials::{AccessToken, AccessTokenCredentials};
123use crate::errors::non_retryable;
124use crate::headers_util::build_cacheable_headers;
125use crate::retry::Builder as RetryTokenProviderBuilder;
126use crate::token::{CachedTokenProvider, Token, TokenProvider};
127use crate::token_cache::TokenCache;
128use crate::{BuildResult, Result};
129use gax::backoff_policy::BackoffPolicyArg;
130use gax::retry_policy::RetryPolicyArg;
131use gax::retry_throttler::RetryThrottlerArg;
132use http::{Extensions, HeaderMap};
133use serde::{Deserialize, Serialize};
134use serde_json::Value;
135use std::collections::HashMap;
136use std::sync::Arc;
137use tokio::time::{Duration, Instant};
138
139const IAM_SCOPE: &str = "https://www.googleapis.com/auth/iam";
140
141#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
142pub(crate) struct CredentialSourceFormat {
143 #[serde(rename = "type")]
144 pub format_type: String,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub subject_token_field_name: Option<String>,
147}
148
149#[derive(Serialize, Deserialize, Debug, Clone, Default)]
150pub(crate) struct ExecutableConfig {
151 pub command: String,
152 pub timeout_millis: Option<u32>,
153 pub output_file: Option<String>,
154}
155
156#[derive(Serialize, Deserialize, Debug, Clone)]
157#[serde(untagged)]
158enum CredentialSourceFile {
159 Executable {
161 executable: ExecutableConfig,
162 },
163 Url {
164 url: String,
165 headers: Option<HashMap<String, String>>,
166 format: Option<CredentialSourceFormat>,
167 },
168 File {
169 file: String,
170 format: Option<CredentialSourceFormat>,
171 },
172 Aws,
173}
174
175#[derive(Serialize, Deserialize, Debug, Clone)]
179struct ExternalAccountFile {
180 audience: String,
181 subject_token_type: String,
182 service_account_impersonation_url: Option<String>,
183 token_url: String,
184 client_id: Option<String>,
185 client_secret: Option<String>,
186 scopes: Option<Vec<String>>,
187 credential_source: CredentialSourceFile,
188}
189
190impl From<ExternalAccountFile> for ExternalAccountConfig {
191 fn from(config: ExternalAccountFile) -> Self {
192 let mut scope = config.scopes.unwrap_or_default();
193 if scope.is_empty() {
194 scope.push(DEFAULT_SCOPE.to_string());
195 }
196 Self {
197 audience: config.audience,
198 client_id: config.client_id,
199 client_secret: config.client_secret,
200 subject_token_type: config.subject_token_type,
201 token_url: config.token_url,
202 service_account_impersonation_url: config.service_account_impersonation_url,
203 credential_source: config.credential_source.into(),
204 scopes: scope,
205 }
206 }
207}
208
209impl From<CredentialSourceFile> for CredentialSource {
210 fn from(source: CredentialSourceFile) -> Self {
211 match source {
212 CredentialSourceFile::Url {
213 url,
214 headers,
215 format,
216 } => Self::Url(UrlSourcedCredentials::new(url, headers, format)),
217 CredentialSourceFile::Executable { executable } => {
218 Self::Executable(ExecutableSourcedCredentials::new(executable))
219 }
220 CredentialSourceFile::File { file, format } => {
221 Self::File(FileSourcedCredentials::new(file, format))
222 }
223 CredentialSourceFile::Aws => {
224 unimplemented!("AWS sourced credential not supported yet")
225 }
226 }
227 }
228}
229
230#[derive(Debug, Clone)]
231struct ExternalAccountConfig {
232 audience: String,
233 subject_token_type: String,
234 token_url: String,
235 service_account_impersonation_url: Option<String>,
236 client_id: Option<String>,
237 client_secret: Option<String>,
238 scopes: Vec<String>,
239 credential_source: CredentialSource,
240}
241
242#[derive(Debug, Default)]
243struct ExternalAccountConfigBuilder {
244 audience: Option<String>,
245 subject_token_type: Option<String>,
246 token_url: Option<String>,
247 service_account_impersonation_url: Option<String>,
248 client_id: Option<String>,
249 client_secret: Option<String>,
250 scopes: Option<Vec<String>>,
251 credential_source: Option<CredentialSource>,
252}
253
254impl ExternalAccountConfigBuilder {
255 fn with_audience<S: Into<String>>(mut self, audience: S) -> Self {
256 self.audience = Some(audience.into());
257 self
258 }
259
260 fn with_subject_token_type<S: Into<String>>(mut self, subject_token_type: S) -> Self {
261 self.subject_token_type = Some(subject_token_type.into());
262 self
263 }
264
265 fn with_token_url<S: Into<String>>(mut self, token_url: S) -> Self {
266 self.token_url = Some(token_url.into());
267 self
268 }
269
270 fn with_service_account_impersonation_url<S: Into<String>>(mut self, url: S) -> Self {
271 self.service_account_impersonation_url = Some(url.into());
272 self
273 }
274
275 fn with_client_id<S: Into<String>>(mut self, client_id: S) -> Self {
276 self.client_id = Some(client_id.into());
277 self
278 }
279
280 fn with_client_secret<S: Into<String>>(mut self, client_secret: S) -> Self {
281 self.client_secret = Some(client_secret.into());
282 self
283 }
284
285 fn with_scopes(mut self, scopes: Vec<String>) -> Self {
286 self.scopes = Some(scopes);
287 self
288 }
289
290 fn with_credential_source(mut self, source: CredentialSource) -> Self {
291 self.credential_source = Some(source);
292 self
293 }
294
295 fn build(self) -> BuildResult<ExternalAccountConfig> {
296 Ok(ExternalAccountConfig {
297 audience: self
298 .audience
299 .ok_or(BuilderError::missing_field("audience"))?,
300 subject_token_type: self
301 .subject_token_type
302 .ok_or(BuilderError::missing_field("subject_token_type"))?,
303 token_url: self
304 .token_url
305 .ok_or(BuilderError::missing_field("token_url"))?,
306 scopes: self.scopes.ok_or(BuilderError::missing_field("scopes"))?,
307 credential_source: self
308 .credential_source
309 .ok_or(BuilderError::missing_field("credential_source"))?,
310 service_account_impersonation_url: self.service_account_impersonation_url,
311 client_id: self.client_id,
312 client_secret: self.client_secret,
313 })
314 }
315}
316
317#[derive(Debug, Clone)]
318#[allow(dead_code)]
319enum CredentialSource {
320 Url(UrlSourcedCredentials),
321 Executable(ExecutableSourcedCredentials),
322 File(FileSourcedCredentials),
323 Aws,
324 Programmatic(ProgrammaticSourcedCredentials),
325}
326
327impl ExternalAccountConfig {
328 fn make_credentials(
329 self,
330 quota_project_id: Option<String>,
331 retry_builder: RetryTokenProviderBuilder,
332 ) -> AccessTokenCredentials {
333 let config = self.clone();
334 match self.credential_source {
335 CredentialSource::Url(source) => {
336 Self::make_credentials_from_source(source, config, quota_project_id, retry_builder)
337 }
338 CredentialSource::Executable(source) => {
339 Self::make_credentials_from_source(source, config, quota_project_id, retry_builder)
340 }
341 CredentialSource::Programmatic(source) => {
342 Self::make_credentials_from_source(source, config, quota_project_id, retry_builder)
343 }
344 CredentialSource::File(source) => {
345 Self::make_credentials_from_source(source, config, quota_project_id, retry_builder)
346 }
347 CredentialSource::Aws => {
348 unimplemented!("AWS sourced credential not supported yet")
349 }
350 }
351 }
352
353 fn make_credentials_from_source<T>(
354 subject_token_provider: T,
355 config: ExternalAccountConfig,
356 quota_project_id: Option<String>,
357 retry_builder: RetryTokenProviderBuilder,
358 ) -> AccessTokenCredentials
359 where
360 T: dynamic::SubjectTokenProvider + 'static,
361 {
362 let token_provider = ExternalAccountTokenProvider {
363 subject_token_provider,
364 config,
365 };
366 let token_provider_with_retry = retry_builder.build(token_provider);
367 let cache = TokenCache::new(token_provider_with_retry);
368 AccessTokenCredentials {
369 inner: Arc::new(ExternalAccountCredentials {
370 token_provider: cache,
371 quota_project_id,
372 }),
373 }
374 }
375}
376
377#[derive(Debug)]
378struct ExternalAccountTokenProvider<T>
379where
380 T: dynamic::SubjectTokenProvider,
381{
382 subject_token_provider: T,
383 config: ExternalAccountConfig,
384}
385
386#[async_trait::async_trait]
387impl<T> TokenProvider for ExternalAccountTokenProvider<T>
388where
389 T: dynamic::SubjectTokenProvider,
390{
391 async fn token(&self) -> Result<Token> {
392 let subject_token = self.subject_token_provider.subject_token().await?;
393
394 let audience = self.config.audience.clone();
395 let subject_token_type = self.config.subject_token_type.clone();
396 let user_scopes = self.config.scopes.clone();
397 let url = self.config.token_url.clone();
398
399 let sts_scope = if self.config.service_account_impersonation_url.is_some() {
405 vec![IAM_SCOPE.to_string()]
406 } else {
407 user_scopes.clone()
408 };
409
410 let req = ExchangeTokenRequest {
411 url,
412 audience: Some(audience),
413 subject_token: subject_token.token,
414 subject_token_type,
415 scope: sts_scope,
416 authentication: ClientAuthentication {
417 client_id: self.config.client_id.clone(),
418 client_secret: self.config.client_secret.clone(),
419 },
420 ..ExchangeTokenRequest::default()
421 };
422
423 let token_res = STSHandler::exchange_token(req).await?;
424
425 if let Some(impersonation_url) = &self.config.service_account_impersonation_url {
426 let mut headers = HeaderMap::new();
427 headers.insert(
428 http::header::AUTHORIZATION,
429 http::HeaderValue::from_str(&format!("Bearer {}", token_res.access_token))
430 .map_err(non_retryable)?,
431 );
432
433 return impersonated::generate_access_token(
434 headers,
435 None,
436 user_scopes,
437 impersonated::DEFAULT_LIFETIME,
438 impersonation_url,
439 )
440 .await;
441 }
442
443 let token = Token {
444 token: token_res.access_token,
445 token_type: token_res.token_type,
446 expires_at: Some(Instant::now() + Duration::from_secs(token_res.expires_in)),
447 metadata: None,
448 };
449 Ok(token)
450 }
451}
452
453#[derive(Debug)]
454pub(crate) struct ExternalAccountCredentials<T>
455where
456 T: CachedTokenProvider,
457{
458 token_provider: T,
459 quota_project_id: Option<String>,
460}
461
462pub struct Builder {
506 external_account_config: Value,
507 quota_project_id: Option<String>,
508 scopes: Option<Vec<String>>,
509 retry_builder: RetryTokenProviderBuilder,
510}
511
512impl Builder {
513 pub fn new(external_account_config: Value) -> Self {
517 Self {
518 external_account_config,
519 quota_project_id: None,
520 scopes: None,
521 retry_builder: RetryTokenProviderBuilder::default(),
522 }
523 }
524
525 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
534 self.quota_project_id = Some(quota_project_id.into());
535 self
536 }
537
538 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
542 where
543 I: IntoIterator<Item = S>,
544 S: Into<String>,
545 {
546 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
547 self
548 }
549
550 pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
572 self.retry_builder = self.retry_builder.with_retry_policy(v.into());
573 self
574 }
575
576 pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
599 self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
600 self
601 }
602
603 pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
631 self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
632 self
633 }
634
635 pub fn build(self) -> BuildResult<Credentials> {
649 Ok(self.build_access_token_credentials()?.into())
650 }
651
652 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
666 let mut file: ExternalAccountFile =
667 serde_json::from_value(self.external_account_config).map_err(BuilderError::parsing)?;
668
669 if let Some(scopes) = self.scopes {
670 file.scopes = Some(scopes);
671 }
672
673 let config: ExternalAccountConfig = file.into();
674
675 Ok(config.make_credentials(self.quota_project_id, self.retry_builder))
676 }
677}
678
679pub struct ProgrammaticBuilder {
727 quota_project_id: Option<String>,
728 config: ExternalAccountConfigBuilder,
729 retry_builder: RetryTokenProviderBuilder,
730}
731
732impl ProgrammaticBuilder {
733 pub fn new(subject_token_provider: Arc<dyn dynamic::SubjectTokenProvider>) -> Self {
766 let config = ExternalAccountConfigBuilder::default().with_credential_source(
767 CredentialSource::Programmatic(ProgrammaticSourcedCredentials::new(
768 subject_token_provider,
769 )),
770 );
771 Self {
772 quota_project_id: None,
773 config,
774 retry_builder: RetryTokenProviderBuilder::default(),
775 }
776 }
777
778 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
817 self.quota_project_id = Some(quota_project_id.into());
818 self
819 }
820
821 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
856 where
857 I: IntoIterator<Item = S>,
858 S: Into<String>,
859 {
860 self.config = self.config.with_scopes(
861 scopes
862 .into_iter()
863 .map(|s| s.into())
864 .collect::<Vec<String>>(),
865 );
866 self
867 }
868
869 pub fn with_audience<S: Into<String>>(mut self, audience: S) -> Self {
904 self.config = self.config.with_audience(audience);
905 self
906 }
907
908 pub fn with_subject_token_type<S: Into<String>>(mut self, subject_token_type: S) -> Self {
942 self.config = self.config.with_subject_token_type(subject_token_type);
943 self
944 }
945
946 pub fn with_token_url<S: Into<String>>(mut self, token_url: S) -> Self {
979 self.config = self.config.with_token_url(token_url);
980 self
981 }
982
983 pub fn with_client_id<S: Into<String>>(mut self, client_id: S) -> Self {
1015 self.config = self.config.with_client_id(client_id.into());
1016 self
1017 }
1018
1019 pub fn with_client_secret<S: Into<String>>(mut self, client_secret: S) -> Self {
1051 self.config = self.config.with_client_secret(client_secret.into());
1052 self
1053 }
1054
1055 pub fn with_target_principal<S: Into<String>>(mut self, target_principal: S) -> Self {
1089 let url = format!(
1090 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken",
1091 target_principal.into()
1092 );
1093 self.config = self.config.with_service_account_impersonation_url(url);
1094 self
1095 }
1096
1097 pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
1137 self.retry_builder = self.retry_builder.with_retry_policy(v.into());
1138 self
1139 }
1140
1141 pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
1181 self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
1182 self
1183 }
1184
1185 pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
1231 self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
1232 self
1233 }
1234
1235 pub fn build(self) -> BuildResult<Credentials> {
1242 let (config, quota_project_id, retry_builder) = self.build_components()?;
1243 Ok(config
1244 .make_credentials(quota_project_id, retry_builder)
1245 .into())
1246 }
1247
1248 fn build_components(
1250 self,
1251 ) -> BuildResult<(
1252 ExternalAccountConfig,
1253 Option<String>,
1254 RetryTokenProviderBuilder,
1255 )> {
1256 let Self {
1257 quota_project_id,
1258 config,
1259 retry_builder,
1260 } = self;
1261
1262 let mut config_builder = config;
1263 if config_builder.scopes.is_none() {
1264 config_builder = config_builder.with_scopes(vec![DEFAULT_SCOPE.to_string()]);
1265 }
1266 if config_builder.token_url.is_none() {
1267 config_builder = config_builder.with_token_url(STS_TOKEN_URL.to_string());
1268 }
1269 let final_config = config_builder.build()?;
1270
1271 Ok((final_config, quota_project_id, retry_builder))
1272 }
1273}
1274
1275#[async_trait::async_trait]
1276impl<T> CredentialsProvider for ExternalAccountCredentials<T>
1277where
1278 T: CachedTokenProvider,
1279{
1280 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
1281 let token = self.token_provider.token(extensions).await?;
1282 build_cacheable_headers(&token, &self.quota_project_id)
1283 }
1284}
1285
1286#[async_trait::async_trait]
1287impl<T> AccessTokenCredentialsProvider for ExternalAccountCredentials<T>
1288where
1289 T: CachedTokenProvider,
1290{
1291 async fn access_token(&self) -> Result<AccessToken> {
1292 let token = self.token_provider.token(Extensions::new()).await?;
1293 token.into()
1294 }
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299 use super::*;
1300 use crate::constants::{
1301 ACCESS_TOKEN_TYPE, DEFAULT_SCOPE, JWT_TOKEN_TYPE, TOKEN_EXCHANGE_GRANT_TYPE,
1302 };
1303 use crate::credentials::subject_token::{
1304 Builder as SubjectTokenBuilder, SubjectToken, SubjectTokenProvider,
1305 };
1306 use crate::credentials::tests::{
1307 find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
1308 get_mock_retry_throttler,
1309 };
1310 use crate::errors::{CredentialsError, SubjectTokenProviderError};
1311 use httptest::{
1312 Expectation, Server, cycle,
1313 matchers::{all_of, contains, request, url_decoded},
1314 responders::{json_encoded, status_code},
1315 };
1316 use serde_json::*;
1317 use std::collections::HashMap;
1318 use std::error::Error;
1319 use std::fmt;
1320 use test_case::test_case;
1321 use time::OffsetDateTime;
1322
1323 #[derive(Debug)]
1324 struct TestProviderError;
1325 impl fmt::Display for TestProviderError {
1326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1327 write!(f, "TestProviderError")
1328 }
1329 }
1330 impl Error for TestProviderError {}
1331 impl SubjectTokenProviderError for TestProviderError {
1332 fn is_transient(&self) -> bool {
1333 false
1334 }
1335 }
1336
1337 #[derive(Debug)]
1338 struct TestSubjectTokenProvider;
1339 impl SubjectTokenProvider for TestSubjectTokenProvider {
1340 type Error = TestProviderError;
1341 async fn subject_token(&self) -> std::result::Result<SubjectToken, Self::Error> {
1342 Ok(SubjectTokenBuilder::new("test-subject-token".to_string()).build())
1343 }
1344 }
1345
1346 #[tokio::test]
1347 async fn create_external_account_builder() {
1348 let contents = json!({
1349 "type": "external_account",
1350 "audience": "audience",
1351 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1352 "token_url": "https://sts.googleapis.com/v1beta/token",
1353 "credential_source": {
1354 "url": "https://example.com/token",
1355 "format": {
1356 "type": "json",
1357 "subject_token_field_name": "access_token"
1358 }
1359 }
1360 });
1361
1362 let creds = Builder::new(contents)
1363 .with_quota_project_id("test_project")
1364 .with_scopes(["a", "b"])
1365 .build()
1366 .unwrap();
1367
1368 let fmt = format!("{creds:?}");
1370 assert!(fmt.contains("ExternalAccountCredentials"));
1371 }
1372
1373 #[tokio::test]
1374 async fn create_external_account_detect_url_sourced() {
1375 let contents = json!({
1376 "type": "external_account",
1377 "audience": "audience",
1378 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1379 "token_url": "https://sts.googleapis.com/v1beta/token",
1380 "credential_source": {
1381 "url": "https://example.com/token",
1382 "headers": {
1383 "Metadata": "True"
1384 },
1385 "format": {
1386 "type": "json",
1387 "subject_token_field_name": "access_token"
1388 }
1389 }
1390 });
1391
1392 let file: ExternalAccountFile =
1393 serde_json::from_value(contents).expect("failed to parse external account config");
1394 let config: ExternalAccountConfig = file.into();
1395 let source = config.credential_source;
1396
1397 match source {
1398 CredentialSource::Url(source) => {
1399 assert_eq!(source.url, "https://example.com/token");
1400 assert_eq!(
1401 source.headers,
1402 HashMap::from([("Metadata".to_string(), "True".to_string()),]),
1403 );
1404 assert_eq!(source.format, "json");
1405 assert_eq!(source.subject_token_field_name, "access_token");
1406 }
1407 _ => {
1408 unreachable!("expected Url Sourced credential")
1409 }
1410 }
1411 }
1412
1413 #[tokio::test]
1414 async fn create_external_account_detect_executable_sourced() {
1415 let contents = json!({
1416 "type": "external_account",
1417 "audience": "audience",
1418 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1419 "token_url": "https://sts.googleapis.com/v1beta/token",
1420 "credential_source": {
1421 "executable": {
1422 "command": "cat /some/file",
1423 "output_file": "/some/file",
1424 "timeout_millis": 5000
1425 }
1426 }
1427 });
1428
1429 let file: ExternalAccountFile =
1430 serde_json::from_value(contents).expect("failed to parse external account config");
1431 let config: ExternalAccountConfig = file.into();
1432 let source = config.credential_source;
1433
1434 match source {
1435 CredentialSource::Executable(source) => {
1436 assert_eq!(source.command, "cat");
1437 assert_eq!(source.args, vec!["/some/file"]);
1438 assert_eq!(source.output_file.as_deref(), Some("/some/file"));
1439 assert_eq!(source.timeout, Duration::from_secs(5));
1440 }
1441 _ => {
1442 unreachable!("expected Executable Sourced credential")
1443 }
1444 }
1445 }
1446
1447 #[tokio::test]
1448 async fn create_external_account_detect_file_sourced() {
1449 let contents = json!({
1450 "type": "external_account",
1451 "audience": "audience",
1452 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1453 "token_url": "https://sts.googleapis.com/v1beta/token",
1454 "credential_source": {
1455 "file": "/foo/bar",
1456 "format": {
1457 "type": "json",
1458 "subject_token_field_name": "token"
1459 }
1460 }
1461 });
1462
1463 let file: ExternalAccountFile =
1464 serde_json::from_value(contents).expect("failed to parse external account config");
1465 let config: ExternalAccountConfig = file.into();
1466 let source = config.credential_source;
1467
1468 match source {
1469 CredentialSource::File(source) => {
1470 assert_eq!(source.file, "/foo/bar");
1471 assert_eq!(source.format, "json");
1472 assert_eq!(source.subject_token_field_name, "token");
1473 }
1474 _ => {
1475 unreachable!("expected File Sourced credential")
1476 }
1477 }
1478 }
1479
1480 #[tokio::test]
1481 async fn test_external_account_with_impersonation_success() {
1482 let subject_token_server = Server::run();
1483 let sts_server = Server::run();
1484 let impersonation_server = Server::run();
1485
1486 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1487 let contents = json!({
1488 "type": "external_account",
1489 "audience": "audience",
1490 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1491 "token_url": sts_server.url("/token").to_string(),
1492 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1493 "credential_source": {
1494 "url": subject_token_server.url("/subject_token").to_string(),
1495 "format": {
1496 "type": "json",
1497 "subject_token_field_name": "access_token"
1498 }
1499 }
1500 });
1501
1502 subject_token_server.expect(
1503 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1504 json_encoded(json!({
1505 "access_token": "subject_token",
1506 })),
1507 ),
1508 );
1509
1510 sts_server.expect(
1511 Expectation::matching(all_of![
1512 request::method_path("POST", "/token"),
1513 request::body(url_decoded(contains((
1514 "grant_type",
1515 TOKEN_EXCHANGE_GRANT_TYPE
1516 )))),
1517 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1518 request::body(url_decoded(contains((
1519 "requested_token_type",
1520 ACCESS_TOKEN_TYPE
1521 )))),
1522 request::body(url_decoded(contains((
1523 "subject_token_type",
1524 JWT_TOKEN_TYPE
1525 )))),
1526 request::body(url_decoded(contains(("audience", "audience")))),
1527 request::body(url_decoded(contains(("scope", IAM_SCOPE)))),
1528 ])
1529 .respond_with(json_encoded(json!({
1530 "access_token": "sts-token",
1531 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1532 "token_type": "Bearer",
1533 "expires_in": 3600,
1534 }))),
1535 );
1536
1537 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1538 .format(&time::format_description::well_known::Rfc3339)
1539 .unwrap();
1540 impersonation_server.expect(
1541 Expectation::matching(all_of![
1542 request::method_path("POST", impersonation_path),
1543 request::headers(contains(("authorization", "Bearer sts-token"))),
1544 ])
1545 .respond_with(json_encoded(json!({
1546 "accessToken": "final-impersonated-token",
1547 "expireTime": expire_time
1548 }))),
1549 );
1550
1551 let creds = Builder::new(contents).build().unwrap();
1552 let headers = creds.headers(Extensions::new()).await.unwrap();
1553 match headers {
1554 CacheableResource::New { data, .. } => {
1555 let token = data.get("authorization").unwrap().to_str().unwrap();
1556 assert_eq!(token, "Bearer final-impersonated-token");
1557 }
1558 CacheableResource::NotModified => panic!("Expected new headers"),
1559 }
1560 }
1561
1562 #[tokio::test]
1563 async fn test_external_account_without_impersonation_success() {
1564 let subject_token_server = Server::run();
1565 let sts_server = Server::run();
1566
1567 let contents = json!({
1568 "type": "external_account",
1569 "audience": "audience",
1570 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1571 "token_url": sts_server.url("/token").to_string(),
1572 "credential_source": {
1573 "url": subject_token_server.url("/subject_token").to_string(),
1574 "format": {
1575 "type": "json",
1576 "subject_token_field_name": "access_token"
1577 }
1578 }
1579 });
1580
1581 subject_token_server.expect(
1582 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1583 json_encoded(json!({
1584 "access_token": "subject_token",
1585 })),
1586 ),
1587 );
1588
1589 sts_server.expect(
1590 Expectation::matching(all_of![
1591 request::method_path("POST", "/token"),
1592 request::body(url_decoded(contains((
1593 "grant_type",
1594 TOKEN_EXCHANGE_GRANT_TYPE
1595 )))),
1596 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1597 request::body(url_decoded(contains((
1598 "requested_token_type",
1599 ACCESS_TOKEN_TYPE
1600 )))),
1601 request::body(url_decoded(contains((
1602 "subject_token_type",
1603 JWT_TOKEN_TYPE
1604 )))),
1605 request::body(url_decoded(contains(("audience", "audience")))),
1606 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1607 ])
1608 .respond_with(json_encoded(json!({
1609 "access_token": "sts-only-token",
1610 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1611 "token_type": "Bearer",
1612 "expires_in": 3600,
1613 }))),
1614 );
1615
1616 let creds = Builder::new(contents).build().unwrap();
1617 let headers = creds.headers(Extensions::new()).await.unwrap();
1618 match headers {
1619 CacheableResource::New { data, .. } => {
1620 let token = data.get("authorization").unwrap().to_str().unwrap();
1621 assert_eq!(token, "Bearer sts-only-token");
1622 }
1623 CacheableResource::NotModified => panic!("Expected new headers"),
1624 }
1625 }
1626
1627 #[tokio::test]
1628 async fn test_external_account_access_token_credentials_success() {
1629 let server = Server::run();
1630
1631 let contents = json!({
1632 "type": "external_account",
1633 "audience": "audience",
1634 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1635 "token_url": server.url("/token").to_string(),
1636 "credential_source": {
1637 "url": server.url("/subject_token").to_string(),
1638 "format": {
1639 "type": "json",
1640 "subject_token_field_name": "access_token"
1641 }
1642 }
1643 });
1644
1645 server.expect(
1646 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1647 json_encoded(json!({
1648 "access_token": "subject_token",
1649 })),
1650 ),
1651 );
1652
1653 server.expect(
1654 Expectation::matching(all_of![
1655 request::method_path("POST", "/token"),
1656 request::body(url_decoded(contains((
1657 "grant_type",
1658 TOKEN_EXCHANGE_GRANT_TYPE
1659 )))),
1660 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1661 request::body(url_decoded(contains((
1662 "requested_token_type",
1663 ACCESS_TOKEN_TYPE
1664 )))),
1665 request::body(url_decoded(contains((
1666 "subject_token_type",
1667 JWT_TOKEN_TYPE
1668 )))),
1669 request::body(url_decoded(contains(("audience", "audience")))),
1670 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1671 ])
1672 .respond_with(json_encoded(json!({
1673 "access_token": "sts-only-token",
1674 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1675 "token_type": "Bearer",
1676 "expires_in": 3600,
1677 }))),
1678 );
1679
1680 let creds = Builder::new(contents)
1681 .build_access_token_credentials()
1682 .unwrap();
1683 let access_token = creds.access_token().await.unwrap();
1684 assert_eq!(access_token.token, "sts-only-token");
1685 }
1686
1687 #[tokio::test]
1688 async fn test_impersonation_flow_sts_call_fails() {
1689 let subject_token_server = Server::run();
1690 let sts_server = Server::run();
1691 let impersonation_server = Server::run();
1692
1693 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1694 let contents = json!({
1695 "type": "external_account",
1696 "audience": "audience",
1697 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1698 "token_url": sts_server.url("/token").to_string(),
1699 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1700 "credential_source": {
1701 "url": subject_token_server.url("/subject_token").to_string(),
1702 "format": {
1703 "type": "json",
1704 "subject_token_field_name": "access_token"
1705 }
1706 }
1707 });
1708
1709 subject_token_server.expect(
1710 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1711 json_encoded(json!({
1712 "access_token": "subject_token",
1713 })),
1714 ),
1715 );
1716
1717 sts_server.expect(
1718 Expectation::matching(request::method_path("POST", "/token"))
1719 .respond_with(status_code(500)),
1720 );
1721
1722 let creds = Builder::new(contents).build().unwrap();
1723 let err = creds.headers(Extensions::new()).await.unwrap_err();
1724 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1725 assert!(
1726 original_err
1727 .to_string()
1728 .contains("failed to exchange token")
1729 );
1730 assert!(original_err.is_transient());
1731 }
1732
1733 #[tokio::test]
1734 async fn test_impersonation_flow_iam_call_fails() {
1735 let subject_token_server = Server::run();
1736 let sts_server = Server::run();
1737 let impersonation_server = Server::run();
1738
1739 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1740 let contents = json!({
1741 "type": "external_account",
1742 "audience": "audience",
1743 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1744 "token_url": sts_server.url("/token").to_string(),
1745 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1746 "credential_source": {
1747 "url": subject_token_server.url("/subject_token").to_string(),
1748 "format": {
1749 "type": "json",
1750 "subject_token_field_name": "access_token"
1751 }
1752 }
1753 });
1754
1755 subject_token_server.expect(
1756 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1757 json_encoded(json!({
1758 "access_token": "subject_token",
1759 })),
1760 ),
1761 );
1762
1763 sts_server.expect(
1764 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1765 json_encoded(json!({
1766 "access_token": "sts-token",
1767 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1768 "token_type": "Bearer",
1769 "expires_in": 3600,
1770 })),
1771 ),
1772 );
1773
1774 impersonation_server.expect(
1775 Expectation::matching(request::method_path("POST", impersonation_path))
1776 .respond_with(status_code(403)),
1777 );
1778
1779 let creds = Builder::new(contents).build().unwrap();
1780 let err = creds.headers(Extensions::new()).await.unwrap_err();
1781 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1782 assert!(original_err.to_string().contains("failed to fetch token"));
1783 assert!(!original_err.is_transient());
1784 }
1785
1786 #[test_case(Some(vec!["scope1", "scope2"]), Some("http://custom.com/token") ; "with custom scopes and token_url")]
1787 #[test_case(None, Some("http://custom.com/token") ; "with default scopes and custom token_url")]
1788 #[test_case(Some(vec!["scope1", "scope2"]), None ; "with custom scopes and default token_url")]
1789 #[test_case(None, None ; "with default scopes and default token_url")]
1790 #[tokio::test]
1791 async fn create_programmatic_builder(scopes: Option<Vec<&str>>, token_url: Option<&str>) {
1792 let provider = Arc::new(TestSubjectTokenProvider);
1793 let mut builder = ProgrammaticBuilder::new(provider)
1794 .with_audience("test-audience")
1795 .with_subject_token_type("test-token-type")
1796 .with_client_id("test-client-id")
1797 .with_client_secret("test-client-secret")
1798 .with_target_principal("test-principal");
1799
1800 let expected_scopes = if let Some(scopes) = scopes.clone() {
1801 scopes.iter().map(|s| s.to_string()).collect()
1802 } else {
1803 vec![DEFAULT_SCOPE.to_string()]
1804 };
1805
1806 let expected_token_url = token_url.unwrap_or(STS_TOKEN_URL).to_string();
1807
1808 if let Some(scopes) = scopes {
1809 builder = builder.with_scopes(scopes);
1810 }
1811 if let Some(token_url) = token_url {
1812 builder = builder.with_token_url(token_url);
1813 }
1814
1815 let (config, _, _) = builder.build_components().unwrap();
1816
1817 assert_eq!(config.audience, "test-audience");
1818 assert_eq!(config.subject_token_type, "test-token-type");
1819 assert_eq!(config.client_id, Some("test-client-id".to_string()));
1820 assert_eq!(config.client_secret, Some("test-client-secret".to_string()));
1821 assert_eq!(config.scopes, expected_scopes);
1822 assert_eq!(config.token_url, expected_token_url);
1823 assert_eq!(
1824 config.service_account_impersonation_url,
1825 Some("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string())
1826 );
1827 }
1828
1829 #[tokio::test]
1830 async fn create_programmatic_builder_with_quota_project_id() {
1831 let provider = Arc::new(TestSubjectTokenProvider);
1832 let builder = ProgrammaticBuilder::new(provider)
1833 .with_audience("test-audience")
1834 .with_subject_token_type("test-token-type")
1835 .with_token_url(STS_TOKEN_URL)
1836 .with_quota_project_id("test-quota-project");
1837
1838 let creds = builder.build().unwrap();
1839
1840 let fmt = format!("{creds:?}");
1841 assert!(
1842 fmt.contains("ExternalAccountCredentials"),
1843 "Expected 'ExternalAccountCredentials', got: {fmt}"
1844 );
1845 assert!(
1846 fmt.contains("test-quota-project"),
1847 "Expected 'test-quota-project', got: {fmt}"
1848 );
1849 }
1850
1851 #[tokio::test]
1852 async fn programmatic_builder_returns_correct_headers() {
1853 let provider = Arc::new(TestSubjectTokenProvider);
1854 let sts_server = Server::run();
1855 let builder = ProgrammaticBuilder::new(provider)
1856 .with_audience("test-audience")
1857 .with_subject_token_type("test-token-type")
1858 .with_token_url(sts_server.url("/token").to_string())
1859 .with_quota_project_id("test-quota-project");
1860
1861 let creds = builder.build().unwrap();
1862
1863 sts_server.expect(
1864 Expectation::matching(all_of![
1865 request::method_path("POST", "/token"),
1866 request::body(url_decoded(contains((
1867 "grant_type",
1868 TOKEN_EXCHANGE_GRANT_TYPE
1869 )))),
1870 request::body(url_decoded(contains((
1871 "subject_token",
1872 "test-subject-token"
1873 )))),
1874 request::body(url_decoded(contains((
1875 "requested_token_type",
1876 ACCESS_TOKEN_TYPE
1877 )))),
1878 request::body(url_decoded(contains((
1879 "subject_token_type",
1880 "test-token-type"
1881 )))),
1882 request::body(url_decoded(contains(("audience", "test-audience")))),
1883 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1884 ])
1885 .respond_with(json_encoded(json!({
1886 "access_token": "sts-only-token",
1887 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1888 "token_type": "Bearer",
1889 "expires_in": 3600,
1890 }))),
1891 );
1892
1893 let headers = creds.headers(Extensions::new()).await.unwrap();
1894 match headers {
1895 CacheableResource::New { data, .. } => {
1896 let token = data.get("authorization").unwrap().to_str().unwrap();
1897 assert_eq!(token, "Bearer sts-only-token");
1898 let quota_project = data.get("x-goog-user-project").unwrap().to_str().unwrap();
1899 assert_eq!(quota_project, "test-quota-project");
1900 }
1901 CacheableResource::NotModified => panic!("Expected new headers"),
1902 }
1903 }
1904
1905 #[tokio::test]
1906 async fn create_programmatic_builder_fails_on_missing_required_field() {
1907 let provider = Arc::new(TestSubjectTokenProvider);
1908 let result = ProgrammaticBuilder::new(provider)
1909 .with_subject_token_type("test-token-type")
1910 .with_token_url("http://test.com/token")
1912 .build();
1913
1914 assert!(result.is_err());
1915 let error_string = result.unwrap_err().to_string();
1916 assert!(
1917 error_string.contains("missing required field: audience"),
1918 "Expected error about missing 'audience', got: {error_string}"
1919 );
1920 }
1921
1922 #[tokio::test]
1923 async fn test_external_account_retries_on_transient_failures() {
1924 let mut subject_token_server = Server::run();
1925 let mut sts_server = Server::run();
1926
1927 let contents = json!({
1928 "type": "external_account",
1929 "audience": "audience",
1930 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1931 "token_url": sts_server.url("/token").to_string(),
1932 "credential_source": {
1933 "url": subject_token_server.url("/subject_token").to_string(),
1934 }
1935 });
1936
1937 subject_token_server.expect(
1938 Expectation::matching(request::method_path("GET", "/subject_token"))
1939 .times(3)
1940 .respond_with(json_encoded(json!({
1941 "access_token": "subject_token",
1942 }))),
1943 );
1944
1945 sts_server.expect(
1946 Expectation::matching(request::method_path("POST", "/token"))
1947 .times(3)
1948 .respond_with(status_code(503)),
1949 );
1950
1951 let creds = Builder::new(contents)
1952 .with_retry_policy(get_mock_auth_retry_policy(3))
1953 .with_backoff_policy(get_mock_backoff_policy())
1954 .with_retry_throttler(get_mock_retry_throttler())
1955 .build()
1956 .unwrap();
1957
1958 let err = creds.headers(Extensions::new()).await.unwrap_err();
1959 assert!(!err.is_transient());
1960 sts_server.verify_and_clear();
1961 subject_token_server.verify_and_clear();
1962 }
1963
1964 #[tokio::test]
1965 async fn test_external_account_does_not_retry_on_non_transient_failures() {
1966 let subject_token_server = Server::run();
1967 let mut sts_server = Server::run();
1968
1969 let contents = json!({
1970 "type": "external_account",
1971 "audience": "audience",
1972 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1973 "token_url": sts_server.url("/token").to_string(),
1974 "credential_source": {
1975 "url": subject_token_server.url("/subject_token").to_string(),
1976 }
1977 });
1978
1979 subject_token_server.expect(
1980 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1981 json_encoded(json!({
1982 "access_token": "subject_token",
1983 })),
1984 ),
1985 );
1986
1987 sts_server.expect(
1988 Expectation::matching(request::method_path("POST", "/token"))
1989 .times(1)
1990 .respond_with(status_code(401)),
1991 );
1992
1993 let creds = Builder::new(contents)
1994 .with_retry_policy(get_mock_auth_retry_policy(1))
1995 .with_backoff_policy(get_mock_backoff_policy())
1996 .with_retry_throttler(get_mock_retry_throttler())
1997 .build()
1998 .unwrap();
1999
2000 let err = creds.headers(Extensions::new()).await.unwrap_err();
2001 assert!(!err.is_transient());
2002 sts_server.verify_and_clear();
2003 }
2004
2005 #[tokio::test]
2006 async fn test_external_account_retries_for_success() {
2007 let mut subject_token_server = Server::run();
2008 let mut sts_server = Server::run();
2009
2010 let contents = json!({
2011 "type": "external_account",
2012 "audience": "audience",
2013 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2014 "token_url": sts_server.url("/token").to_string(),
2015 "credential_source": {
2016 "url": subject_token_server.url("/subject_token").to_string(),
2017 }
2018 });
2019
2020 subject_token_server.expect(
2021 Expectation::matching(request::method_path("GET", "/subject_token"))
2022 .times(3)
2023 .respond_with(json_encoded(json!({
2024 "access_token": "subject_token",
2025 }))),
2026 );
2027
2028 sts_server.expect(
2029 Expectation::matching(request::method_path("POST", "/token"))
2030 .times(3)
2031 .respond_with(cycle![
2032 status_code(503).body("try-again"),
2033 status_code(503).body("try-again"),
2034 json_encoded(json!({
2035 "access_token": "sts-only-token",
2036 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2037 "token_type": "Bearer",
2038 "expires_in": 3600,
2039 }))
2040 ]),
2041 );
2042
2043 let creds = Builder::new(contents)
2044 .with_retry_policy(get_mock_auth_retry_policy(3))
2045 .with_backoff_policy(get_mock_backoff_policy())
2046 .with_retry_throttler(get_mock_retry_throttler())
2047 .build()
2048 .unwrap();
2049
2050 let headers = creds.headers(Extensions::new()).await.unwrap();
2051 match headers {
2052 CacheableResource::New { data, .. } => {
2053 let token = data.get("authorization").unwrap().to_str().unwrap();
2054 assert_eq!(token, "Bearer sts-only-token");
2055 }
2056 CacheableResource::NotModified => panic!("Expected new headers"),
2057 }
2058 sts_server.verify_and_clear();
2059 subject_token_server.verify_and_clear();
2060 }
2061
2062 #[tokio::test]
2063 async fn test_programmatic_builder_retries_on_transient_failures() {
2064 let provider = Arc::new(TestSubjectTokenProvider);
2065 let mut sts_server = Server::run();
2066
2067 sts_server.expect(
2068 Expectation::matching(request::method_path("POST", "/token"))
2069 .times(3)
2070 .respond_with(status_code(503)),
2071 );
2072
2073 let creds = ProgrammaticBuilder::new(provider)
2074 .with_audience("test-audience")
2075 .with_subject_token_type("test-token-type")
2076 .with_token_url(sts_server.url("/token").to_string())
2077 .with_retry_policy(get_mock_auth_retry_policy(3))
2078 .with_backoff_policy(get_mock_backoff_policy())
2079 .with_retry_throttler(get_mock_retry_throttler())
2080 .build()
2081 .unwrap();
2082
2083 let err = creds.headers(Extensions::new()).await.unwrap_err();
2084 assert!(!err.is_transient());
2085 sts_server.verify_and_clear();
2086 }
2087
2088 #[tokio::test]
2089 async fn test_programmatic_builder_does_not_retry_on_non_transient_failures() {
2090 let provider = Arc::new(TestSubjectTokenProvider);
2091 let mut sts_server = Server::run();
2092
2093 sts_server.expect(
2094 Expectation::matching(request::method_path("POST", "/token"))
2095 .times(1)
2096 .respond_with(status_code(401)),
2097 );
2098
2099 let creds = ProgrammaticBuilder::new(provider)
2100 .with_audience("test-audience")
2101 .with_subject_token_type("test-token-type")
2102 .with_token_url(sts_server.url("/token").to_string())
2103 .with_retry_policy(get_mock_auth_retry_policy(1))
2104 .with_backoff_policy(get_mock_backoff_policy())
2105 .with_retry_throttler(get_mock_retry_throttler())
2106 .build()
2107 .unwrap();
2108
2109 let err = creds.headers(Extensions::new()).await.unwrap_err();
2110 assert!(!err.is_transient());
2111 sts_server.verify_and_clear();
2112 }
2113
2114 #[tokio::test]
2115 async fn test_programmatic_builder_retries_for_success() {
2116 let provider = Arc::new(TestSubjectTokenProvider);
2117 let mut sts_server = Server::run();
2118
2119 sts_server.expect(
2120 Expectation::matching(request::method_path("POST", "/token"))
2121 .times(3)
2122 .respond_with(cycle![
2123 status_code(503).body("try-again"),
2124 status_code(503).body("try-again"),
2125 json_encoded(json!({
2126 "access_token": "sts-only-token",
2127 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2128 "token_type": "Bearer",
2129 "expires_in": 3600,
2130 }))
2131 ]),
2132 );
2133
2134 let creds = ProgrammaticBuilder::new(provider)
2135 .with_audience("test-audience")
2136 .with_subject_token_type("test-token-type")
2137 .with_token_url(sts_server.url("/token").to_string())
2138 .with_retry_policy(get_mock_auth_retry_policy(3))
2139 .with_backoff_policy(get_mock_backoff_policy())
2140 .with_retry_throttler(get_mock_retry_throttler())
2141 .build()
2142 .unwrap();
2143
2144 let headers = creds.headers(Extensions::new()).await.unwrap();
2145 match headers {
2146 CacheableResource::New { data, .. } => {
2147 let token = data.get("authorization").unwrap().to_str().unwrap();
2148 assert_eq!(token, "Bearer sts-only-token");
2149 }
2150 CacheableResource::NotModified => panic!("Expected new headers"),
2151 }
2152 sts_server.verify_and_clear();
2153 }
2154
2155 #[tokio::test]
2156 async fn test_kubernetes_wif_direct_identity_parsing() {
2157 let contents = json!({
2158 "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/pool-name/providers/k8s-cluster",
2159 "credential_source": {
2160 "file": "/var/run/service-account/token"
2161 },
2162 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2163 "token_url": "https://sts.googleapis.com/v1/token",
2164 "type": "external_account"
2165 });
2166
2167 let file: ExternalAccountFile = serde_json::from_value(contents)
2168 .expect("failed to parse kubernetes WIF direct identity config");
2169 let config: ExternalAccountConfig = file.into();
2170
2171 match config.credential_source {
2172 CredentialSource::File(source) => {
2173 assert_eq!(source.file, "/var/run/service-account/token");
2174 assert_eq!(source.format, "text"); assert_eq!(source.subject_token_field_name, ""); }
2177 _ => {
2178 unreachable!("expected File sourced credential")
2179 }
2180 }
2181 }
2182
2183 #[tokio::test]
2184 async fn test_kubernetes_wif_impersonation_parsing() {
2185 let contents = json!({
2186 "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/pool-name/providers/k8s-cluster",
2187 "credential_source": {
2188 "file": "/var/run/service-account/token",
2189 "format": {
2190 "type": "text"
2191 }
2192 },
2193 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-sa@test-project.iam.gserviceaccount.com:generateAccessToken",
2194 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2195 "token_url": "https://sts.googleapis.com/v1/token",
2196 "type": "external_account",
2197 "universe_domain": "googleapis.com"
2198 });
2199
2200 let file: ExternalAccountFile = serde_json::from_value(contents)
2201 .expect("failed to parse kubernetes WIF impersonation config");
2202 let config: ExternalAccountConfig = file.into();
2203
2204 match config.credential_source {
2205 CredentialSource::File(source) => {
2206 assert_eq!(source.file, "/var/run/service-account/token");
2207 assert_eq!(source.format, "text");
2208 assert_eq!(source.subject_token_field_name, ""); }
2210 _ => {
2211 unreachable!("expected File sourced credential")
2212 }
2213 }
2214
2215 assert_eq!(
2216 config.service_account_impersonation_url,
2217 Some("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-sa@test-project.iam.gserviceaccount.com:generateAccessToken".to_string())
2218 );
2219 }
2220}