AWS SDK

AWS SDK

rev. 5fb07742e6b2f1d99e19ffd831fff2001331355d

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-config/Cargo.toml

@@ -163,163 +0,194 @@
  183    183   
version = "0.3.16"
  184    184   
features = ["fmt", "json"]
  185    185   
  186    186   
[dev-dependencies.tokio]
  187    187   
version = "1.23.1"
  188    188   
features = ["full", "test-util"]
  189    189   
  190    190   
[dev-dependencies.serde]
  191    191   
version = "1"
  192    192   
features = ["derive"]
         193  +
[target."cfg(windows)".dev-dependencies]
         194  +
tempfile = "3.16.0"

tmp-codegen-diff/aws-sdk/sdk/aws-config/src/credential_process.rs

@@ -58,58 +122,137 @@
   78     78   
    }
   79     79   
   80     80   
    pub(crate) fn builder() -> Builder {
   81     81   
        Builder::default()
   82     82   
    }
   83     83   
   84     84   
    async fn credentials(&self) -> provider::Result {
   85     85   
        // Security: command arguments must be redacted at debug level
   86     86   
        tracing::debug!(command = %self.command, "loading credentials from external process");
   87     87   
   88         -
        let command = if cfg!(windows) {
          88  +
        // On Windows, the command runs through `cmd.exe /C`. The command string
          89  +
        // is appended with `raw_arg` rather than as a normal argument so that
          90  +
        // Rust does not apply its own C runtime style escaping (which `cmd.exe`
          91  +
        // does not understand), and the whole command is wrapped in an extra
          92  +
        // pair of quotes as `cmd.exe` requires. This preserves a quoted first
          93  +
        // token containing spaces. Ex: for an executable installed under
          94  +
        // `C:\Program Files\...`, such as AppStream 2.0's machine-role provider.
          95  +
        // Previously the entire string was passed as a single normal argument,
          96  +
        // whose escaping combined with `cmd.exe`'s quote-stripping to mangle
          97  +
        // such paths.
          98  +
        #[cfg(windows)]
          99  +
        let command = {
         100  +
            use std::os::windows::process::CommandExt;
   89    101   
            let mut command = Command::new("cmd.exe");
   90         -
            command.args(["/C", self.command.unredacted()]);
         102  +
            command.arg("/C");
         103  +
            command.raw_arg(format!("\"{}\"", self.command.unredacted()));
   91    104   
            command
   92         -
        } else {
         105  +
        };
         106  +
        #[cfg(not(windows))]
         107  +
        let command = {
   93    108   
            let mut command = Command::new("sh");
   94    109   
            command.args(["-c", self.command.unredacted()]);
   95    110   
            command
   96    111   
        };
   97    112   
        let output = tokio::process::Command::from(command)
   98    113   
            .output()
   99    114   
            .await
  100    115   
            .map_err(|e| {
  101    116   
                CredentialsError::provider_error(format!(
  102    117   
                    "Error retrieving credentials from external process: {e}",
@@ -247,262 +0,483 @@
  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  +
}