From f2b05cf49aaacf2ea5987cba91ee7eeac0ecaa83 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Fri, 19 Nov 2021 10:13:53 -0600 Subject: [PATCH] fix: don't transform a ProfileDidNotContainCredentials error if base_provider is running for first provider in chain (#873) * fix: don't transform a ProfileDidNotContainCredentials if base_provider is running for first provider in chain add: more comments to aws_config::profile::credentials::repr functions * add: Test ensuring provider chain hits IMDS even if some config data exists * update: test to expect success response * add: clarifying comment about chain vs base providers --- .../aws-config/src/default_provider.rs | 1 + .../src/profile/credentials/repr.rs | 56 ++-- .../imds_config_with_no_creds/env.json | 4 + .../fs/home/.aws/config | 2 + .../http-traffic.json | 270 ++++++++++++++++++ .../imds_config_with_no_creds/test-case.json | 12 + 6 files changed, 324 insertions(+), 21 deletions(-) create mode 100644 aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/env.json create mode 100644 aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/fs/home/.aws/config create mode 100644 aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/http-traffic.json create mode 100644 aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/test-case.json diff --git a/aws/rust-runtime/aws-config/src/default_provider.rs b/aws/rust-runtime/aws-config/src/default_provider.rs index d470be2b40..72bc6c7121 100644 --- a/aws/rust-runtime/aws-config/src/default_provider.rs +++ b/aws/rust-runtime/aws-config/src/default_provider.rs @@ -524,6 +524,7 @@ pub mod credentials { make_test!(imds_default_chain_error); make_test!(imds_default_chain_success); make_test!(imds_assume_role); + make_test!(imds_config_with_no_creds); make_test!(imds_disabled); make_test!(imds_default_chain_retries); 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 555a2877f5..bd0be9fe42 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs @@ -113,6 +113,7 @@ pub fn resolve_chain<'a>( let mut visited_profiles = vec![]; let mut chain = vec![]; let base = loop { + // Get the next profile in the chain let profile = profile_set.get_profile(source_profile_name).ok_or( ProfileFileError::MissingProfile { profile: source_profile_name.into(), @@ -124,6 +125,8 @@ pub fn resolve_chain<'a>( .into(), }, )?; + // If the profile we just got is one we've already seen, we're in a loop and + // need to break out with a CredentialLoop error if visited_profiles.contains(&source_profile_name) { return Err(ProfileFileError::CredentialLoop { profiles: visited_profiles @@ -133,6 +136,7 @@ pub fn resolve_chain<'a>( next: source_profile_name.to_string(), }); } + // otherwise, store the name of the profile in case we see it again later visited_profiles.push(source_profile_name); // After the first item in the chain, we will prioritize static credentials if they exist if visited_profiles.len() > 1 { @@ -141,22 +145,32 @@ pub fn resolve_chain<'a>( break BaseProvider::AccessKey(static_credentials); } } - let next_profile = match chain_provider(profile) { - // this provider wasn't a chain provider, reload it as a base provider - None => { + + let next_profile = { + // The existence of a `role_arn` is the only signal that multiple profiles will be chained. + // We check for one here and then process the profile accordingly as either a "chain provider" + // or a "base provider" + if let Some(role_provider) = role_arn_from_profile(profile) { + let next = chain_provider(profile)?; + chain.push(role_provider); + next + } else { break base_provider(profile).map_err(|err| { - ProfileFileError::InvalidCredentialSource { - profile: profile.name().into(), - message: format!("could not load source profile: {}", err).into(), + // 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` + if visited_profiles.len() == 1 { + err + } else { + ProfileFileError::InvalidCredentialSource { + profile: profile.name().into(), + message: format!("could not load source profile: {}", err).into(), + } } })?; } - Some(result) => { - let (chain_profile, next) = result?; - chain.push(chain_profile); - next - } }; + match next_profile { NextProfile::SelfReference => { // self referential profile, don't go through the loop because it will error @@ -205,13 +219,12 @@ enum NextProfile<'a> { Named(&'a str), } -fn chain_provider(profile: &Profile) -> Option> { - let role_provider = role_arn_from_profile(profile)?; +fn chain_provider(profile: &Profile) -> Result { let (source_profile, credential_source) = ( profile.get(role::SOURCE_PROFILE), profile.get(role::CREDENTIAL_SOURCE), ); - let profile = match (source_profile, credential_source) { + match (source_profile, credential_source) { (Some(_), Some(_)) => Err(ProfileFileError::InvalidCredentialSource { profile: profile.name().to_string(), message: "profile contained both source_profile and credential_source. \ @@ -225,14 +238,12 @@ fn chain_provider(profile: &Profile) -> Option { - Ok((role_provider, NextProfile::SelfReference)) + Ok(NextProfile::SelfReference) } - - (Some(source_profile), None) => Ok((role_provider, NextProfile::Named(source_profile))), + (Some(source_profile), None) => Ok(NextProfile::Named(source_profile)), // we want to loop back into this profile and pick up the credential source - (None, Some(_credential_source)) => Ok((role_provider, NextProfile::SelfReference)), - }; - Some(profile) + (None, Some(_credential_source)) => Ok(NextProfile::SelfReference), + } } fn role_arn_from_profile(profile: &Profile) -> Option { @@ -285,11 +296,13 @@ fn static_creds_from_profile(profile: &Profile) -> Result Result assert!( format!("{}", e).contains(&s), - "expected {} to contain `{}`", + "expected\n{}\nto contain\n{}\n", e, s ), diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/env.json new file mode 100644 index 0000000000..db12cd280b --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/env.json @@ -0,0 +1,4 @@ +{ + "HOME": "/home", + "AWS_REGION": "us-east-1" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/fs/home/.aws/config new file mode 100644 index 0000000000..490734a829 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/fs/home/.aws/config @@ -0,0 +1,2 @@ +[default] +max_attempts = 1 diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/http-traffic.json new file mode 100644 index 0000000000..b47742e8f4 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/http-traffic.json @@ -0,0 +1,270 @@ +{ + "events": [ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-amz-user-agent": [ + "aws-sdk-rust/0.1.0 api/imds/0.1.0 os/linux lang/rust/1.52.1" + ], + "user-agent": [ + "aws-sdk-rust/0.1.0 os/linux lang/rust/1.52.1" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "content-type": [ + "text/plain" + ], + "connection": [ + "close" + ], + "server": [ + "EC2ws" + ], + "content-length": [ + "56" + ], + "date": [ + "Mon, 20 Sep 2021 21:43:31 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "AQAEAKQRRHnsX8GCPgYTGMShrFJkMhru3n-8Ul5Gzvzj-bpWKYZuiw==" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials", + "headers": { + "x-amz-user-agent": [ + "aws-sdk-rust/0.1.0 api/imds/0.1.0 os/linux lang/rust/1.52.1" + ], + "x-aws-ec2-metadata-token": [ + "AQAEAKQRRHnsX8GCPgYTGMShrFJkMhru3n-8Ul5Gzvzj-bpWKYZuiw==" + ], + "user-agent": [ + "aws-sdk-rust/0.1.0 os/linux lang/rust/1.52.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 1, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "content-length": [ + "21" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "connection": [ + "close" + ], + "last-modified": [ + "Mon, 20 Sep 2021 21:41:54 GMT" + ], + "content-type": [ + "text/plain" + ], + "date": [ + "Mon, 20 Sep 2021 21:43:31 GMT" + ], + "accept-ranges": [ + "none" + ] + } + } + } + } + } + }, + { + "connection_id": 1, + "action": { + "Data": { + "data": { + "Utf8": "imds-assume-role-test" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-assume-role-test", + "headers": { + "x-aws-ec2-metadata-token": [ + "AQAEAKQRRHnsX8GCPgYTGMShrFJkMhru3n-8Ul5Gzvzj-bpWKYZuiw==" + ], + "user-agent": [ + "aws-sdk-rust/0.1.0 os/linux lang/rust/1.52.1" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.1.0 api/imds/0.1.0 os/linux lang/rust/1.52.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 2, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "content-type": [ + "text/plain" + ], + "server": [ + "EC2ws" + ], + "content-length": [ + "1322" + ], + "accept-ranges": [ + "none" + ], + "date": [ + "Mon, 20 Sep 2021 21:43:31 GMT" + ], + "connection": [ + "close" + ], + "last-modified": [ + "Mon, 20 Sep 2021 21:41:54 GMT" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ] + } + } + } + } + } + }, + { + "connection_id": 2, + "action": { + "Data": { + "data": { + "Utf8": "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } + ], + "docs": "profile provider should hit IMDS if a profile contains config data but no credentials.", + "version": "V0" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/test-case.json new file mode 100644 index 0000000000..73a9fe8bb2 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_config_with_no_creds/test-case.json @@ -0,0 +1,12 @@ +{ + "name": "imds-config-with-no-creds", + "docs": "profile provider should hit IMDS if a profile contains config data but no credentials.", + "result": { + "Ok": { + "access_key_id": "ASIARTEST", + "secret_access_key": "testsecret", + "session_token": "testtoken", + "expiry": 1632197813 + } + } +}