diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 03b9741050..1c5a362d1d 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -16,3 +16,9 @@ message = "The types in the aws-http crate were moved into aws-runtime. Deprecat references = ["smithy-rs#3355"] meta = { "breaking" = false, "tada" = false, "bug" = false } author = "jdisanti" + +[[aws-sdk-rust]] +message = "Add support for `[sso-session]` in AWS config file for AWS Identity Center SSO credentials. Note that this does not include support for AWS Builder ID SSO sessions for services such as Code Catalyst (these lack the `sso_account_id` and `sso_role_name` fields in the profile config). Support for AWS Builder IDs is still being tracked in https://github.com/awslabs/aws-sdk-rust/issues/703." +references = ["aws-sdk-rust#703", "smithy-rs#3379"] +meta = { "breaking" = false, "tada" = true, "bug" = false } +author = "jdisanti" diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index e050f2d962..c492011007 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -265,6 +265,29 @@ pub enum ProfileFileError { /// Additional information about the missing feature message: Option>, }, + + /// Missing sso-session section in config + #[non_exhaustive] + MissingSsoSession { + /// The name of the profile that specified `sso_session` + profile: String, + /// SSO session name + sso_session: String, + }, + + /// Invalid SSO configuration + #[non_exhaustive] + InvalidSsoConfig { + /// The name of the profile that the error originates in + profile: String, + /// Error message + message: Cow<'static, str>, + }, + + /// Profile is intended to be used in the token provider chain rather + /// than in the credentials chain. + #[non_exhaustive] + TokenProviderConfig {}, } impl ProfileFileError { @@ -324,6 +347,25 @@ impl Display for ProfileFileError { "This behavior requires following cargo feature(s) enabled: {feature}. {message}", ) } + ProfileFileError::MissingSsoSession { + profile, + sso_session, + } => { + write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found") + } + ProfileFileError::InvalidSsoConfig { profile, message } => { + write!(f, "profile `{profile}` has invalid SSO config: {message}") + } + ProfileFileError::TokenProviderConfig { .. } => { + // TODO(https://github.com/awslabs/aws-sdk-rust/issues/703): Update error message once token support is added + write!( + f, + "selected profile will resolve an access token instead of credentials \ + since it doesn't have `sso_account_id` and `sso_role_name` set. Access token \ + support for services such as Code Catalyst hasn't been implemented yet and is \ + being tracked in https://github.com/awslabs/aws-sdk-rust/issues/703" + ) + } } } } @@ -497,4 +539,8 @@ mod test { make_test!(credential_process_failure); #[cfg(feature = "credentials-process")] make_test!(credential_process_invalid); + #[cfg(feature = "sso")] + make_test!(sso_credentials); + #[cfg(feature = "sso")] + make_test!(sso_token); } diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs index 6a84c6d7d3..10ba42d5b0 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs @@ -130,19 +130,24 @@ impl ProviderChain { sso_region, sso_role_name, sso_start_url, + sso_session_name, } => { #[cfg(feature = "sso")] { use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider}; use aws_types::region::Region; + let (Some(sso_account_id), Some(sso_role_name)) = + (sso_account_id, sso_role_name) + else { + return Err(ProfileFileError::TokenProviderConfig {}); + }; let sso_config = SsoProviderConfig { account_id: sso_account_id.to_string(), role_name: sso_role_name.to_string(), start_url: sso_start_url.to_string(), region: Region::new(sso_region.to_string()), - // TODO(https://github.com/awslabs/aws-sdk-rust/issues/703): Implement sso_session_name profile property - session_name: None, + session_name: sso_session_name.map(|s| s.to_string()), }; Arc::new(SsoCredentialsProvider::new(provider_config, sso_config)) } diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs index 4bd044b3d3..d0a0906219 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs @@ -78,10 +78,13 @@ pub(super) enum BaseProvider<'a> { /// An SSO Provider Sso { - sso_account_id: &'a str, + sso_session_name: Option<&'a str>, sso_region: &'a str, - sso_role_name: &'a str, sso_start_url: &'a str, + + // Credentials from SSO fields + sso_account_id: Option<&'a str>, + sso_role_name: Option<&'a str>, }, /// A profile that specifies a `credential_process` @@ -172,7 +175,7 @@ pub(super) fn resolve_chain( chain.push(role_provider); next } else { - break base_provider(profile).map_err(|err| { + break base_provider(profile_set, profile).map_err(|err| { // It's possible for base_provider to return a `ProfileFileError::ProfileDidNotContainCredentials` // if we're still looking at the first provider we want to surface it. However, // if we're looking at any provider after the first we want to instead return a `ProfileFileError::InvalidCredentialSource` @@ -193,7 +196,7 @@ pub(super) fn resolve_chain( // self referential profile, don't go through the loop because it will error // on the infinite loop check. Instead, reload this profile as a base profile // and exit. - break base_provider(profile)?; + break base_provider(profile_set, profile)?; } NextProfile::Named(name) => source_profile_name = name, } @@ -216,6 +219,7 @@ mod sso { pub(super) const REGION: &str = "sso_region"; pub(super) const ROLE_NAME: &str = "sso_role_name"; pub(super) const START_URL: &str = "sso_start_url"; + pub(super) const SESSION_NAME: &str = "sso_session"; } mod web_identity_token { @@ -234,12 +238,15 @@ mod credential_process { const PROVIDER_NAME: &str = "ProfileFile"; -fn base_provider(profile: &Profile) -> Result, ProfileFileError> { +fn base_provider<'a>( + profile_set: &'a ProfileSet, + profile: &'a Profile, +) -> Result, ProfileFileError> { // the profile must define either a `CredentialsSource` or a concrete set of access keys match profile.get(role::CREDENTIAL_SOURCE) { Some(source) => Ok(BaseProvider::NamedSource(source)), None => web_identity_token_from_profile(profile) - .or_else(|| sso_from_profile(profile)) + .or_else(|| sso_from_profile(profile_set, profile).transpose()) .or_else(|| credential_process_from_profile(profile)) .unwrap_or_else(|| Ok(BaseProvider::AccessKey(static_creds_from_profile(profile)?))), } @@ -292,39 +299,93 @@ fn role_arn_from_profile(profile: &Profile) -> Option> { }) } -fn sso_from_profile(profile: &Profile) -> Option, ProfileFileError>> { +fn sso_from_profile<'a>( + profile_set: &'a ProfileSet, + profile: &'a Profile, +) -> Result>, ProfileFileError> { /* - Sample: + -- Sample without sso-session: -- + [profile sample-profile] sso_account_id = 012345678901 sso_region = us-east-1 sso_role_name = SampleRole sso_start_url = https://d-abc123.awsapps.com/start-beta + + -- Sample with sso-session: -- + + [profile sample-profile] + sso_session = dev + sso_account_id = 012345678901 + sso_role_name = SampleRole + + [sso-session dev] + sso_region = us-east-1 + sso_start_url = https://d-abc123.awsapps.com/start-beta */ - let account_id = profile.get(sso::ACCOUNT_ID); - let region = profile.get(sso::REGION); - let role_name = profile.get(sso::ROLE_NAME); - let start_url = profile.get(sso::START_URL); - if [account_id, region, role_name, start_url] - .iter() - .all(|field| field.is_none()) + let sso_account_id = profile.get(sso::ACCOUNT_ID); + let mut sso_region = profile.get(sso::REGION); + let sso_role_name = profile.get(sso::ROLE_NAME); + let mut sso_start_url = profile.get(sso::START_URL); + let sso_session_name = profile.get(sso::SESSION_NAME); + if [ + sso_account_id, + sso_region, + sso_role_name, + sso_start_url, + sso_session_name, + ] + .iter() + .all(Option::is_none) { - return None; + return Ok(None); } - let missing_field = |s| move || ProfileFileError::missing_field(profile, s); - let parse_profile = || { - let sso_account_id = account_id.ok_or_else(missing_field(sso::ACCOUNT_ID))?; - let sso_region = region.ok_or_else(missing_field(sso::REGION))?; - let sso_role_name = role_name.ok_or_else(missing_field(sso::ROLE_NAME))?; - let sso_start_url = start_url.ok_or_else(missing_field(sso::START_URL))?; - Ok(BaseProvider::Sso { - sso_account_id, - sso_region, - sso_role_name, - sso_start_url, - }) + + let invalid_sso_config = |s: &str| ProfileFileError::InvalidSsoConfig { + profile: profile.name().into(), + message: format!( + "`{s}` can only be specified in the [sso-session] config when a session name is given" + ) + .into(), + }; + if let Some(sso_session_name) = sso_session_name { + if sso_start_url.is_some() { + return Err(invalid_sso_config(sso::START_URL)); + } + if sso_region.is_some() { + return Err(invalid_sso_config(sso::REGION)); + } + if let Some(session) = profile_set.sso_session(sso_session_name) { + sso_start_url = session.get(sso::START_URL); + sso_region = session.get(sso::REGION); + } else { + return Err(ProfileFileError::MissingSsoSession { + profile: profile.name().into(), + sso_session: sso_session_name.into(), + }); + } + } + + let invalid_sso_creds = |left: &str, right: &str| ProfileFileError::InvalidSsoConfig { + profile: profile.name().into(), + message: format!("if `{left}` is set, then `{right}` must also be set").into(), }; - Some(parse_profile()) + match (sso_account_id, sso_role_name) { + (Some(_), Some(_)) | (None, None) => { /* good */ } + (Some(_), None) => return Err(invalid_sso_creds(sso::ACCOUNT_ID, sso::ROLE_NAME)), + (None, Some(_)) => return Err(invalid_sso_creds(sso::ROLE_NAME, sso::ACCOUNT_ID)), + } + + let missing_field = |s| move || ProfileFileError::missing_field(profile, s); + let sso_region = sso_region.ok_or_else(missing_field(sso::REGION))?; + let sso_start_url = sso_start_url.ok_or_else(missing_field(sso::START_URL))?; + Ok(Some(BaseProvider::Sso { + sso_account_id, + sso_region, + sso_role_name, + sso_start_url, + sso_session_name, + })) } fn web_identity_token_from_profile( @@ -429,7 +490,11 @@ mod tests { } fn check(test_case: TestCase) { - let source = ProfileSet::new(test_case.input.profile, test_case.input.selected_profile); + let source = ProfileSet::new( + test_case.input.profiles, + test_case.input.selected_profile, + test_case.input.sso_sessions, + ); let actual = resolve_chain(&source); let expected = test_case.output; match (expected, actual) { @@ -458,8 +523,10 @@ mod tests { #[derive(Deserialize)] struct TestInput { - profile: HashMap>, + profiles: HashMap>, selected_profile: String, + #[serde(default)] + sso_sessions: HashMap>, } fn to_test_output(profile_chain: ProfileChain<'_>) -> Vec { @@ -484,15 +551,17 @@ mod tests { role_session_name: session_name.map(|sess| sess.to_string()), }), BaseProvider::Sso { - sso_account_id, sso_region, - sso_role_name, sso_start_url, + sso_session_name, + sso_account_id, + sso_role_name, } => output.push(Provider::Sso { - sso_account_id: sso_account_id.into(), sso_region: sso_region.into(), - sso_role_name: sso_role_name.into(), sso_start_url: sso_start_url.into(), + sso_session: sso_session_name.map(|s| s.to_string()), + sso_account_id: sso_account_id.map(|s| s.to_string()), + sso_role_name: sso_role_name.map(|s| s.to_string()), }), }; for role in profile_chain.chain { @@ -531,10 +600,12 @@ mod tests { role_session_name: Option, }, Sso { - sso_account_id: String, sso_region: String, - sso_role_name: String, sso_start_url: String, + sso_session: Option, + + sso_account_id: Option, + sso_role_name: Option, }, } diff --git a/aws/rust-runtime/aws-config/src/profile/parser.rs b/aws/rust-runtime/aws-config/src/profile/parser.rs index dfa705823e..a29f282c1f 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser.rs @@ -74,6 +74,7 @@ pub async fn load( pub struct ProfileSet { profiles: HashMap, selected_profile: Cow<'static, str>, + sso_sessions: HashMap, } impl ProfileSet { @@ -84,6 +85,7 @@ impl ProfileSet { pub(crate) fn new( profiles: HashMap>, selected_profile: impl Into>, + sso_sessions: HashMap>, ) -> Self { let mut base = ProfileSet::empty(); base.selected_profile = selected_profile.into(); @@ -99,6 +101,18 @@ impl ProfileSet { ), ); } + for (name, session) in sso_sessions { + base.sso_sessions.insert( + name.clone(), + SsoSession::new( + name, + session + .into_iter() + .map(|(k, v)| (k.clone(), Property::new(k, v))) + .collect(), + ), + ); + } base } @@ -124,11 +138,21 @@ impl ProfileSet { self.profiles.is_empty() } - /// Returns the names of the profiles in this profile set + /// Returns the names of the profiles in this config pub fn profiles(&self) -> impl Iterator { self.profiles.keys().map(String::as_ref) } + /// Returns the names of the SSO sessions in this config + pub fn sso_sessions(&self) -> impl Iterator { + self.sso_sessions.keys().map(String::as_ref) + } + + /// Retrieves a named SSO session from the config + pub(crate) fn sso_session(&self, name: &str) -> Option<&SsoSession> { + self.sso_sessions.get(name) + } + fn parse(source: Source) -> Result { let mut base = ProfileSet::empty(); base.selected_profile = source.profile; @@ -143,35 +167,146 @@ impl ProfileSet { Self { profiles: Default::default(), selected_profile: "default".into(), + sso_sessions: Default::default(), } } } -/// An individual configuration profile -/// -/// An AWS config may be composed of a multiple named profiles within a [`ProfileSet`]. +/// Represents a top-level section (e.g., `[profile name]`) in a config file. +pub(crate) trait Section { + /// The name of this section + fn name(&self) -> &str; + + /// Returns all the properties in this section + fn properties(&self) -> &HashMap; + + /// Returns a reference to the property named `name` + fn get(&self, name: &str) -> Option<&str>; + + /// True if there are no properties in this section. + fn is_empty(&self) -> bool; + + /// Insert a property into a section + fn insert(&mut self, name: String, value: Property); +} + #[derive(Debug, Clone, Eq, PartialEq)] -pub struct Profile { +struct SectionInner { name: String, properties: HashMap, } +impl Section for SectionInner { + fn name(&self) -> &str { + &self.name + } + + fn properties(&self) -> &HashMap { + &self.properties + } + + fn get(&self, name: &str) -> Option<&str> { + self.properties + .get(to_ascii_lowercase(name).as_ref()) + .map(|prop| prop.value()) + } + + fn is_empty(&self) -> bool { + self.properties.is_empty() + } + + fn insert(&mut self, name: String, value: Property) { + self.properties + .insert(to_ascii_lowercase(&name).into(), value); + } +} + +/// An individual configuration profile +/// +/// An AWS config may be composed of a multiple named profiles within a [`ProfileSet`]. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Profile(SectionInner); + impl Profile { /// Create a new profile - pub fn new(name: String, properties: HashMap) -> Self { - Self { name, properties } + pub fn new(name: impl Into, properties: HashMap) -> Self { + Self(SectionInner { + name: name.into(), + properties, + }) } /// The name of this profile pub fn name(&self) -> &str { - &self.name + self.0.name() } /// Returns a reference to the property named `name` pub fn get(&self, name: &str) -> Option<&str> { - self.properties - .get(to_ascii_lowercase(name).as_ref()) - .map(|prop| prop.value()) + self.0.get(name) + } +} + +impl Section for Profile { + fn name(&self) -> &str { + self.0.name() + } + + fn properties(&self) -> &HashMap { + self.0.properties() + } + + fn get(&self, name: &str) -> Option<&str> { + self.0.get(name) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn insert(&mut self, name: String, value: Property) { + self.0.insert(name, value) + } +} + +/// A `[sso-session name]` section in the config. +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct SsoSession(SectionInner); + +impl SsoSession { + /// Create a new SSO session section. + pub(crate) fn new(name: impl Into, properties: HashMap) -> Self { + Self(SectionInner { + name: name.into(), + properties, + }) + } + + /// Returns a reference to the property named `name` + pub(crate) fn get(&self, name: &str) -> Option<&str> { + self.0.get(name) + } +} + +impl Section for SsoSession { + fn name(&self) -> &str { + self.0.name() + } + + fn properties(&self) -> &HashMap { + self.0.properties() + } + + fn get(&self, name: &str) -> Option<&str> { + self.0.get(name) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn insert(&mut self, name: String, value: Property) { + self.0.insert(name, value) } } @@ -248,7 +383,10 @@ pub struct CouldNotReadProfileFile { #[cfg(test)] mod test { - use crate::profile::parser::source::{File, Source}; + use crate::profile::parser::{ + source::{File, Source}, + Section, + }; use crate::profile::profile_file::ProfileFileKind; use crate::profile::ProfileSet; use arbitrary::{Arbitrary, Unstructured}; @@ -333,17 +471,28 @@ mod test { } // for test comparison purposes, flatten a profile into a hashmap - fn flatten(profile: ProfileSet) -> HashMap> { - profile - .profiles - .into_values() - .map(|profile| { + #[derive(Debug)] + struct FlattenedProfileSet { + profiles: HashMap>, + sso_sessions: HashMap>, + } + fn flatten(config: ProfileSet) -> FlattenedProfileSet { + FlattenedProfileSet { + profiles: flatten_sections(config.profiles.values().map(|p| p as _)), + sso_sessions: flatten_sections(config.sso_sessions.values().map(|s| s as _)), + } + } + fn flatten_sections<'a>( + sections: impl Iterator, + ) -> HashMap> { + sections + .map(|section| { ( - profile.name, - profile - .properties - .into_values() - .map(|prop| (prop.key, prop.value)) + section.name().to_string(), + section + .properties() + .values() + .map(|prop| (prop.key.clone(), prop.value.clone())) .collect(), ) }) @@ -373,11 +522,28 @@ mod test { let copy = test_case.clone(); let parsed = ProfileSet::parse(make_source(test_case.input)); let res = match (parsed.map(flatten), &test_case.output) { - (Ok(actual), ParserOutput::Profiles(expected)) if &actual != expected => Err(format!( - "mismatch:\nExpected: {:#?}\nActual: {:#?}", - expected, actual - )), - (Ok(_), ParserOutput::Profiles(_)) => Ok(()), + ( + Ok(FlattenedProfileSet { + profiles: actual_profiles, + sso_sessions: actual_sso_sessions, + }), + ParserOutput::Config { + profiles, + sso_sessions, + }, + ) => { + if profiles != &actual_profiles { + Err(format!( + "mismatched profiles:\nExpected: {profiles:#?}\nActual: {actual_profiles:#?}", + )) + } else if sso_sessions != &actual_sso_sessions { + Err(format!( + "mismatched sso_sessions:\nExpected: {sso_sessions:#?}\nActual: {actual_sso_sessions:#?}", + )) + } else { + Ok(()) + } + } (Err(msg), ParserOutput::ErrorContaining(substr)) => { if format!("{}", msg).contains(substr) { Ok(()) @@ -386,10 +552,9 @@ mod test { } } (Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!( - "expected an error: {} but parse succeeded:\n{:#?}", - err, output + "expected an error: {err} but parse succeeded:\n{output:#?}", )), - (Err(err), ParserOutput::Profiles(_expected)) => { + (Err(err), ParserOutput::Config { .. }) => { Err(format!("Expected to succeed but got: {}", err)) } }; @@ -417,7 +582,11 @@ mod test { #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] enum ParserOutput { - Profiles(HashMap>), + Config { + profiles: HashMap>, + #[serde(default)] + sso_sessions: HashMap>, + }, ErrorContaining(String), } 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 36518d9ef7..4ee0d69dae 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs @@ -3,7 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::profile::parser::parse::{RawProfileSet, WHITESPACE}; +use crate::profile::parser::{ + parse::{RawProfileSet, WHITESPACE}, + Section, SsoSession, +}; use crate::profile::profile_file::ProfileFileKind; use crate::profile::{Profile, ProfileSet, Property}; use std::borrow::Cow; @@ -11,51 +14,92 @@ use std::collections::HashMap; const DEFAULT: &str = "default"; const PROFILE_PREFIX: &str = "profile"; +const SSO_SESSION_PREFIX: &str = "sso-session"; #[derive(Eq, PartialEq, Hash, Debug)] -struct ProfileName<'a> { - name: &'a str, - has_profile_prefix: bool, +enum SectionKey<'a> { + /// `[default]` or `[profile default]` + Default { + /// True when it is `[profile default]` + prefixed: bool, + }, + /// `[profile name]` or `[name]` + Profile { + name: Cow<'a, str>, + /// True if prefixed with `profile`. + prefixed: bool, + }, + /// `[sso-session name]` + SsoSession { name: Cow<'a, str> }, } -impl ProfileName<'_> { - fn parse(input: &str) -> ProfileName<'_> { +impl<'a> SectionKey<'a> { + fn parse(input: &str) -> SectionKey<'_> { let input = input.trim_matches(WHITESPACE); - let (name, has_profile_prefix) = match input.strip_prefix(PROFILE_PREFIX) { - // profilefoo isn't considered as having the profile prefix - Some(stripped) if stripped.starts_with(WHITESPACE) => (stripped.trim(), true), - _ => (input, false), - }; - ProfileName { - name, - has_profile_prefix, + if input == DEFAULT { + return SectionKey::Default { prefixed: false }; + } else if let Some((prefix, suffix)) = input.split_once(WHITESPACE) { + let suffix = suffix.trim(); + if prefix == PROFILE_PREFIX { + if suffix == "default" { + return SectionKey::Default { prefixed: true }; + } else { + return SectionKey::Profile { + name: suffix.into(), + prefixed: true, + }; + } + } else if prefix == SSO_SESSION_PREFIX { + return SectionKey::SsoSession { + name: suffix.into(), + }; + } + } + + SectionKey::Profile { + name: input.into(), + prefixed: false, } } - /// Validate a ProfileName for a given file key + /// Validate a SectionKey for a given file key /// /// 1. `name` must ALWAYS be a valid identifier /// 2. For Config files, the profile must either be `default` or it must have a profile prefix /// 3. For credentials files, the profile name MUST NOT have a profile prefix + /// 4. Only config files can have sso-session sections fn valid_for(self, kind: ProfileFileKind) -> Result { - if validate_identifier(self.name).is_err() { - return Err(format!( - "profile `{}` ignored because `{}` was not a valid identifier", - &self.name, &self.name - )); - } - match (self.name, kind, self.has_profile_prefix) { - (_, ProfileFileKind::Config, true) => Ok(self), - (DEFAULT, ProfileFileKind::Config, false) => Ok(self), - (_not_default, ProfileFileKind::Config, false) => Err(format!( - "profile `{}` ignored because config profiles must be of the form `[profile ]`", - self.name - )), - (_, ProfileFileKind::Credentials, true) => Err(format!( - "profile `{}` ignored because credential profiles must NOT begin with `profile`", - self.name - )), - (_, ProfileFileKind::Credentials, false) => Ok(self), + match &self { + SectionKey::Default { .. } => Ok(self), + SectionKey::Profile { name, prefixed } => { + if validate_identifier(name).is_err() { + return Err(format!( + "profile `{}` ignored because `{}` was not a valid identifier", + name, name + )); + } + match (kind, prefixed) { + (ProfileFileKind::Config, false) => Err(format!("profile `{}` ignored because config profiles must be of the form `[profile ]`", name)), + (ProfileFileKind::Credentials, true) => Err(format!("profile `{}` ignored because credential profiles must NOT begin with `profile`", name)), + _ => Ok(self) + } + } + SectionKey::SsoSession { name } => { + if validate_identifier(name).is_err() { + return Err(format!( + "sso-session `{}` ignored because `{}` was not a valid identifier", + name, name + )); + } + if let ProfileFileKind::Config = kind { + Ok(self) + } else { + Err(format!( + "sso-session `{}` ignored sso-sessions must be in the AWS config file rather than the credentials file", + name + )) + } + } } } } @@ -72,17 +116,17 @@ pub(super) fn merge_in( raw_profile_set: RawProfileSet<'_>, kind: ProfileFileKind, ) { - // parse / validate profile names - let validated_profiles = raw_profile_set + // parse / validate sections + let validated_sections = raw_profile_set .into_iter() - .map(|(name, profile)| (ProfileName::parse(name).valid_for(kind), profile)); + .map(|(name, properties)| (SectionKey::parse(name).valid_for(kind), properties)); // remove invalid profiles & emit warning - // valid_profiles contains only valid profiles but it may contain `[profile default]` and `[default]` + // valid_sections contains only valid profiles but it may contain `[profile default]` and `[default]` // which must be filtered later - let valid_profiles = validated_profiles - .filter_map(|(name, profile)| match name { - Ok(profile_name) => Some((profile_name, profile)), + let valid_sections = validated_sections + .filter_map(|(name, properties)| match name { + Ok(section_key) => Some((section_key, properties)), Err(err_str) => { tracing::warn!("{}", err_str); None @@ -90,39 +134,46 @@ pub(super) fn merge_in( }) .collect::>(); // if a `[profile default]` exists then we should ignore `[default]` - let ignore_unprefixed_default = valid_profiles + let ignore_unprefixed_default = valid_sections .iter() - .any(|(profile, _)| profile.name == DEFAULT && profile.has_profile_prefix); + .any(|(section_key, _)| matches!(section_key, SectionKey::Default { prefixed: true })); - for (profile_name, raw_profile) in valid_profiles { + for (section_key, raw_profile) in valid_sections { // When normalizing profiles, profiles should be merged. However, `[profile default]` and // `[default]` are considered two separate profiles. Furthermore, `[profile default]` fully // replaces any contents of `[default]`! if ignore_unprefixed_default - && profile_name.name == DEFAULT - && !profile_name.has_profile_prefix + && matches!(section_key, SectionKey::Default { prefixed: false }) { tracing::warn!("profile `default` ignored because `[profile default]` was found which takes priority"); continue; } - let profile = base - .profiles - .entry(profile_name.name.to_string()) - .or_insert_with(|| Profile::new(profile_name.name.to_string(), Default::default())); - merge_into_base(profile, raw_profile) + let section: &mut dyn Section = match section_key { + SectionKey::Default { .. } => base + .profiles + .entry("default".to_string()) + .or_insert_with(|| Profile::new("default", Default::default())), + SectionKey::Profile { name, .. } => base + .profiles + .entry(name.to_string()) + .or_insert_with(|| Profile::new(name, Default::default())), + SectionKey::SsoSession { name } => base + .sso_sessions + .entry(name.to_string()) + .or_insert_with(|| SsoSession::new(name, Default::default())), + }; + merge_into_base(section, raw_profile) } } -fn merge_into_base(target: &mut Profile, profile: HashMap, Cow<'_, str>>) { +fn merge_into_base(target: &mut dyn Section, profile: HashMap, Cow<'_, str>>) { for (k, v) in profile { match validate_identifier(k.as_ref()) { Ok(k) => { - target - .properties - .insert(k.to_owned(), Property::new(k.to_owned(), v.into())); + target.insert(k.to_owned(), Property::new(k.to_owned(), v.into())); } Err(_) => { - tracing::warn!(profile = %&target.name, key = ?k, "key ignored because `{}` was not a valid identifier", k); + tracing::warn!(profile = %target.name(), key = ?k, "key ignored because `{}` was not a valid identifier", k); } } } @@ -146,61 +197,98 @@ fn validate_identifier(input: &str) -> Result<&str, ()> { #[cfg(test)] mod tests { + use super::*; + use crate::profile::parser::{normalize::validate_identifier, Section}; + use crate::profile::parser::{normalize::SectionKey, parse::RawProfileSet}; + use crate::profile::profile_file::ProfileFileKind; + use crate::profile::ProfileSet; use std::borrow::Cow; use std::collections::HashMap; - use tracing_test::traced_test; - use crate::profile::parser::parse::RawProfileSet; - use crate::profile::ProfileSet; - - use super::{merge_in, ProfileName}; - use crate::profile::parser::normalize::validate_identifier; - use crate::profile::profile_file::ProfileFileKind; - #[test] - fn profile_name_parsing() { + fn section_key_parsing() { assert_eq!( - ProfileName::parse("profile name"), - ProfileName { - name: "name", - has_profile_prefix: true - } + SectionKey::Default { prefixed: false }, + SectionKey::parse("default"), ); assert_eq!( - ProfileName::parse("name"), - ProfileName { - name: "name", - has_profile_prefix: false - } + SectionKey::Default { prefixed: false }, + SectionKey::parse(" default "), ); assert_eq!( - ProfileName::parse("profile\tname"), - ProfileName { - name: "name", - has_profile_prefix: true - } + SectionKey::Default { prefixed: true }, + SectionKey::parse("profile default"), ); assert_eq!( - ProfileName::parse("profile name "), - ProfileName { - name: "name", - has_profile_prefix: true - } + SectionKey::Default { prefixed: true }, + SectionKey::parse(" profile default "), ); + assert_eq!( - ProfileName::parse("profilename"), - ProfileName { - name: "profilename", - has_profile_prefix: false - } + SectionKey::Profile { + name: "name".into(), + prefixed: true + }, + SectionKey::parse("profile name"), ); assert_eq!( - ProfileName::parse(" whitespace "), - ProfileName { - name: "whitespace", - has_profile_prefix: false - } + SectionKey::Profile { + name: "name".into(), + prefixed: false + }, + SectionKey::parse("name"), + ); + assert_eq!( + SectionKey::Profile { + name: "name".into(), + prefixed: true + }, + SectionKey::parse("profile\tname"), + ); + assert_eq!( + SectionKey::Profile { + name: "name".into(), + prefixed: true + }, + SectionKey::parse("profile name "), + ); + assert_eq!( + SectionKey::Profile { + name: "profilename".into(), + prefixed: false + }, + SectionKey::parse("profilename"), + ); + assert_eq!( + SectionKey::Profile { + name: "whitespace".into(), + prefixed: false + }, + SectionKey::parse(" whitespace "), + ); + + assert_eq!( + SectionKey::SsoSession { name: "foo".into() }, + SectionKey::parse("sso-session foo"), + ); + assert_eq!( + SectionKey::SsoSession { name: "foo".into() }, + SectionKey::parse("sso-session\tfoo "), + ); + assert_eq!( + SectionKey::Profile { + name: "sso-sessionfoo".into(), + prefixed: false + }, + SectionKey::parse("sso-sessionfoo"), + ); + assert_eq!( + SectionKey::Profile { + name: "sso-session".into(), + prefixed: false + }, + SectionKey::parse("sso-session "), ); } @@ -227,7 +315,6 @@ mod tests { assert!(base .get_profile("default") .expect("contains default profile") - .properties .is_empty()); assert!(logs_contain( "key ignored because `invalid key` was not a valid identifier" diff --git a/aws/rust-runtime/aws-config/src/sso/credentials.rs b/aws/rust-runtime/aws-config/src/sso/credentials.rs index fbf7ff4553..dee3ffa9ea 100644 --- a/aws/rust-runtime/aws-config/src/sso/credentials.rs +++ b/aws/rust-runtime/aws-config/src/sso/credentials.rs @@ -63,7 +63,7 @@ impl SsoCredentialsProvider { .start_url(&sso_provider_config.start_url) .session_name(session_name) .region(sso_provider_config.region.clone()) - .build_sync(), + .build_with(env.clone(), fs.clone()), ) } else { None diff --git a/aws/rust-runtime/aws-config/src/sso/token.rs b/aws/rust-runtime/aws-config/src/sso/token.rs index a805f3f6b9..2ff6f027b6 100644 --- a/aws/rust-runtime/aws-config/src/sso/token.rs +++ b/aws/rust-runtime/aws-config/src/sso/token.rs @@ -38,7 +38,7 @@ const MIN_TIME_BETWEEN_REFRESH: Duration = Duration::from_secs(30); /// SSO Token Provider /// /// This token provider will use cached SSO tokens stored in `~/.aws/sso/cache/.json`. -/// `` is computed based on the configured [`session_namej`](Builder::session_name). +/// `` is computed based on the configured [`session_name`](Builder::session_name). /// /// If possible, the cached token will be refreshed when it gets close to expiring. #[derive(Debug)] @@ -324,11 +324,7 @@ impl Builder { self.build_with(Env::real(), Fs::real()) } - pub(crate) fn build_sync(self) -> SsoTokenProvider { - self.build_with(Env::real(), Fs::real()) - } - - fn build_with(self, env: Env, fs: Fs) -> SsoTokenProvider { + pub(crate) fn build_with(self, env: Env, fs: Fs) -> SsoTokenProvider { SsoTokenProvider { inner: Arc::new(Inner { env, diff --git a/aws/rust-runtime/aws-config/test-data/assume-role-tests.json b/aws/rust-runtime/aws-config/test-data/assume-role-tests.json index 6d58d060c3..52e2e6639c 100644 --- a/aws/rust-runtime/aws-config/test-data/assume-role-tests.json +++ b/aws/rust-runtime/aws-config/test-data/assume-role-tests.json @@ -2,7 +2,7 @@ { "docs": "basic test case, a role_arn backed by a static credential", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "source_profile": "B" @@ -33,7 +33,7 @@ { "docs": "ignore explicit credentials when source profile is specified", "input": { - "profile": { + "profiles": { "A": { "aws_access_key_id": "abc123", "aws_secret_access_key": "def456", @@ -66,7 +66,7 @@ { "docs": "load role_session_name for the AssumeRole provider", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "role_session_name": "my_session_name", @@ -99,7 +99,7 @@ { "docs": "load external id for the AssumeRole provider", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "external_id": "my_external_id", @@ -132,7 +132,7 @@ { "docs": "self referential profile (first load base creds, then use for the role)", "input": { - "profile": { + "profiles": { "A": { "aws_access_key_id": "abc123", "aws_secret_access_key": "def456", @@ -161,7 +161,7 @@ { "docs": "Load credentials from a credential_source", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "credential_source": "Ec2InstanceMetadata" @@ -185,7 +185,7 @@ { "docs": "role_arn without source source_profile", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA" } @@ -199,7 +199,7 @@ { "docs": "source profile and credential source both present", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "credential_source": "Environment", @@ -219,7 +219,7 @@ { "docs": "partial credentials error (missing secret)", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:foo", "source_profile": "B" @@ -237,7 +237,7 @@ { "docs": "partial credentials error (missing access key)", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:foo", "source_profile": "B" @@ -255,7 +255,7 @@ { "docs": "missing credentials error (empty source profile)", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:foo", "source_profile": "B" @@ -271,7 +271,7 @@ { "docs": "profile only contains configuration", "input": { - "profile": { + "profiles": { "A": { "ec2_metadata_service_endpoint_mode": "IPv6" } @@ -285,7 +285,7 @@ { "docs": "missing source profile", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:foo", "source_profile": "B" @@ -300,7 +300,7 @@ { "docs": "missing root profile (should never happen in practice)", "input": { - "profile": { + "profiles": { }, "selected_profile": "A" }, @@ -311,7 +311,7 @@ { "docs": "multiple chained assume role profiles", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "source_profile": "B" @@ -351,7 +351,7 @@ { "docs": "chained assume role profiles with static credentials (ignore assume role when static credentials present)", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "aws_access_key_id": "bug_if_returned", @@ -390,7 +390,7 @@ { "docs": "assume role profile infinite loop", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "source_profile": "B" @@ -409,7 +409,7 @@ { "docs": "infinite loop with static credentials", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "aws_access_key_id": "bug_if_returned", @@ -430,7 +430,7 @@ { "docs": "web identity role", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "web_identity_token_file": "/var/token.jwt" @@ -452,7 +452,7 @@ { "docs": "web identity role with session name", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "web_identity_token_file": "/var/token.jwt", @@ -476,7 +476,7 @@ { "docs": "web identity role", "input": { - "profile": { + "profiles": { "A": { "web_identity_token_file": "/var/token.jwt" } @@ -490,7 +490,7 @@ { "docs": "web identity token as source profile", "input": { - "profile": { + "profiles": { "A": { "role_arn": "arn:aws:iam::123456789:role/RoleA", "source_profile": "B" @@ -521,10 +521,10 @@ } }, { - "docs": "SSO profile selected", + "docs": "SSO credentials profile selected", "input": { "selected_profile": "A", - "profile": { + "profiles": { "A": { "sso_account_id": "0123", "sso_region": "us-east-7", @@ -547,10 +547,10 @@ } }, { - "docs": "invalid SSO configuration", + "docs": "invalid SSO credentials configuration: missing account ID", "input": { "selected_profile": "A", - "profile": { + "profiles": { "A": { "sso_region": "us-east-7", "sso_role_name": "testrole", @@ -559,7 +559,87 @@ } }, "output": { - "Error": "`sso_account_id` was missing" + "Error": "if `sso_role_name` is set, then `sso_account_id` must also be set" + } + }, + { + "docs": "invalid SSO credentials configuration: missing role name", + "input": { + "selected_profile": "A", + "profiles": { + "A": { + "sso_region": "us-east-7", + "sso_account_id": "012345678901", + "sso_start_url": "https://foo.bar" + } + } + }, + "output": { + "Error": "if `sso_account_id` is set, then `sso_role_name` must also be set" + } + }, + { + "docs": "invalid SSO token configuration: sso_region must be in sso-session", + "input": { + "selected_profile": "A", + "profiles": { + "A": { + "sso_session": "A", + "sso_region": "us-east-7" + } + }, + "sso_sessions": { + "A": { + "sso_start_url": "https://foo.bar" + } + } + }, + "output": { + "Error": "`sso_region` can only be specified in the [sso-session] config when a session name is given" + } + }, + { + "docs": "SSO token profile selected", + "input": { + "selected_profile": "A", + "profiles": { + "A": { + "sso_session": "foo" + } + }, + "sso_sessions": { + "foo": { + "sso_region": "us-east-7", + "sso_start_url": "https://foo.bar" + } + } + }, + "output": { + "ProfileChain": [ + { + "Sso": { + "sso_session": "foo", + "sso_region": "us-east-7", + "sso_start_url": "https://foo.bar" + } + } + ] + } + }, + { + "docs": "invalid SSO token configuration: sso-session not found", + "input": { + "selected_profile": "A", + "profiles": { + "A": { + "sso_session": "oops" + } + }, + "sso_sessions": { + } + }, + "output": { + "Error": "sso-session named `oops` (referenced by profile `A`) was not found" } } ] 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 6f6f2a8a70..98dd9064c2 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 @@ -11,7 +11,9 @@ "configFile": "" }, "output": { - "profiles": {} + "config": { + "profiles": {} + } } }, { @@ -20,8 +22,10 @@ "configFile": "[profile foo]" }, "output": { - "profiles": { - "foo": {} + "config": { + "profiles": { + "foo": {} + } } } }, @@ -31,8 +35,10 @@ "configFile": "[profile some-thing:long/the_one%only.foo@bar+]\n[profile foo!bar]" }, "output": { - "profiles": { - "some-thing:long/the_one%only.foo@bar+": {} + "config": { + "profiles": { + "some-thing:long/the_one%only.foo@bar+": {} + } } } }, @@ -51,8 +57,10 @@ "configFile": "[profile \tfoo \t]" }, "output": { - "profiles": { - "foo": {} + "config": { + "profiles": { + "foo": {} + } } } }, @@ -62,8 +70,10 @@ "configFile": "[profile\tfoo]" }, "output": { - "profiles": { - "foo": {} + "config": { + "profiles": { + "foo": {} + } } } }, @@ -82,9 +92,11 @@ "configFile": "[profile foo]\nname = value" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -95,9 +107,11 @@ "configFile": "[profile foo]\r\nname = value" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -108,9 +122,11 @@ "configFile": "[profile foo]\nname = val=ue" }, "output": { - "profiles": { - "foo": { - "name": "val=ue" + "config": { + "profiles": { + "foo": { + "name": "val=ue" + } } } } @@ -121,9 +137,11 @@ "configFile": "[profile foo]\nname = 😂" }, "output": { - "profiles": { - "foo": { - "name": "😂" + "config": { + "profiles": { + "foo": { + "name": "😂" + } } } } @@ -134,10 +152,12 @@ "configFile": "[profile foo]\nname = value\nname2 = value2" }, "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" + "config": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } } } } @@ -148,10 +168,12 @@ "configFile": "[profile foo]\nname = value\nname2 = value2" }, "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" + "config": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } } } } @@ -162,9 +184,11 @@ "configFile": "[profile foo]\nname \t= \tvalue \t" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -175,9 +199,11 @@ "configFile": "[profile foo]\nname =" }, "output": { - "profiles": { - "foo": { - "name": "" + "config": { + "profiles": { + "foo": { + "name": "" + } } } } @@ -206,9 +232,11 @@ "configFile": "[profile foo]\n[profile bar]" }, "output": { - "profiles": { - "foo": {}, - "bar": {} + "config": { + "profiles": { + "foo": {}, + "bar": {} + } } } }, @@ -218,12 +246,14 @@ "configFile": "[profile foo]\nname = value\n[profile bar]\nname2 = value2" }, "output": { - "profiles": { - "foo": { - "name": "value" - }, - "bar": { - "name2": "value2" + "config": { + "profiles": { + "foo": { + "name": "value" + }, + "bar": { + "name2": "value2" + } } } } @@ -234,11 +264,13 @@ "configFile": "\t \n[profile foo]\n\t\n \nname = value\n\t \n[profile bar]\n \t" }, "output": { - "profiles": { - "foo": { - "name": "value" - }, - "bar": {} + "config": { + "profiles": { + "foo": { + "name": "value" + }, + "bar": {} + } } } }, @@ -248,9 +280,11 @@ "configFile": "# Comment\n[profile foo] # Comment\nname = value # Comment with # sign" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -261,9 +295,11 @@ "configFile": "; Comment\n[profile foo] ; Comment\nname = value ; Comment with ; sign" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -274,9 +310,11 @@ "configFile": "# Comment\n[profile foo] ; Comment\nname = value # Comment with ; sign" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -287,9 +325,11 @@ "configFile": ";\n[profile foo];\nname = value ;\n" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -300,9 +340,11 @@ "configFile": "[profile foo]; Adjacent semicolons\n[profile bar]# Adjacent pound signs" }, "output": { - "profiles": { - "foo": {}, - "bar": {} + "config": { + "profiles": { + "foo": {}, + "bar": {} + } } } }, @@ -312,10 +354,12 @@ "configFile": "[profile foo]\nname = value; Adjacent semicolons\nname2 = value# Adjacent pound signs" }, "output": { - "profiles": { - "foo": { - "name": "value; Adjacent semicolons", - "name2": "value# Adjacent pound signs" + "config": { + "profiles": { + "foo": { + "name": "value; Adjacent semicolons", + "name2": "value# Adjacent pound signs" + } } } } @@ -326,9 +370,11 @@ "configFile": "[profile foo]\nname = value\n -continued" }, "output": { - "profiles": { - "foo": { - "name": "value\n-continued" + "config": { + "profiles": { + "foo": { + "name": "value\n-continued" + } } } } @@ -339,9 +385,11 @@ "configFile": "[profile foo]\nname = value\n -continued\n -and-continued" }, "output": { - "profiles": { - "foo": { - "name": "value\n-continued\n-and-continued" + "config": { + "profiles": { + "foo": { + "name": "value\n-continued\n-and-continued" + } } } } @@ -352,9 +400,11 @@ "configFile": "[profile foo]\nname = value\n \t -continued \t " }, "output": { - "profiles": { - "foo": { - "name": "value\n-continued" + "config": { + "profiles": { + "foo": { + "name": "value\n-continued" + } } } } @@ -365,9 +415,11 @@ "configFile": "[profile foo]\nname = value\n -continued # Comment" }, "output": { - "profiles": { - "foo": { - "name": "value\n-continued # Comment" + "config": { + "profiles": { + "foo": { + "name": "value\n-continued # Comment" + } } } } @@ -378,9 +430,11 @@ "configFile": "[profile foo]\nname = value\n -continued ; Comment" }, "output": { - "profiles": { - "foo": { - "name": "value\n-continued ; Comment" + "config": { + "profiles": { + "foo": { + "name": "value\n-continued ; Comment" + } } } } @@ -418,10 +472,12 @@ "configFile": "[profile foo]\nname = value\n[profile foo]\nname2 = value2" }, "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" + "config": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } } } } @@ -432,9 +488,11 @@ "configFile": "[profile foo]\nname = value\nname = value2" }, "output": { - "profiles": { - "foo": { - "name": "value2" + "config": { + "profiles": { + "foo": { + "name": "value2" + } } } } @@ -445,9 +503,11 @@ "configFile": "[profile foo]\nname = value\n[profile foo]\nname = value2" }, "output": { - "profiles": { - "foo": { - "name": "value2" + "config": { + "profiles": { + "foo": { + "name": "value2" + } } } } @@ -458,9 +518,11 @@ "configFile": "[profile foo]\nName = value\n[profile foo]\nname = value2" }, "output": { - "profiles": { - "foo": { - "name": "value2" + "config": { + "profiles": { + "foo": { + "name": "value2" + } } } } @@ -471,9 +533,11 @@ "configFile": "[profile default]\nname = value\n[default]\nname2 = value2" }, "output": { - "profiles": { - "default": { - "name": "value" + "config": { + "profiles": { + "default": { + "name": "value" + } } } } @@ -484,9 +548,11 @@ "configFile": "[default]\nname2 = value2\n[profile default]\nname = value" }, "output": { - "profiles": { - "default": { - "name": "value" + "config": { + "profiles": { + "default": { + "name": "value" + } } } } @@ -498,7 +564,9 @@ "credentialsFile": "[in valid 2]\nname2 = value2" }, "output": { - "profiles": {} + "config": { + "profiles": {} + } } }, { @@ -507,8 +575,10 @@ "configFile": "[profile foo]\nin valid = value" }, "output": { - "profiles": { - "foo": {} + "config": { + "profiles": { + "foo": {} + } } } }, @@ -518,8 +588,10 @@ "configFile": "[profile ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_]" }, "output": { - "profiles": { - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_": {} + "config": { + "profiles": { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_": {} + } } } }, @@ -529,9 +601,11 @@ "configFile": "[profile foo]\nABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ = value" }, "output": { - "profiles": { - "foo": { - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789-_": "value" + "config": { + "profiles": { + "foo": { + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789-_": "value" + } } } } @@ -542,9 +616,11 @@ "configFile": "[profile foo]\ns3 =\n name = value" }, "output": { - "profiles": { - "foo": { - "s3": "\nname = value" + "config": { + "profiles": { + "foo": { + "s3": "\nname = value" + } } } } @@ -564,9 +640,11 @@ "configFile": "[profile foo]\ns3 =\n name =" }, "output": { - "profiles": { - "foo": { - "s3": "\nname =" + "config": { + "profiles": { + "foo": { + "s3": "\nname =" + } } } } @@ -586,9 +664,11 @@ "configFile": "[profile foo]\ns3 =\n in valid = value" }, "output": { - "profiles": { - "foo": { - "s3": "\nin valid = value" + "config": { + "profiles": { + "foo": { + "s3": "\nin valid = value" + } } } } @@ -599,9 +679,11 @@ "configFile": "[profile foo]\ns3 =\n name = value\n\t \n name2 = value2" }, "output": { - "profiles": { - "foo": { - "s3": "\nname = value\nname2 = value2" + "config": { + "profiles": { + "foo": { + "s3": "\nname = value\nname2 = value2" + } } } } @@ -613,10 +695,12 @@ "credentialsFile": "[foo]\nname2 = value2" }, "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" + "config": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } } } } @@ -627,10 +711,12 @@ "configFile": "[profile default]\nname = value\n[default]\nname2 = value2\n[profile default]\nname3 = value3" }, "output": { - "profiles": { - "default": { - "name": "value", - "name3": "value3" + "config": { + "profiles": { + "default": { + "name": "value", + "name3": "value3" + } } } } @@ -642,11 +728,13 @@ "credentialsFile": "[default]\nsecret=foo" }, "output": { - "profiles": { - "default": { - "name": "value", - "name3": "value3", - "secret": "foo" + "config": { + "profiles": { + "default": { + "name": "value", + "name3": "value3", + "secret": "foo" + } } } } @@ -658,9 +746,11 @@ "credentialsFile": "[foo]\nname = value2" }, "output": { - "profiles": { - "foo": { - "name": "value2" + "config": { + "profiles": { + "foo": { + "name": "value2" + } } } } @@ -671,7 +761,9 @@ "configFile": "[foo]\nname = value" }, "output": { - "profiles": {} + "config": { + "profiles": {} + } } }, { @@ -680,7 +772,9 @@ "credentialsFile": "[profile foo]\nname = value" }, "output": { - "profiles": {} + "config": { + "profiles": {} + } } }, { @@ -689,9 +783,11 @@ "configFile": "[profile foo]; semicolon\n[profile bar]# pound" }, "output": { - "profiles": { - "foo": {}, - "bar": {} + "config": { + "profiles": { + "foo": {}, + "bar": {} + } } } }, @@ -710,8 +806,10 @@ "configFile": "[profilefoo]\nname = value\n[profile bar]" }, "output": { - "profiles": { - "bar": {} + "config": { + "profiles": { + "bar": {} + } } } }, @@ -721,10 +819,12 @@ "configFile": "[ profile foo ]\nname = value\n[profile bar]" }, "output": { - "profiles": { - "bar": {}, - "foo": { - "name": "value" + "config": { + "profiles": { + "bar": {}, + "foo": { + "name": "value" + } } } } @@ -735,9 +835,11 @@ "credentialsFile": "[ foo ]\nname = value\n[profile bar]" }, "output": { - "profiles": { - "foo": { - "name": "value" + "config": { + "profiles": { + "foo": { + "name": "value" + } } } } @@ -748,12 +850,63 @@ "configFile": "[profile foo]\nname = value\n[profile in valid]\nx = 1\n[profile bar]\nname = value2" }, "output": { - "profiles": { - "bar": { - "name": "value2" + "config": { + "profiles": { + "bar": { + "name": "value2" + }, + "foo": { + "name": "value" + } + } + } + } + }, + { + "name": "sso-session with properties", + "input": { + "configFile": "[sso-session foo]\nname = value\n[profile bar]" + }, + "output": { + "config": { + "profiles": { + "bar": {} + }, + "sso_sessions": { + "foo": { + "name": "value" + } + } + } + } + }, + { + "name": "sso-session without the space before the name is a profile", + "input": { + "credentialsFile": "[sso-sessionfoo]\nname = value\n[bar]" + }, + "output": { + "config": { + "profiles": { + "sso-sessionfoo": { "name": "value" }, + "bar": {} + }, + "sso_sessions": { + } + } + } + }, + { + "name": "A typo'd sso-session will be ignored", + "input": { + "configFile": "[sso-sesion foo]\nname = value\n[profile bar]" + }, + "output": { + "config": { + "profiles": { + "bar": {} }, - "foo": { - "name": "value" + "sso_sessions": { } } } diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/env.json new file mode 100644 index 0000000000..55fcfbeb05 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/env.json @@ -0,0 +1,3 @@ +{ + "HOME": "/home" +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/config new file mode 100644 index 0000000000..91939db925 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/config @@ -0,0 +1,9 @@ +[profile default] +sso_session = dev +sso_account_id = 012345678901 +sso_role_name = SampleRole +region = us-east-1 + +[sso-session dev] +sso_region = us-east-1 +sso_start_url = https://d-abc123.awsapps.com/start diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/credentials new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json new file mode 100644 index 0000000000..06853e98f9 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/fs/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json @@ -0,0 +1,10 @@ +{ + "accessToken": "secret-access-token", + "expiresAt": "2199-11-14T04:05:45Z", + "refreshToken": "secret-refresh-token", + "clientId": "ABCDEFG323242423121312312312312312", + "clientSecret": "ABCDE123", + "registrationExpiresAt": "2199-03-06T19:53:17Z", + "region": "us-east-1", + "startUrl": "https://d-abc123.awsapps.com/start" +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/http-traffic.json new file mode 100644 index 0000000000..e92f6fafd3 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/http-traffic.json @@ -0,0 +1,92 @@ +{ + "docs": "test case using sso credentials", + "version": "V0", + "events": [ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "https://portal.sso.us-east-1.amazonaws.com/federation/credentials?role_name=SampleRole&account_id=012345678901", + "headers": { + "x-amz-sso_bearer_token": [ + "secret-access-token" + ], + "host": [ + "portal.sso.us-east-1.amazonaws.com" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "date": [ + "Thu, 05 Aug 2021 18:58:02 GMT" + ], + "content-length": [ + "1491" + ], + "content-type": [ + "text/xml" + ], + "x-amzn-requestid": [ + "c2e971c2-702d-4124-9b1f-1670febbea18" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } + ] +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/test-case.json new file mode 100644 index 0000000000..fcc74ffab0 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_credentials/test-case.json @@ -0,0 +1,12 @@ +{ + "name": "sso_credentials", + "docs": "sso_credentials loads SSO credentials using a local SSO session", + "result": { + "Ok": { + "access_key_id": "ASIARTESTID", + "secret_access_key": "TESTSECRETKEY", + "session_token": "TESTSESSIONTOKEN", + "expiry": 1651516560 + } + } +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/env.json new file mode 100644 index 0000000000..55fcfbeb05 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/env.json @@ -0,0 +1,3 @@ +{ + "HOME": "/home" +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/env.json new file mode 100644 index 0000000000..55fcfbeb05 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/env.json @@ -0,0 +1,3 @@ +{ + "HOME": "/home" +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/config new file mode 100644 index 0000000000..3cf147e265 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/config @@ -0,0 +1,7 @@ +[profile default] +sso_session = dev +region = us-east-1 + +[sso-session dev] +sso_region = us-east-1 +sso_start_url = https://d-abc123.awsapps.com/start diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/credentials new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json new file mode 100644 index 0000000000..06853e98f9 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json @@ -0,0 +1,10 @@ +{ + "accessToken": "secret-access-token", + "expiresAt": "2199-11-14T04:05:45Z", + "refreshToken": "secret-refresh-token", + "clientId": "ABCDEFG323242423121312312312312312", + "clientSecret": "ABCDE123", + "registrationExpiresAt": "2199-03-06T19:53:17Z", + "region": "us-east-1", + "startUrl": "https://d-abc123.awsapps.com/start" +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/http-traffic.json new file mode 100644 index 0000000000..e92f6fafd3 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/http-traffic.json @@ -0,0 +1,92 @@ +{ + "docs": "test case using sso credentials", + "version": "V0", + "events": [ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "https://portal.sso.us-east-1.amazonaws.com/federation/credentials?role_name=SampleRole&account_id=012345678901", + "headers": { + "x-amz-sso_bearer_token": [ + "secret-access-token" + ], + "host": [ + "portal.sso.us-east-1.amazonaws.com" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "date": [ + "Thu, 05 Aug 2021 18:58:02 GMT" + ], + "content-length": [ + "1491" + ], + "content-type": [ + "text/xml" + ], + "x-amzn-requestid": [ + "c2e971c2-702d-4124-9b1f-1670febbea18" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } + ] +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/test-case.json new file mode 100644 index 0000000000..7ff24f62a8 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/fs/test-case.json @@ -0,0 +1,7 @@ +{ + "name": "sso_token", + "docs": "sso_token attempts to use an SSO access token (AWS Builder ID) through the credentials chain when it should go through the token provider chain", + "result": { + "ErrorContains": "foobaz" + } +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/http-traffic.json new file mode 100644 index 0000000000..6a053d6a3b --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/http-traffic.json @@ -0,0 +1,5 @@ +{ + "docs": "test case using sso token", + "version": "V0", + "events": [] +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/test-case.json new file mode 100644 index 0000000000..2a0e6c87d2 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-provider/sso_token/test-case.json @@ -0,0 +1,7 @@ +{ + "name": "sso_token", + "docs": "sso_token loads a profile that doesn't have `sso_account_id` and `sso_role_name` fields, indicating that it's intended for the token provider chain in a service that uses SSO with an AWS Builder ID instead of credentials", + "result": { + "ErrorContains": "selected profile will resolve an access token instead of credentials" + } +}