diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index d546c0b6f0..8ef2fea4c5 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -86,3 +86,9 @@ message = "Fix bug in `CredentialsProcess` provider where `expiry` was incorrect references = ["smithy-rs#3335", "aws-sdk-rust#1021"] meta = { "breaking" = false, "tada" = false, "bug" = true } author = "rcoh" + +[[aws-sdk-rust]] +message = "~/.aws/config and ~/.aws/credentials now parse keys in a case-insensitive way. This means the `AWS_SECRET_ACCESS_KEY` is supported in addition to `aws_secret_access_key`." +references = ["aws-sdk#574", "aws-sdk-rust#1020", "smithy-rs#3344"] +meta = { "breaking" = false, "bug" = true, "tada" = false } +author = "rcoh" diff --git a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs index f64986be7c..2999117c29 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs @@ -268,6 +268,7 @@ mod test { make_test!(prefer_environment); make_test!(profile_static_keys); + make_test!(profile_static_keys_case_insensitive); make_test!(web_identity_token_env); make_test!(web_identity_source_profile_no_env); make_test!(web_identity_token_invalid_jwt); diff --git a/aws/rust-runtime/aws-config/src/profile/parser.rs b/aws/rust-runtime/aws-config/src/profile/parser.rs index 6a83e908af..dfa705823e 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::profile::parser::parse::parse_profile_file; +use crate::profile::parser::parse::{parse_profile_file, to_ascii_lowercase}; use crate::profile::parser::source::Source; use crate::profile::profile_file::ProfileFiles; use aws_types::os_shim_internal::{Env, Fs}; @@ -169,7 +169,9 @@ impl Profile { /// Returns a reference to the property named `name` pub fn get(&self, name: &str) -> Option<&str> { - self.properties.get(name).map(|prop| prop.value()) + self.properties + .get(to_ascii_lowercase(name).as_ref()) + .map(|prop| prop.value()) } } diff --git a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs b/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs index 5b3b5c1ea8..36518d9ef7 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs @@ -113,9 +113,9 @@ pub(super) fn merge_in( } } -fn merge_into_base(target: &mut Profile, profile: HashMap<&str, Cow<'_, str>>) { +fn merge_into_base(target: &mut Profile, profile: HashMap, Cow<'_, str>>) { for (k, v) in profile { - match validate_identifier(k) { + match validate_identifier(k.as_ref()) { Ok(k) => { target .properties @@ -146,6 +146,7 @@ fn validate_identifier(input: &str) -> Result<&str, ()> { #[cfg(test)] mod tests { + use std::borrow::Cow; use std::collections::HashMap; use tracing_test::traced_test; @@ -218,7 +219,7 @@ mod tests { let mut profile: RawProfileSet<'_> = HashMap::new(); profile.insert("default", { let mut out = HashMap::new(); - out.insert("invalid key", "value".into()); + out.insert(Cow::Borrowed("invalid key"), "value".into()); out }); let mut base = ProfileSet::empty(); diff --git a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs index 1d3c3acb05..062c32fa34 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs @@ -19,7 +19,7 @@ use std::error::Error; use std::fmt::{self, Display, Formatter}; /// A set of profiles that still carries a reference to the underlying data -pub(super) type RawProfileSet<'a> = HashMap<&'a str, HashMap<&'a str, Cow<'a, str>>>; +pub(super) type RawProfileSet<'a> = HashMap<&'a str, HashMap, Cow<'a, str>>>; /// Characters considered to be whitespace by the spec /// @@ -98,7 +98,7 @@ enum State<'a> { Starting, ReadingProfile { profile: &'a str, - property: Option<&'a str>, + property: Option>, is_subproperty: bool, }, } @@ -152,7 +152,7 @@ impl<'a> Parser<'a> { .map_err(|err| err.into_error("property", location.clone()))?; self.state = State::ReadingProfile { profile: name, - property: Some(k), + property: Some(k.clone()), is_subproperty: v.is_empty(), }; current_profile.insert(k, v.into()); @@ -184,7 +184,7 @@ impl<'a> Parser<'a> { self.data .get_mut(*profile) .expect("profile must exist") - .get_mut(*property) + .get_mut(property.as_ref()) .expect("property must exist") } State::ReadingProfile { @@ -246,7 +246,7 @@ impl PropertyError { } /// Parse a property line into a key-value pair -fn parse_property_line(line: &str) -> Result<(&str, &str), PropertyError> { +fn parse_property_line(line: &str) -> Result<(Cow<'_, str>, &str), PropertyError> { let line = prepare_line(line, true); let (k, v) = line.split_once('=').ok_or(PropertyError::NoEquals)?; let k = k.trim_matches(WHITESPACE); @@ -254,7 +254,15 @@ fn parse_property_line(line: &str) -> Result<(&str, &str), PropertyError> { if k.is_empty() { return Err(PropertyError::NoName); } - Ok((k, v)) + Ok((to_ascii_lowercase(k), v)) +} + +pub(crate) fn to_ascii_lowercase(s: &str) -> Cow<'_, str> { + if s.bytes().any(|b| b.is_ascii_uppercase()) { + Cow::Owned(s.to_ascii_lowercase()) + } else { + Cow::Borrowed(s) + } } /// Prepare a line for parsing @@ -291,23 +299,30 @@ mod test { use crate::profile::parser::parse::{parse_property_line, PropertyError}; use crate::profile::parser::source::File; use crate::profile::profile_file::ProfileFileKind; + use std::borrow::Cow; // most test cases covered by the JSON test suite #[test] fn property_parsing() { - assert_eq!(parse_property_line("a = b"), Ok(("a", "b"))); - assert_eq!(parse_property_line("a=b"), Ok(("a", "b"))); - assert_eq!(parse_property_line("a = b "), Ok(("a", "b"))); - assert_eq!(parse_property_line(" a = b "), Ok(("a", "b"))); - assert_eq!(parse_property_line(" a = b 🐱 "), Ok(("a", "b 🐱"))); + fn ok<'a>(key: &'a str, value: &'a str) -> Result<(Cow<'a, str>, &'a str), PropertyError> { + Ok((Cow::Borrowed(key), value)) + } + + assert_eq!(parse_property_line("a = b"), ok("a", "b")); + assert_eq!(parse_property_line("a=b"), ok("a", "b")); + assert_eq!(parse_property_line("a = b "), ok("a", "b")); + assert_eq!(parse_property_line(" a = b "), ok("a", "b")); + assert_eq!(parse_property_line(" a = b 🐱 "), ok("a", "b 🐱")); assert_eq!(parse_property_line("a b"), Err(PropertyError::NoEquals)); assert_eq!(parse_property_line("= b"), Err(PropertyError::NoName)); - assert_eq!(parse_property_line("a = "), Ok(("a", ""))); + assert_eq!(parse_property_line("a = "), ok("a", "")); assert_eq!( parse_property_line("something_base64=aGVsbG8gZW50aHVzaWFzdGljIHJlYWRlcg=="), - Ok(("something_base64", "aGVsbG8gZW50aHVzaWFzdGljIHJlYWRlcg==")) + ok("something_base64", "aGVsbG8gZW50aHVzaWFzdGljIHJlYWRlcg==") ); + + assert_eq!(parse_property_line("ABc = DEF"), ok("abc", "DEF")); } #[test] diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/env.json new file mode 100644 index 0000000000..55fcfbeb05 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/env.json @@ -0,0 +1,3 @@ +{ + "HOME": "/home" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/fs/home/.aws/config new file mode 100644 index 0000000000..a8c11c6e8c --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/fs/home/.aws/config @@ -0,0 +1,2 @@ +[default] +region = us-east-1 diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/fs/home/.aws/credentials new file mode 100644 index 0000000000..bac520774d --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/fs/home/.aws/credentials @@ -0,0 +1,3 @@ +[default] +AWS_ACCESS_KEY_ID = correct_key +AWS_SECRET_ACCESS_KEY = correct_secret diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/http-traffic.json new file mode 100644 index 0000000000..489a35c611 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/http-traffic.json @@ -0,0 +1,5 @@ +{ + "events": [], + "docs": "test case uses static creds, no network requests", + "version": "V0" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/test-case.json new file mode 100644 index 0000000000..c56a02bac6 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys_case_insensitive/test-case.json @@ -0,0 +1,10 @@ +{ + "name": "profile_static_keys", + "docs": "load static keys from a profile", + "result": { + "Ok": { + "access_key_id": "correct_key", + "secret_access_key": "correct_secret" + } + } +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json b/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json index 70ce8a046e..6f6f2a8a70 100644 --- a/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json +++ b/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json @@ -452,6 +452,19 @@ } } }, + { + "name": "Duplicate properties in duplicate profiles use the last one defined (case insensitive).", + "input": { + "configFile": "[profile foo]\nName = value\n[profile foo]\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + }, { "name": "Default profile with profile prefix overrides default profile without prefix when profile prefix is first.", "input": { @@ -518,7 +531,7 @@ "output": { "profiles": { "foo": { - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_": "value" + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789-_": "value" } } } diff --git a/tools/ci-build/smithy-rs-tool-common/src/changelog.rs b/tools/ci-build/smithy-rs-tool-common/src/changelog.rs index 6cc10e5258..f68325d463 100644 --- a/tools/ci-build/smithy-rs-tool-common/src/changelog.rs +++ b/tools/ci-build/smithy-rs-tool-common/src/changelog.rs @@ -108,7 +108,7 @@ impl FromStr for Reference { ), Some((repo, number)) => { let number = number.parse::()?; - if !matches!(repo, "smithy-rs" | "aws-sdk-rust") { + if !matches!(repo, "smithy-rs" | "aws-sdk-rust" | "aws-sdk") { bail!("unexpected repo: {}", repo); } Ok(Reference {