267 282 | mod test {
|
268 283 | use crate::credential_process::CredentialProcessProvider;
|
269 284 | use crate::sensitive_command::CommandWithSensitiveArgs;
|
270 285 | use aws_credential_types::credential_feature::AwsCredentialFeature;
|
271 286 | use aws_credential_types::provider::ProvideCredentials;
|
272 287 | use std::time::{Duration, SystemTime};
|
273 288 | use time::format_description::well_known::Rfc3339;
|
274 289 | use time::OffsetDateTime;
|
275 290 | use tokio::time::timeout;
|
276 291 |
|
277 - | // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
|
292 + | /// Builds a shell command that prints `json` to stdout, quoted correctly for
|
293 + | /// the shell the provider will use on this platform.
|
294 + | ///
|
295 + | /// The provider runs the command through `sh -c` on Unix and `cmd.exe /C` on
|
296 + | /// Windows, and the two disagree about quoting:
|
297 + | ///
|
298 + | /// * `sh` needs the JSON wrapped in single quotes so the double quotes inside
|
299 + | /// it survive word splitting.
|
300 + | /// * `cmd.exe` has no notion of single quotes. It would pass them through
|
301 + | /// literally, yielding output like `'{"Version":1}'`, which is not valid
|
302 + | /// JSON. Its `echo` emits the remainder of the line verbatim, so the double
|
303 + | /// quotes survive with no quoting at all.
|
304 + | ///
|
305 + | /// A runtime `cfg!` is fine here because both branches compile everywhere;
|
306 + | /// contrast with `credentials()` above, which needs `#[cfg(windows)]` because
|
307 + | /// `raw_arg` only exists on Windows.
|
308 + | fn echo_json(json: &str) -> String {
|
309 + | if cfg!(windows) {
|
310 + | format!("echo {json}")
|
311 + | } else {
|
312 + | format!("echo '{json}'")
|
313 + | }
|
314 + | }
|
315 + |
|
278 316 | #[tokio::test]
|
279 - | #[cfg_attr(windows, ignore)]
|
280 317 | async fn test_credential_process() {
|
281 - | let provider = CredentialProcessProvider::new(String::from(
|
282 - | r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "AccountId": "123456789001", "Expiration": "2022-05-02T18:36:00+00:00" }'"#,
|
318 + | let provider = CredentialProcessProvider::new(echo_json(
|
319 + | r#"{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "AccountId": "123456789001", "Expiration": "2022-05-02T18:36:00+00:00" }"#,
|
283 320 | ));
|
284 321 | let creds = provider.provide_credentials().await.expect("valid creds");
|
285 322 | assert_eq!(creds.access_key_id(), "ASIARTESTID");
|
286 323 | assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
|
287 324 | assert_eq!(creds.session_token(), Some("TESTSESSIONTOKEN"));
|
288 325 | assert_eq!(creds.account_id().unwrap().as_str(), "123456789001");
|
289 326 | assert_eq!(
|
290 327 | creds.expiry(),
|
291 328 | Some(SystemTime::from(
|
292 329 | OffsetDateTime::parse("2022-05-02T18:36:00+00:00", &Rfc3339)
|
293 330 | .expect("static datetime"),
|
294 331 | ))
|
295 332 | );
|
296 333 | }
|
297 334 |
|
298 - | // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
|
299 335 | #[tokio::test]
|
300 - | #[cfg_attr(windows, ignore)]
|
301 336 | async fn test_credential_process_no_expiry() {
|
302 - | let provider = CredentialProcessProvider::new(String::from(
|
303 - | r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
|
337 + | let provider = CredentialProcessProvider::new(echo_json(
|
338 + | r#"{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }"#,
|
304 339 | ));
|
305 340 | let creds = provider.provide_credentials().await.expect("valid creds");
|
306 341 | assert_eq!(creds.access_key_id(), "ASIARTESTID");
|
307 342 | assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
|
308 343 | assert_eq!(creds.session_token(), None);
|
309 344 | assert_eq!(creds.expiry(), None);
|
310 345 | }
|
311 346 |
|
312 347 | #[tokio::test]
|
313 348 | async fn credentials_process_timeouts() {
|
314 - | let provider = CredentialProcessProvider::new(String::from("sleep 1000"));
|
349 + | // Keep this sleep short. The 1ms timeout below fires long before it
|
350 + | // elapses, but the spawned process is not killed when the timed-out
|
351 + | // future is dropped, and on Windows the test is not reported as finished
|
352 + | // until that child exits, stalling the whole test binary for the
|
353 + | // duration. `sleep` still has to outlast the 1ms timeout by a wide
|
354 + | // margin for the assertion to hold.
|
355 + | let provider = CredentialProcessProvider::new(String::from("sleep 1"));
|
315 356 | let _creds = timeout(Duration::from_millis(1), provider.provide_credentials())
|
316 357 | .await
|
317 358 | .expect_err("timeout forced");
|
318 359 | }
|
319 360 |
|
320 361 | #[tokio::test]
|
321 362 | async fn credentials_with_fallback_account_id() {
|
322 363 | let provider = CredentialProcessProvider::builder()
|
323 - | .command(CommandWithSensitiveArgs::new(String::from(
|
324 - | r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
|
364 + | .command(CommandWithSensitiveArgs::new(echo_json(
|
365 + | r#"{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }"#,
|
325 366 | )))
|
326 367 | .account_id("012345678901")
|
327 368 | .build();
|
328 369 | let creds = provider.provide_credentials().await.unwrap();
|
329 370 | assert_eq!("012345678901", creds.account_id().unwrap().as_str());
|
330 371 | }
|
331 372 |
|
332 373 | #[tokio::test]
|
333 374 | async fn fallback_account_id_shadowed_by_account_id_in_process_output() {
|
334 375 | let provider = CredentialProcessProvider::builder()
|
335 - | .command(CommandWithSensitiveArgs::new(String::from(
|
336 - | r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }'"#,
|
376 + | .command(CommandWithSensitiveArgs::new(echo_json(
|
377 + | r#"{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }"#,
|
337 378 | )))
|
338 379 | .account_id("012345678901")
|
339 380 | .build();
|
340 381 | let creds = provider.provide_credentials().await.unwrap();
|
341 382 | assert_eq!("111122223333", creds.account_id().unwrap().as_str());
|
342 383 | }
|
343 384 |
|
344 385 | #[tokio::test]
|
345 386 | async fn credential_feature() {
|
346 387 | let provider = CredentialProcessProvider::builder()
|
347 - | .command(CommandWithSensitiveArgs::new(String::from(
|
348 - | r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }'"#,
|
388 + | .command(CommandWithSensitiveArgs::new(echo_json(
|
389 + | r#"{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }"#,
|
349 390 | )))
|
350 391 | .account_id("012345678901")
|
351 392 | .build();
|
352 393 | let creds = provider.provide_credentials().await.unwrap();
|
353 394 | assert_eq!(
|
354 395 | &vec![AwsCredentialFeature::CredentialsProcess],
|
355 396 | creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
|
356 397 | );
|
357 398 | }
|
358 399 | }
|
400 + |
|
401 + | // Integration tests that actually spawn a process from a path containing a
|
402 + | // space. These run only on Windows: they are the regression tests for the
|
403 + | // `credential_process` quoting bug (internal: P491659165). The pre-existing
|
404 + | // `credential_process` tests above use the Unix `echo` builtin and are
|
405 + | // skipped on Windows.
|
406 + | #[cfg(all(test, windows))]
|
407 + | mod windows_tests {
|
408 + | use crate::credential_process::CredentialProcessProvider;
|
409 + | use aws_credential_types::provider::ProvideCredentials;
|
410 + | use std::path::{Path, PathBuf};
|
411 + |
|
412 + | const CREDS_JSON: &str = "{\"Version\":1,\"AccessKeyId\":\"ASIARTESTID\",\"SecretAccessKey\":\"TESTSECRETKEY\",\"SessionToken\":\"TESTSESSIONTOKEN\",\"Expiration\":\"2035-01-01T00:00:00Z\"}";
|
413 + |
|
414 + | // Write a `.cmd` provider that prints valid credential JSON to stdout into
|
415 + | // `dir`, returning the path to the script. `@echo off` keeps stdout clean so
|
416 + | // the only thing emitted is the JSON document.
|
417 + | fn write_provider(dir: &Path) -> PathBuf {
|
418 + | std::fs::create_dir_all(dir).unwrap();
|
419 + | let script = dir.join("provider.cmd");
|
420 + | std::fs::write(&script, format!("@echo off\r\necho {CREDS_JSON}\r\n")).unwrap();
|
421 + | script
|
422 + | }
|
423 + |
|
424 + | #[tokio::test]
|
425 + | async fn spaced_path_with_argument_resolves() {
|
426 + | let tmp = tempfile::TempDir::new().unwrap();
|
427 + | let script = write_provider(&tmp.path().join("Program Space"));
|
428 + | assert!(
|
429 + | script.to_string_lossy().contains(' '),
|
430 + | "test fixture path must contain a space: {}",
|
431 + | script.display()
|
432 + | );
|
433 + |
|
434 + | // Quote the path as a real config would, and pass an argument — exactly
|
435 + | // the AppStream shape: `"...PhotonRoleCredentialProvider.exe" --role=Machine`.
|
436 + | let command = format!("\"{}\" --role=Machine", script.display());
|
437 + | let provider = CredentialProcessProvider::new(command);
|
438 + |
|
439 + | let creds = provider
|
440 + | .provide_credentials()
|
441 + | .await
|
442 + | .expect("credentials should resolve from a quoted spaced path with an argument");
|
443 + | assert_eq!(creds.access_key_id(), "ASIARTESTID");
|
444 + | assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
|
445 + | assert_eq!(creds.session_token(), Some("TESTSESSIONTOKEN"));
|
446 + | }
|
447 + |
|
448 + | #[tokio::test]
|
449 + | async fn spaced_path_without_argument_resolves() {
|
450 + | let tmp = tempfile::TempDir::new().unwrap();
|
451 + | let script = write_provider(&tmp.path().join("Program Space"));
|
452 + |
|
453 + | let command = format!("\"{}\"", script.display());
|
454 + | let provider = CredentialProcessProvider::new(command);
|
455 + |
|
456 + | let creds = provider
|
457 + | .provide_credentials()
|
458 + | .await
|
459 + | .expect("credentials should resolve from a quoted spaced path with no argument");
|
460 + | assert_eq!(creds.access_key_id(), "ASIARTESTID");
|
461 + | }
|
462 + |
|
463 + | #[tokio::test]
|
464 + | async fn unquoted_unspaced_path_still_resolves() {
|
465 + | // Control: an unquoted path with no spaces continues to work.
|
466 + | let tmp = tempfile::TempDir::new().unwrap();
|
467 + | let script = write_provider(&tmp.path().join("nospace"));
|
468 + | // Only meaningful if no path component (including the temp root) has a
|
469 + | // space; otherwise the unquoted form is not a valid no-space control.
|
470 + | if script.to_string_lossy().contains(' ') {
|
471 + | return;
|
472 + | }
|
473 + |
|
474 + | let command = script.display().to_string();
|
475 + | let provider = CredentialProcessProvider::new(command);
|
476 + |
|
477 + | let creds = provider
|
478 + | .provide_credentials()
|
479 + | .await
|
480 + | .expect("control (unquoted, no spaces) should resolve");
|
481 + | assert_eq!(creds.access_key_id(), "ASIARTESTID");
|
482 + | }
|
483 + | }
|