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::AuthHeadersBuilder;
125use crate::retry::Builder as RetryTokenProviderBuilder;
126use crate::token::{CachedTokenProvider, Token, TokenProvider};
127use crate::token_cache::TokenCache;
128use crate::{BuildResult, Result};
129use google_cloud_gax::backoff_policy::BackoffPolicyArg;
130use google_cloud_gax::retry_policy::RetryPolicyArg;
131use google_cloud_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
1283 AuthHeadersBuilder::new(&token)
1284 .maybe_quota_project_id(self.quota_project_id.as_deref())
1285 .build()
1286 }
1287}
1288
1289#[async_trait::async_trait]
1290impl<T> AccessTokenCredentialsProvider for ExternalAccountCredentials<T>
1291where
1292 T: CachedTokenProvider,
1293{
1294 async fn access_token(&self) -> Result<AccessToken> {
1295 let token = self.token_provider.token(Extensions::new()).await?;
1296 token.into()
1297 }
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302 use super::*;
1303 use crate::constants::{
1304 ACCESS_TOKEN_TYPE, DEFAULT_SCOPE, JWT_TOKEN_TYPE, TOKEN_EXCHANGE_GRANT_TYPE,
1305 };
1306 use crate::credentials::subject_token::{
1307 Builder as SubjectTokenBuilder, SubjectToken, SubjectTokenProvider,
1308 };
1309 use crate::credentials::tests::{
1310 find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
1311 get_mock_retry_throttler,
1312 };
1313 use crate::errors::{CredentialsError, SubjectTokenProviderError};
1314 use httptest::{
1315 Expectation, Server, cycle,
1316 matchers::{all_of, contains, request, url_decoded},
1317 responders::{json_encoded, status_code},
1318 };
1319 use serde_json::*;
1320 use std::collections::HashMap;
1321 use std::error::Error;
1322 use std::fmt;
1323 use test_case::test_case;
1324 use time::OffsetDateTime;
1325
1326 #[derive(Debug)]
1327 struct TestProviderError;
1328 impl fmt::Display for TestProviderError {
1329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1330 write!(f, "TestProviderError")
1331 }
1332 }
1333 impl Error for TestProviderError {}
1334 impl SubjectTokenProviderError for TestProviderError {
1335 fn is_transient(&self) -> bool {
1336 false
1337 }
1338 }
1339
1340 #[derive(Debug)]
1341 struct TestSubjectTokenProvider;
1342 impl SubjectTokenProvider for TestSubjectTokenProvider {
1343 type Error = TestProviderError;
1344 async fn subject_token(&self) -> std::result::Result<SubjectToken, Self::Error> {
1345 Ok(SubjectTokenBuilder::new("test-subject-token".to_string()).build())
1346 }
1347 }
1348
1349 #[tokio::test]
1350 async fn create_external_account_builder() {
1351 let contents = json!({
1352 "type": "external_account",
1353 "audience": "audience",
1354 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1355 "token_url": "https://sts.googleapis.com/v1beta/token",
1356 "credential_source": {
1357 "url": "https://example.com/token",
1358 "format": {
1359 "type": "json",
1360 "subject_token_field_name": "access_token"
1361 }
1362 }
1363 });
1364
1365 let creds = Builder::new(contents)
1366 .with_quota_project_id("test_project")
1367 .with_scopes(["a", "b"])
1368 .build()
1369 .unwrap();
1370
1371 let fmt = format!("{creds:?}");
1373 assert!(fmt.contains("ExternalAccountCredentials"));
1374 }
1375
1376 #[tokio::test]
1377 async fn create_external_account_detect_url_sourced() {
1378 let contents = json!({
1379 "type": "external_account",
1380 "audience": "audience",
1381 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1382 "token_url": "https://sts.googleapis.com/v1beta/token",
1383 "credential_source": {
1384 "url": "https://example.com/token",
1385 "headers": {
1386 "Metadata": "True"
1387 },
1388 "format": {
1389 "type": "json",
1390 "subject_token_field_name": "access_token"
1391 }
1392 }
1393 });
1394
1395 let file: ExternalAccountFile =
1396 serde_json::from_value(contents).expect("failed to parse external account config");
1397 let config: ExternalAccountConfig = file.into();
1398 let source = config.credential_source;
1399
1400 match source {
1401 CredentialSource::Url(source) => {
1402 assert_eq!(source.url, "https://example.com/token");
1403 assert_eq!(
1404 source.headers,
1405 HashMap::from([("Metadata".to_string(), "True".to_string()),]),
1406 );
1407 assert_eq!(source.format, "json");
1408 assert_eq!(source.subject_token_field_name, "access_token");
1409 }
1410 _ => {
1411 unreachable!("expected Url Sourced credential")
1412 }
1413 }
1414 }
1415
1416 #[tokio::test]
1417 async fn create_external_account_detect_executable_sourced() {
1418 let contents = json!({
1419 "type": "external_account",
1420 "audience": "audience",
1421 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1422 "token_url": "https://sts.googleapis.com/v1beta/token",
1423 "credential_source": {
1424 "executable": {
1425 "command": "cat /some/file",
1426 "output_file": "/some/file",
1427 "timeout_millis": 5000
1428 }
1429 }
1430 });
1431
1432 let file: ExternalAccountFile =
1433 serde_json::from_value(contents).expect("failed to parse external account config");
1434 let config: ExternalAccountConfig = file.into();
1435 let source = config.credential_source;
1436
1437 match source {
1438 CredentialSource::Executable(source) => {
1439 assert_eq!(source.command, "cat");
1440 assert_eq!(source.args, vec!["/some/file"]);
1441 assert_eq!(source.output_file.as_deref(), Some("/some/file"));
1442 assert_eq!(source.timeout, Duration::from_secs(5));
1443 }
1444 _ => {
1445 unreachable!("expected Executable Sourced credential")
1446 }
1447 }
1448 }
1449
1450 #[tokio::test]
1451 async fn create_external_account_detect_file_sourced() {
1452 let contents = json!({
1453 "type": "external_account",
1454 "audience": "audience",
1455 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1456 "token_url": "https://sts.googleapis.com/v1beta/token",
1457 "credential_source": {
1458 "file": "/foo/bar",
1459 "format": {
1460 "type": "json",
1461 "subject_token_field_name": "token"
1462 }
1463 }
1464 });
1465
1466 let file: ExternalAccountFile =
1467 serde_json::from_value(contents).expect("failed to parse external account config");
1468 let config: ExternalAccountConfig = file.into();
1469 let source = config.credential_source;
1470
1471 match source {
1472 CredentialSource::File(source) => {
1473 assert_eq!(source.file, "/foo/bar");
1474 assert_eq!(source.format, "json");
1475 assert_eq!(source.subject_token_field_name, "token");
1476 }
1477 _ => {
1478 unreachable!("expected File Sourced credential")
1479 }
1480 }
1481 }
1482
1483 #[tokio::test]
1484 async fn test_external_account_with_impersonation_success() {
1485 let subject_token_server = Server::run();
1486 let sts_server = Server::run();
1487 let impersonation_server = Server::run();
1488
1489 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1490 let contents = json!({
1491 "type": "external_account",
1492 "audience": "audience",
1493 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1494 "token_url": sts_server.url("/token").to_string(),
1495 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1496 "credential_source": {
1497 "url": subject_token_server.url("/subject_token").to_string(),
1498 "format": {
1499 "type": "json",
1500 "subject_token_field_name": "access_token"
1501 }
1502 }
1503 });
1504
1505 subject_token_server.expect(
1506 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1507 json_encoded(json!({
1508 "access_token": "subject_token",
1509 })),
1510 ),
1511 );
1512
1513 sts_server.expect(
1514 Expectation::matching(all_of![
1515 request::method_path("POST", "/token"),
1516 request::body(url_decoded(contains((
1517 "grant_type",
1518 TOKEN_EXCHANGE_GRANT_TYPE
1519 )))),
1520 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1521 request::body(url_decoded(contains((
1522 "requested_token_type",
1523 ACCESS_TOKEN_TYPE
1524 )))),
1525 request::body(url_decoded(contains((
1526 "subject_token_type",
1527 JWT_TOKEN_TYPE
1528 )))),
1529 request::body(url_decoded(contains(("audience", "audience")))),
1530 request::body(url_decoded(contains(("scope", IAM_SCOPE)))),
1531 ])
1532 .respond_with(json_encoded(json!({
1533 "access_token": "sts-token",
1534 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1535 "token_type": "Bearer",
1536 "expires_in": 3600,
1537 }))),
1538 );
1539
1540 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1541 .format(&time::format_description::well_known::Rfc3339)
1542 .unwrap();
1543 impersonation_server.expect(
1544 Expectation::matching(all_of![
1545 request::method_path("POST", impersonation_path),
1546 request::headers(contains(("authorization", "Bearer sts-token"))),
1547 ])
1548 .respond_with(json_encoded(json!({
1549 "accessToken": "final-impersonated-token",
1550 "expireTime": expire_time
1551 }))),
1552 );
1553
1554 let creds = Builder::new(contents).build().unwrap();
1555 let headers = creds.headers(Extensions::new()).await.unwrap();
1556 match headers {
1557 CacheableResource::New { data, .. } => {
1558 let token = data.get("authorization").unwrap().to_str().unwrap();
1559 assert_eq!(token, "Bearer final-impersonated-token");
1560 }
1561 CacheableResource::NotModified => panic!("Expected new headers"),
1562 }
1563 }
1564
1565 #[tokio::test]
1566 async fn test_external_account_without_impersonation_success() {
1567 let subject_token_server = Server::run();
1568 let sts_server = Server::run();
1569
1570 let contents = json!({
1571 "type": "external_account",
1572 "audience": "audience",
1573 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1574 "token_url": sts_server.url("/token").to_string(),
1575 "credential_source": {
1576 "url": subject_token_server.url("/subject_token").to_string(),
1577 "format": {
1578 "type": "json",
1579 "subject_token_field_name": "access_token"
1580 }
1581 }
1582 });
1583
1584 subject_token_server.expect(
1585 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1586 json_encoded(json!({
1587 "access_token": "subject_token",
1588 })),
1589 ),
1590 );
1591
1592 sts_server.expect(
1593 Expectation::matching(all_of![
1594 request::method_path("POST", "/token"),
1595 request::body(url_decoded(contains((
1596 "grant_type",
1597 TOKEN_EXCHANGE_GRANT_TYPE
1598 )))),
1599 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1600 request::body(url_decoded(contains((
1601 "requested_token_type",
1602 ACCESS_TOKEN_TYPE
1603 )))),
1604 request::body(url_decoded(contains((
1605 "subject_token_type",
1606 JWT_TOKEN_TYPE
1607 )))),
1608 request::body(url_decoded(contains(("audience", "audience")))),
1609 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1610 ])
1611 .respond_with(json_encoded(json!({
1612 "access_token": "sts-only-token",
1613 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1614 "token_type": "Bearer",
1615 "expires_in": 3600,
1616 }))),
1617 );
1618
1619 let creds = Builder::new(contents).build().unwrap();
1620 let headers = creds.headers(Extensions::new()).await.unwrap();
1621 match headers {
1622 CacheableResource::New { data, .. } => {
1623 let token = data.get("authorization").unwrap().to_str().unwrap();
1624 assert_eq!(token, "Bearer sts-only-token");
1625 }
1626 CacheableResource::NotModified => panic!("Expected new headers"),
1627 }
1628 }
1629
1630 #[tokio::test]
1631 async fn test_external_account_access_token_credentials_success() {
1632 let server = Server::run();
1633
1634 let contents = json!({
1635 "type": "external_account",
1636 "audience": "audience",
1637 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1638 "token_url": server.url("/token").to_string(),
1639 "credential_source": {
1640 "url": server.url("/subject_token").to_string(),
1641 "format": {
1642 "type": "json",
1643 "subject_token_field_name": "access_token"
1644 }
1645 }
1646 });
1647
1648 server.expect(
1649 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1650 json_encoded(json!({
1651 "access_token": "subject_token",
1652 })),
1653 ),
1654 );
1655
1656 server.expect(
1657 Expectation::matching(all_of![
1658 request::method_path("POST", "/token"),
1659 request::body(url_decoded(contains((
1660 "grant_type",
1661 TOKEN_EXCHANGE_GRANT_TYPE
1662 )))),
1663 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1664 request::body(url_decoded(contains((
1665 "requested_token_type",
1666 ACCESS_TOKEN_TYPE
1667 )))),
1668 request::body(url_decoded(contains((
1669 "subject_token_type",
1670 JWT_TOKEN_TYPE
1671 )))),
1672 request::body(url_decoded(contains(("audience", "audience")))),
1673 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1674 ])
1675 .respond_with(json_encoded(json!({
1676 "access_token": "sts-only-token",
1677 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1678 "token_type": "Bearer",
1679 "expires_in": 3600,
1680 }))),
1681 );
1682
1683 let creds = Builder::new(contents)
1684 .build_access_token_credentials()
1685 .unwrap();
1686 let access_token = creds.access_token().await.unwrap();
1687 assert_eq!(access_token.token, "sts-only-token");
1688 }
1689
1690 #[tokio::test]
1691 async fn test_impersonation_flow_sts_call_fails() {
1692 let subject_token_server = Server::run();
1693 let sts_server = Server::run();
1694 let impersonation_server = Server::run();
1695
1696 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1697 let contents = json!({
1698 "type": "external_account",
1699 "audience": "audience",
1700 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1701 "token_url": sts_server.url("/token").to_string(),
1702 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1703 "credential_source": {
1704 "url": subject_token_server.url("/subject_token").to_string(),
1705 "format": {
1706 "type": "json",
1707 "subject_token_field_name": "access_token"
1708 }
1709 }
1710 });
1711
1712 subject_token_server.expect(
1713 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1714 json_encoded(json!({
1715 "access_token": "subject_token",
1716 })),
1717 ),
1718 );
1719
1720 sts_server.expect(
1721 Expectation::matching(request::method_path("POST", "/token"))
1722 .respond_with(status_code(500)),
1723 );
1724
1725 let creds = Builder::new(contents).build().unwrap();
1726 let err = creds.headers(Extensions::new()).await.unwrap_err();
1727 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1728 assert!(
1729 original_err
1730 .to_string()
1731 .contains("failed to exchange token")
1732 );
1733 assert!(original_err.is_transient());
1734 }
1735
1736 #[tokio::test]
1737 async fn test_impersonation_flow_iam_call_fails() {
1738 let subject_token_server = Server::run();
1739 let sts_server = Server::run();
1740 let impersonation_server = Server::run();
1741
1742 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1743 let contents = json!({
1744 "type": "external_account",
1745 "audience": "audience",
1746 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1747 "token_url": sts_server.url("/token").to_string(),
1748 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1749 "credential_source": {
1750 "url": subject_token_server.url("/subject_token").to_string(),
1751 "format": {
1752 "type": "json",
1753 "subject_token_field_name": "access_token"
1754 }
1755 }
1756 });
1757
1758 subject_token_server.expect(
1759 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1760 json_encoded(json!({
1761 "access_token": "subject_token",
1762 })),
1763 ),
1764 );
1765
1766 sts_server.expect(
1767 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1768 json_encoded(json!({
1769 "access_token": "sts-token",
1770 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1771 "token_type": "Bearer",
1772 "expires_in": 3600,
1773 })),
1774 ),
1775 );
1776
1777 impersonation_server.expect(
1778 Expectation::matching(request::method_path("POST", impersonation_path))
1779 .respond_with(status_code(403)),
1780 );
1781
1782 let creds = Builder::new(contents).build().unwrap();
1783 let err = creds.headers(Extensions::new()).await.unwrap_err();
1784 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1785 assert!(original_err.to_string().contains("failed to fetch token"));
1786 assert!(!original_err.is_transient());
1787 }
1788
1789 #[test_case(Some(vec!["scope1", "scope2"]), Some("http://custom.com/token") ; "with custom scopes and token_url")]
1790 #[test_case(None, Some("http://custom.com/token") ; "with default scopes and custom token_url")]
1791 #[test_case(Some(vec!["scope1", "scope2"]), None ; "with custom scopes and default token_url")]
1792 #[test_case(None, None ; "with default scopes and default token_url")]
1793 #[tokio::test]
1794 async fn create_programmatic_builder(scopes: Option<Vec<&str>>, token_url: Option<&str>) {
1795 let provider = Arc::new(TestSubjectTokenProvider);
1796 let mut builder = ProgrammaticBuilder::new(provider)
1797 .with_audience("test-audience")
1798 .with_subject_token_type("test-token-type")
1799 .with_client_id("test-client-id")
1800 .with_client_secret("test-client-secret")
1801 .with_target_principal("test-principal");
1802
1803 let expected_scopes = if let Some(scopes) = scopes.clone() {
1804 scopes.iter().map(|s| s.to_string()).collect()
1805 } else {
1806 vec![DEFAULT_SCOPE.to_string()]
1807 };
1808
1809 let expected_token_url = token_url.unwrap_or(STS_TOKEN_URL).to_string();
1810
1811 if let Some(scopes) = scopes {
1812 builder = builder.with_scopes(scopes);
1813 }
1814 if let Some(token_url) = token_url {
1815 builder = builder.with_token_url(token_url);
1816 }
1817
1818 let (config, _, _) = builder.build_components().unwrap();
1819
1820 assert_eq!(config.audience, "test-audience");
1821 assert_eq!(config.subject_token_type, "test-token-type");
1822 assert_eq!(config.client_id, Some("test-client-id".to_string()));
1823 assert_eq!(config.client_secret, Some("test-client-secret".to_string()));
1824 assert_eq!(config.scopes, expected_scopes);
1825 assert_eq!(config.token_url, expected_token_url);
1826 assert_eq!(
1827 config.service_account_impersonation_url,
1828 Some("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string())
1829 );
1830 }
1831
1832 #[tokio::test]
1833 async fn create_programmatic_builder_with_quota_project_id() {
1834 let provider = Arc::new(TestSubjectTokenProvider);
1835 let builder = ProgrammaticBuilder::new(provider)
1836 .with_audience("test-audience")
1837 .with_subject_token_type("test-token-type")
1838 .with_token_url(STS_TOKEN_URL)
1839 .with_quota_project_id("test-quota-project");
1840
1841 let creds = builder.build().unwrap();
1842
1843 let fmt = format!("{creds:?}");
1844 assert!(
1845 fmt.contains("ExternalAccountCredentials"),
1846 "Expected 'ExternalAccountCredentials', got: {fmt}"
1847 );
1848 assert!(
1849 fmt.contains("test-quota-project"),
1850 "Expected 'test-quota-project', got: {fmt}"
1851 );
1852 }
1853
1854 #[tokio::test]
1855 async fn programmatic_builder_returns_correct_headers() {
1856 let provider = Arc::new(TestSubjectTokenProvider);
1857 let sts_server = Server::run();
1858 let builder = ProgrammaticBuilder::new(provider)
1859 .with_audience("test-audience")
1860 .with_subject_token_type("test-token-type")
1861 .with_token_url(sts_server.url("/token").to_string())
1862 .with_quota_project_id("test-quota-project");
1863
1864 let creds = builder.build().unwrap();
1865
1866 sts_server.expect(
1867 Expectation::matching(all_of![
1868 request::method_path("POST", "/token"),
1869 request::body(url_decoded(contains((
1870 "grant_type",
1871 TOKEN_EXCHANGE_GRANT_TYPE
1872 )))),
1873 request::body(url_decoded(contains((
1874 "subject_token",
1875 "test-subject-token"
1876 )))),
1877 request::body(url_decoded(contains((
1878 "requested_token_type",
1879 ACCESS_TOKEN_TYPE
1880 )))),
1881 request::body(url_decoded(contains((
1882 "subject_token_type",
1883 "test-token-type"
1884 )))),
1885 request::body(url_decoded(contains(("audience", "test-audience")))),
1886 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1887 ])
1888 .respond_with(json_encoded(json!({
1889 "access_token": "sts-only-token",
1890 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1891 "token_type": "Bearer",
1892 "expires_in": 3600,
1893 }))),
1894 );
1895
1896 let headers = creds.headers(Extensions::new()).await.unwrap();
1897 match headers {
1898 CacheableResource::New { data, .. } => {
1899 let token = data.get("authorization").unwrap().to_str().unwrap();
1900 assert_eq!(token, "Bearer sts-only-token");
1901 let quota_project = data.get("x-goog-user-project").unwrap().to_str().unwrap();
1902 assert_eq!(quota_project, "test-quota-project");
1903 }
1904 CacheableResource::NotModified => panic!("Expected new headers"),
1905 }
1906 }
1907
1908 #[tokio::test]
1909 async fn create_programmatic_builder_fails_on_missing_required_field() {
1910 let provider = Arc::new(TestSubjectTokenProvider);
1911 let result = ProgrammaticBuilder::new(provider)
1912 .with_subject_token_type("test-token-type")
1913 .with_token_url("http://test.com/token")
1915 .build();
1916
1917 assert!(result.is_err(), "{result:?}");
1918 let error_string = result.unwrap_err().to_string();
1919 assert!(
1920 error_string.contains("missing required field: audience"),
1921 "Expected error about missing 'audience', got: {error_string}"
1922 );
1923 }
1924
1925 #[tokio::test]
1926 async fn test_external_account_retries_on_transient_failures() {
1927 let mut subject_token_server = Server::run();
1928 let mut sts_server = Server::run();
1929
1930 let contents = json!({
1931 "type": "external_account",
1932 "audience": "audience",
1933 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1934 "token_url": sts_server.url("/token").to_string(),
1935 "credential_source": {
1936 "url": subject_token_server.url("/subject_token").to_string(),
1937 }
1938 });
1939
1940 subject_token_server.expect(
1941 Expectation::matching(request::method_path("GET", "/subject_token"))
1942 .times(3)
1943 .respond_with(json_encoded(json!({
1944 "access_token": "subject_token",
1945 }))),
1946 );
1947
1948 sts_server.expect(
1949 Expectation::matching(request::method_path("POST", "/token"))
1950 .times(3)
1951 .respond_with(status_code(503)),
1952 );
1953
1954 let creds = Builder::new(contents)
1955 .with_retry_policy(get_mock_auth_retry_policy(3))
1956 .with_backoff_policy(get_mock_backoff_policy())
1957 .with_retry_throttler(get_mock_retry_throttler())
1958 .build()
1959 .unwrap();
1960
1961 let err = creds.headers(Extensions::new()).await.unwrap_err();
1962 assert!(err.is_transient(), "{err:?}");
1963 sts_server.verify_and_clear();
1964 subject_token_server.verify_and_clear();
1965 }
1966
1967 #[tokio::test]
1968 async fn test_external_account_does_not_retry_on_non_transient_failures() {
1969 let subject_token_server = Server::run();
1970 let mut sts_server = Server::run();
1971
1972 let contents = json!({
1973 "type": "external_account",
1974 "audience": "audience",
1975 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1976 "token_url": sts_server.url("/token").to_string(),
1977 "credential_source": {
1978 "url": subject_token_server.url("/subject_token").to_string(),
1979 }
1980 });
1981
1982 subject_token_server.expect(
1983 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1984 json_encoded(json!({
1985 "access_token": "subject_token",
1986 })),
1987 ),
1988 );
1989
1990 sts_server.expect(
1991 Expectation::matching(request::method_path("POST", "/token"))
1992 .times(1)
1993 .respond_with(status_code(401)),
1994 );
1995
1996 let creds = Builder::new(contents)
1997 .with_retry_policy(get_mock_auth_retry_policy(1))
1998 .with_backoff_policy(get_mock_backoff_policy())
1999 .with_retry_throttler(get_mock_retry_throttler())
2000 .build()
2001 .unwrap();
2002
2003 let err = creds.headers(Extensions::new()).await.unwrap_err();
2004 assert!(!err.is_transient());
2005 sts_server.verify_and_clear();
2006 }
2007
2008 #[tokio::test]
2009 async fn test_external_account_retries_for_success() {
2010 let mut subject_token_server = Server::run();
2011 let mut sts_server = Server::run();
2012
2013 let contents = json!({
2014 "type": "external_account",
2015 "audience": "audience",
2016 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2017 "token_url": sts_server.url("/token").to_string(),
2018 "credential_source": {
2019 "url": subject_token_server.url("/subject_token").to_string(),
2020 }
2021 });
2022
2023 subject_token_server.expect(
2024 Expectation::matching(request::method_path("GET", "/subject_token"))
2025 .times(3)
2026 .respond_with(json_encoded(json!({
2027 "access_token": "subject_token",
2028 }))),
2029 );
2030
2031 sts_server.expect(
2032 Expectation::matching(request::method_path("POST", "/token"))
2033 .times(3)
2034 .respond_with(cycle![
2035 status_code(503).body("try-again"),
2036 status_code(503).body("try-again"),
2037 json_encoded(json!({
2038 "access_token": "sts-only-token",
2039 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2040 "token_type": "Bearer",
2041 "expires_in": 3600,
2042 }))
2043 ]),
2044 );
2045
2046 let creds = Builder::new(contents)
2047 .with_retry_policy(get_mock_auth_retry_policy(3))
2048 .with_backoff_policy(get_mock_backoff_policy())
2049 .with_retry_throttler(get_mock_retry_throttler())
2050 .build()
2051 .unwrap();
2052
2053 let headers = creds.headers(Extensions::new()).await.unwrap();
2054 match headers {
2055 CacheableResource::New { data, .. } => {
2056 let token = data.get("authorization").unwrap().to_str().unwrap();
2057 assert_eq!(token, "Bearer sts-only-token");
2058 }
2059 CacheableResource::NotModified => panic!("Expected new headers"),
2060 }
2061 sts_server.verify_and_clear();
2062 subject_token_server.verify_and_clear();
2063 }
2064
2065 #[tokio::test]
2066 async fn test_programmatic_builder_retries_on_transient_failures() {
2067 let provider = Arc::new(TestSubjectTokenProvider);
2068 let mut sts_server = Server::run();
2069
2070 sts_server.expect(
2071 Expectation::matching(request::method_path("POST", "/token"))
2072 .times(3)
2073 .respond_with(status_code(503)),
2074 );
2075
2076 let creds = ProgrammaticBuilder::new(provider)
2077 .with_audience("test-audience")
2078 .with_subject_token_type("test-token-type")
2079 .with_token_url(sts_server.url("/token").to_string())
2080 .with_retry_policy(get_mock_auth_retry_policy(3))
2081 .with_backoff_policy(get_mock_backoff_policy())
2082 .with_retry_throttler(get_mock_retry_throttler())
2083 .build()
2084 .unwrap();
2085
2086 let err = creds.headers(Extensions::new()).await.unwrap_err();
2087 assert!(err.is_transient(), "{err:?}");
2088 sts_server.verify_and_clear();
2089 }
2090
2091 #[tokio::test]
2092 async fn test_programmatic_builder_does_not_retry_on_non_transient_failures() {
2093 let provider = Arc::new(TestSubjectTokenProvider);
2094 let mut sts_server = Server::run();
2095
2096 sts_server.expect(
2097 Expectation::matching(request::method_path("POST", "/token"))
2098 .times(1)
2099 .respond_with(status_code(401)),
2100 );
2101
2102 let creds = ProgrammaticBuilder::new(provider)
2103 .with_audience("test-audience")
2104 .with_subject_token_type("test-token-type")
2105 .with_token_url(sts_server.url("/token").to_string())
2106 .with_retry_policy(get_mock_auth_retry_policy(1))
2107 .with_backoff_policy(get_mock_backoff_policy())
2108 .with_retry_throttler(get_mock_retry_throttler())
2109 .build()
2110 .unwrap();
2111
2112 let err = creds.headers(Extensions::new()).await.unwrap_err();
2113 assert!(!err.is_transient());
2114 sts_server.verify_and_clear();
2115 }
2116
2117 #[tokio::test]
2118 async fn test_programmatic_builder_retries_for_success() {
2119 let provider = Arc::new(TestSubjectTokenProvider);
2120 let mut sts_server = Server::run();
2121
2122 sts_server.expect(
2123 Expectation::matching(request::method_path("POST", "/token"))
2124 .times(3)
2125 .respond_with(cycle![
2126 status_code(503).body("try-again"),
2127 status_code(503).body("try-again"),
2128 json_encoded(json!({
2129 "access_token": "sts-only-token",
2130 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2131 "token_type": "Bearer",
2132 "expires_in": 3600,
2133 }))
2134 ]),
2135 );
2136
2137 let creds = ProgrammaticBuilder::new(provider)
2138 .with_audience("test-audience")
2139 .with_subject_token_type("test-token-type")
2140 .with_token_url(sts_server.url("/token").to_string())
2141 .with_retry_policy(get_mock_auth_retry_policy(3))
2142 .with_backoff_policy(get_mock_backoff_policy())
2143 .with_retry_throttler(get_mock_retry_throttler())
2144 .build()
2145 .unwrap();
2146
2147 let headers = creds.headers(Extensions::new()).await.unwrap();
2148 match headers {
2149 CacheableResource::New { data, .. } => {
2150 let token = data.get("authorization").unwrap().to_str().unwrap();
2151 assert_eq!(token, "Bearer sts-only-token");
2152 }
2153 CacheableResource::NotModified => panic!("Expected new headers"),
2154 }
2155 sts_server.verify_and_clear();
2156 }
2157
2158 #[tokio::test]
2159 async fn test_kubernetes_wif_direct_identity_parsing() {
2160 let contents = json!({
2161 "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/pool-name/providers/k8s-cluster",
2162 "credential_source": {
2163 "file": "/var/run/service-account/token"
2164 },
2165 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2166 "token_url": "https://sts.googleapis.com/v1/token",
2167 "type": "external_account"
2168 });
2169
2170 let file: ExternalAccountFile = serde_json::from_value(contents)
2171 .expect("failed to parse kubernetes WIF direct identity config");
2172 let config: ExternalAccountConfig = file.into();
2173
2174 match config.credential_source {
2175 CredentialSource::File(source) => {
2176 assert_eq!(source.file, "/var/run/service-account/token");
2177 assert_eq!(source.format, "text"); assert_eq!(source.subject_token_field_name, ""); }
2180 _ => {
2181 unreachable!("expected File sourced credential")
2182 }
2183 }
2184 }
2185
2186 #[tokio::test]
2187 async fn test_kubernetes_wif_impersonation_parsing() {
2188 let contents = json!({
2189 "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/pool-name/providers/k8s-cluster",
2190 "credential_source": {
2191 "file": "/var/run/service-account/token",
2192 "format": {
2193 "type": "text"
2194 }
2195 },
2196 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-sa@test-project.iam.gserviceaccount.com:generateAccessToken",
2197 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2198 "token_url": "https://sts.googleapis.com/v1/token",
2199 "type": "external_account",
2200 "universe_domain": "googleapis.com"
2201 });
2202
2203 let file: ExternalAccountFile = serde_json::from_value(contents)
2204 .expect("failed to parse kubernetes WIF impersonation config");
2205 let config: ExternalAccountConfig = file.into();
2206
2207 match config.credential_source {
2208 CredentialSource::File(source) => {
2209 assert_eq!(source.file, "/var/run/service-account/token");
2210 assert_eq!(source.format, "text");
2211 assert_eq!(source.subject_token_field_name, ""); }
2213 _ => {
2214 unreachable!("expected File sourced credential")
2215 }
2216 }
2217
2218 assert_eq!(
2219 config.service_account_impersonation_url,
2220 Some("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-sa@test-project.iam.gserviceaccount.com:generateAccessToken".to_string())
2221 );
2222 }
2223}