Loading aws/rust-runtime/aws-config/src/imds/credentials.rs +14 −299 Original line number Diff line number Diff line Loading @@ -10,19 +10,13 @@ use crate::imds; use crate::imds::client::{ImdsError, LazyClient}; use crate::json_credentials::{parse_json_credentials, JsonCredentials}; use crate::provider_config::ProviderConfig; use aws_types::credentials::{future, CredentialsError, ProvideCredentials}; use aws_types::os_shim_internal::Env; use aws_types::{credentials, Credentials}; use smithy_client::SdkError; use smithy_json::deserialize::token::skip_value; use smithy_json::deserialize::{json_token_iter, EscapeError, Token}; use smithy_types::instant::Format; use smithy_types::Instant; use std::borrow::Cow; use std::error::Error; use std::fmt::{Display, Formatter}; use std::time::SystemTime; use tokio::sync::OnceCell; /// IMDSv2 Credentials Provider Loading Loading @@ -171,8 +165,8 @@ impl ImdsCredentialsProvider { )) .await .map_err(|e| CredentialsError::ProviderError(e.into()))?; match parse_imds_credentials(&credentials) { Ok(ImdsCredentialsResponse::Success { match parse_json_credentials(&credentials) { Ok(JsonCredentials::RefreshableCredentials { access_key_id, secret_access_key, session_token, Loading @@ -181,11 +175,11 @@ impl ImdsCredentialsProvider { }) => Ok(Credentials::new( access_key_id, secret_access_key, session_token.map(|tok| tok.to_string()), Some(expiration), Some(session_token.to_string()), expiration.into(), "IMDSv2", )), Ok(ImdsCredentialsResponse::Error { code, message }) Ok(JsonCredentials::Error { code, message }) if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS => { Err(CredentialsError::InvalidConfiguration( Loading @@ -197,294 +191,15 @@ impl ImdsCredentialsProvider { .into(), )) } Ok(ImdsCredentialsResponse::Error { code, message }) => { Err(CredentialsError::ProviderError( Ok(JsonCredentials::Error { code, message }) => Err(CredentialsError::ProviderError( format!( "Error retrieving credentials from IMDS: {} {}", code, message ) .into(), )) } )), // got bad data from IMDS, should not occur during normal operation: Err(invalid) => Err(CredentialsError::Unhandled(invalid.into())), } } } /// Internal data mapping for IMDS response #[derive(PartialEq, Eq, Debug)] enum ImdsCredentialsResponse<'a> { Success { access_key_id: Cow<'a, str>, secret_access_key: Cow<'a, str>, session_token: Option<Cow<'a, str>>, expiration: SystemTime, r#type: Cow<'a, str>, }, Error { code: Cow<'a, str>, message: Cow<'a, str>, }, } impl From<EscapeError> for InvalidImdsResponse { fn from(err: EscapeError) -> Self { InvalidImdsResponse::JsonError(err.into()) } } impl From<smithy_json::deserialize::Error> for InvalidImdsResponse { fn from(err: smithy_json::deserialize::Error) -> Self { InvalidImdsResponse::JsonError(err.into()) } } #[derive(Debug)] enum InvalidImdsResponse { JsonError(Box<dyn Error + Send + Sync>), Custom(Cow<'static, str>), MissingField(&'static str), } impl Display for InvalidImdsResponse { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { InvalidImdsResponse::Custom(msg) => write!(f, "{}", msg), InvalidImdsResponse::MissingField(field) => write!( f, "Expected field `{}` in IMDS response but it was missing", field ), InvalidImdsResponse::JsonError(json) => { write!(f, "invalid JSON in IMDS response: {}", json) } } } } impl Error for InvalidImdsResponse {} /// Deserialize an IMDS response from a string /// /// There are two levels of error here: the top level distinguishes between a successfully parsed /// response from IMDS vs. something invalid / unexpected. The inner error distinguishes between /// a successful request to IMDS that includes credentials vs. an error with a code. fn parse_imds_credentials( credentials_response: &str, ) -> Result<ImdsCredentialsResponse, InvalidImdsResponse> { let mut tokens = json_token_iter(credentials_response.as_bytes()).peekable(); let mut code = None; let mut tpe = None; let mut access_key_id = None; let mut secret_access_key = None; let mut session_token = None; let mut expiration = None; let mut message = None; if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) { return Err(InvalidImdsResponse::JsonError( "expected a JSON document starting with `{`".into(), )); } loop { match tokens.next().transpose()? { Some(Token::EndObject { .. }) => break, Some(Token::ObjectKey { key, .. }) => { if let Some(Ok(Token::ValueString { value, .. })) = tokens.peek() { match key.as_escaped_str() { /* "Code": "Success", "Type": "AWS-HMAC", "AccessKeyId" : "accessKey", "SecretAccessKey" : "secret", "Token" : "token", "Expiration" : "....", "LastUpdated" : "2009-11-23T0:00:00Z" */ "Code" => code = Some(value.to_unescaped()?), "Type" => tpe = Some(value.to_unescaped()?), "AccessKeyId" => access_key_id = Some(value.to_unescaped()?), "SecretAccessKey" => secret_access_key = Some(value.to_unescaped()?), "Token" => session_token = Some(value.to_unescaped()?), "Expiration" => expiration = Some(value.to_unescaped()?), // Error case handling: message will be set "Message" => message = Some(value.to_unescaped()?), _ => {} } } skip_value(&mut tokens)?; } other => { return Err(InvalidImdsResponse::Custom( format!("expected object key, found: {:?}", other,).into(), )); } } } if tokens.next().is_some() { return Err(InvalidImdsResponse::Custom( "found more JSON tokens after completing parsing".into(), )); } match code { // IMDS does not appear to reply with an `Code` missing, but documentation indicates it // may be possible None | Some(Cow::Borrowed("Success")) => { let tpe = tpe.ok_or(InvalidImdsResponse::MissingField("Type"))?; let access_key_id = access_key_id.ok_or(InvalidImdsResponse::MissingField("AccessKeyId"))?; let secret_access_key = secret_access_key.ok_or(InvalidImdsResponse::MissingField("SecretAccessKey"))?; let expiration = expiration.ok_or(InvalidImdsResponse::MissingField("Expiration"))?; let expiration = Instant::from_str(expiration.as_ref(), Format::DateTime) .map_err(|err| { InvalidImdsResponse::Custom(format!("invalid date: {}", err).into()) })? .to_system_time() .ok_or_else(|| { InvalidImdsResponse::Custom("invalid expiration (prior to unix epoch)".into()) })?; Ok(ImdsCredentialsResponse::Success { access_key_id, secret_access_key, r#type: tpe, session_token, expiration, }) } Some(other) => Ok(ImdsCredentialsResponse::Error { code: other, message: message.unwrap_or_else(|| "no message".into()), }), } } #[cfg(test)] mod test { use crate::imds::credentials::{ parse_imds_credentials, ImdsCredentialsResponse, InvalidImdsResponse, }; use std::time::{Duration, UNIX_EPOCH}; #[test] fn imds_success_response() { let response = r#" { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_imds_credentials(response).expect("valid JSON"); assert_eq!( parsed, ImdsCredentialsResponse::Success { r#type: "AWS-HMAC".into(), access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: Some("IQote///test".into()), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn imds_invalid_json() { let error = parse_imds_credentials("404: not found").expect_err("no json"); match error { InvalidImdsResponse::JsonError(_) => {} // ok. err => panic!("incorrect error: {:?}", err), } } #[test] fn imds_not_json_object() { let error = parse_imds_credentials("[1,2,3]").expect_err("no json"); match error { InvalidImdsResponse::JsonError(_) => {} // ok. _ => panic!("incorrect error"), } } #[test] fn imds_missing_code() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_imds_credentials(resp).expect("code not required"); assert_eq!( parsed, ImdsCredentialsResponse::Success { r#type: "AWS-HMAC".into(), access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: Some("IQote///test".into()), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn imds_optional_session_token() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_imds_credentials(resp).expect("code not required"); assert_eq!( parsed, ImdsCredentialsResponse::Success { r#type: "AWS-HMAC".into(), access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: None, expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn imds_missing_akid() { let resp = r#"{ "Code": "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; match parse_imds_credentials(resp).expect_err("no code") { InvalidImdsResponse::MissingField("AccessKeyId") => {} // ok resp => panic!("incorrect imds response: {:?}", resp), } } #[test] fn imds_error_response() { let response = r#"{ "Code" : "AssumeRoleUnauthorizedAccess", "Message" : "EC2 cannot assume the role integration-test.", "LastUpdated" : "2021-09-17T20:46:56Z" }"#; let parsed = parse_imds_credentials(response).expect("valid JSON"); assert_eq!( parsed, ImdsCredentialsResponse::Error { code: "AssumeRoleUnauthorizedAccess".into(), message: "EC2 cannot assume the role integration-test.".into(), } ); } } aws/rust-runtime/aws-config/src/json_credentials.rs 0 → 100644 +299 −0 Original line number Diff line number Diff line use smithy_json::deserialize::token::skip_value; use smithy_json::deserialize::{json_token_iter, EscapeError, Token}; use smithy_types::instant::Format; use smithy_types::Instant; use std::borrow::Cow; use std::error::Error; use std::fmt::{Display, Formatter}; use std::time::SystemTime; #[derive(Debug)] pub(crate) enum InvalidJsonCredentials { /// The response did not contain valid JSON JsonError(Box<dyn Error + Send + Sync>), /// The response was missing a required field MissingField(&'static str), /// Another unhandled error occured Other(Cow<'static, str>), } impl From<EscapeError> for InvalidJsonCredentials { fn from(err: EscapeError) -> Self { InvalidJsonCredentials::JsonError(err.into()) } } impl From<smithy_json::deserialize::Error> for InvalidJsonCredentials { fn from(err: smithy_json::deserialize::Error) -> Self { InvalidJsonCredentials::JsonError(err.into()) } } impl Display for InvalidJsonCredentials { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { InvalidJsonCredentials::JsonError(json) => { write!(f, "invalid JSON in response: {}", json) } InvalidJsonCredentials::MissingField(field) => write!( f, "Expected field `{}` in response but it was missing", field ), InvalidJsonCredentials::Other(msg) => write!(f, "{}", msg), } } } impl Error for InvalidJsonCredentials {} #[non_exhaustive] #[derive(Debug, PartialEq, Eq)] pub(crate) enum JsonCredentials<'a> { RefreshableCredentials { access_key_id: Cow<'a, str>, secret_access_key: Cow<'a, str>, session_token: Cow<'a, str>, expiration: SystemTime, }, Error { code: Cow<'a, str>, message: Cow<'a, str>, }, // TODO(GeneralizedHttpCredentials): Add support for static credentials: // { // "AccessKeyId" : "MUA...", // "SecretAccessKey" : "/7PC5om...." // } // TODO(GeneralizedHttpCredentials): Add support for Assume role credentials: // { // // fields to construct STS client: // "Region": "sts-region-name", // "AccessKeyId" : "MUA...", // "Expiration" : "2016-02-25T06:03:31Z", // optional // "SecretAccessKey" : "/7PC5om....", // "Token" : "AQoDY....=", // optional // // fields controlling the STS role: // "RoleArn": "...", // required // "RoleSessionName": "...", // required // // and also: DurationSeconds, ExternalId, SerialNumber, TokenCode, Policy // ... // } } /// Deserialize an IMDS response from a string /// /// There are two levels of error here: the top level distinguishes between a successfully parsed /// response from the credential provider vs. something invalid / unexpected. The inner error /// distinguishes between a successful response that contains credentials vs. an error with a code and /// error message. pub(crate) fn parse_json_credentials( credentials_response: &str, ) -> Result<JsonCredentials, InvalidJsonCredentials> { let mut tokens = json_token_iter(credentials_response.as_bytes()).peekable(); let mut code = None; let mut access_key_id = None; let mut secret_access_key = None; let mut session_token = None; let mut expiration = None; let mut message = None; if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) { return Err(InvalidJsonCredentials::JsonError( "expected a JSON document starting with `{`".into(), )); } loop { match tokens.next().transpose()? { Some(Token::EndObject { .. }) => break, Some(Token::ObjectKey { key, .. }) => { if let Some(Ok(Token::ValueString { value, .. })) = tokens.peek() { match key.as_escaped_str() { /* "Code": "Success", "Type": "AWS-HMAC", "AccessKeyId" : "accessKey", "SecretAccessKey" : "secret", "Token" : "token", "Expiration" : "....", "LastUpdated" : "2009-11-23T0:00:00Z" */ "Code" => code = Some(value.to_unescaped()?), "AccessKeyId" => access_key_id = Some(value.to_unescaped()?), "SecretAccessKey" => secret_access_key = Some(value.to_unescaped()?), "Token" => session_token = Some(value.to_unescaped()?), "Expiration" => expiration = Some(value.to_unescaped()?), // Error case handling: message will be set "Message" => message = Some(value.to_unescaped()?), _ => {} } } skip_value(&mut tokens)?; } other => { return Err(InvalidJsonCredentials::Other( format!("expected object key, found: {:?}", other,).into(), )); } } } if tokens.next().is_some() { return Err(InvalidJsonCredentials::Other( "found more JSON tokens after completing parsing".into(), )); } match code { // IMDS does not appear to reply with an `Code` missing, but documentation indicates it // may be possible None | Some(Cow::Borrowed("Success")) => { let access_key_id = access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?; let secret_access_key = secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?; let session_token = session_token.ok_or(InvalidJsonCredentials::MissingField("Token"))?; let expiration = expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?; let expiration = Instant::from_str(expiration.as_ref(), Format::DateTime) .map_err(|err| { InvalidJsonCredentials::Other(format!("invalid date: {}", err).into()) })? .to_system_time() .ok_or_else(|| { InvalidJsonCredentials::Other("invalid expiration (prior to unix epoch)".into()) })?; Ok(JsonCredentials::RefreshableCredentials { access_key_id, secret_access_key, session_token, expiration, }) } Some(other) => Ok(JsonCredentials::Error { code: other, message: message.unwrap_or_else(|| "no message".into()), }), } } #[cfg(test)] mod test { use crate::json_credentials::{ parse_json_credentials, InvalidJsonCredentials, JsonCredentials, }; use std::time::{Duration, UNIX_EPOCH}; #[test] fn json_credentials_success_response() { let response = r#" { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_json_credentials(response).expect("valid JSON"); assert_eq!( parsed, JsonCredentials::RefreshableCredentials { access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: "IQote///test".into(), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn json_credentials_invalid_json() { let error = parse_json_credentials("404: not found").expect_err("no json"); match error { InvalidJsonCredentials::JsonError(_) => {} // ok. err => panic!("incorrect error: {:?}", err), } } #[test] fn json_credentials_not_json_object() { let error = parse_json_credentials("[1,2,3]").expect_err("no json"); match error { InvalidJsonCredentials::JsonError(_) => {} // ok. _ => panic!("incorrect error"), } } #[test] fn json_credentials_missing_code() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_json_credentials(resp).expect("code not required"); assert_eq!( parsed, JsonCredentials::RefreshableCredentials { access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: "IQote///test".into(), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn json_credentials_required_session_token() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_json_credentials(resp).expect_err("token missing"); assert_eq!( format!("{}", parsed), "Expected field `Token` in response but it was missing" ); } #[test] fn json_credentials_missing_akid() { let resp = r#"{ "Code": "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; match parse_json_credentials(resp).expect_err("no code") { InvalidJsonCredentials::MissingField("AccessKeyId") => {} // ok resp => panic!("incorrect json_credentials response: {:?}", resp), } } #[test] fn json_credentials_error_response() { let response = r#"{ "Code" : "AssumeRoleUnauthorizedAccess", "Message" : "EC2 cannot assume the role integration-test.", "LastUpdated" : "2021-09-17T20:46:56Z" }"#; let parsed = parse_json_credentials(response).expect("valid JSON"); assert_eq!( parsed, JsonCredentials::Error { code: "AssumeRoleUnauthorizedAccess".into(), message: "EC2 cannot assume the role integration-test.".into(), } ); } } aws/rust-runtime/aws-config/src/lib.rs +1 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ pub mod provider_config; mod cache; #[cfg(feature = "imds")] pub mod imds; mod json_credentials; /// Create an environment loader for AWS Configuration /// Loading Loading
aws/rust-runtime/aws-config/src/imds/credentials.rs +14 −299 Original line number Diff line number Diff line Loading @@ -10,19 +10,13 @@ use crate::imds; use crate::imds::client::{ImdsError, LazyClient}; use crate::json_credentials::{parse_json_credentials, JsonCredentials}; use crate::provider_config::ProviderConfig; use aws_types::credentials::{future, CredentialsError, ProvideCredentials}; use aws_types::os_shim_internal::Env; use aws_types::{credentials, Credentials}; use smithy_client::SdkError; use smithy_json::deserialize::token::skip_value; use smithy_json::deserialize::{json_token_iter, EscapeError, Token}; use smithy_types::instant::Format; use smithy_types::Instant; use std::borrow::Cow; use std::error::Error; use std::fmt::{Display, Formatter}; use std::time::SystemTime; use tokio::sync::OnceCell; /// IMDSv2 Credentials Provider Loading Loading @@ -171,8 +165,8 @@ impl ImdsCredentialsProvider { )) .await .map_err(|e| CredentialsError::ProviderError(e.into()))?; match parse_imds_credentials(&credentials) { Ok(ImdsCredentialsResponse::Success { match parse_json_credentials(&credentials) { Ok(JsonCredentials::RefreshableCredentials { access_key_id, secret_access_key, session_token, Loading @@ -181,11 +175,11 @@ impl ImdsCredentialsProvider { }) => Ok(Credentials::new( access_key_id, secret_access_key, session_token.map(|tok| tok.to_string()), Some(expiration), Some(session_token.to_string()), expiration.into(), "IMDSv2", )), Ok(ImdsCredentialsResponse::Error { code, message }) Ok(JsonCredentials::Error { code, message }) if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS => { Err(CredentialsError::InvalidConfiguration( Loading @@ -197,294 +191,15 @@ impl ImdsCredentialsProvider { .into(), )) } Ok(ImdsCredentialsResponse::Error { code, message }) => { Err(CredentialsError::ProviderError( Ok(JsonCredentials::Error { code, message }) => Err(CredentialsError::ProviderError( format!( "Error retrieving credentials from IMDS: {} {}", code, message ) .into(), )) } )), // got bad data from IMDS, should not occur during normal operation: Err(invalid) => Err(CredentialsError::Unhandled(invalid.into())), } } } /// Internal data mapping for IMDS response #[derive(PartialEq, Eq, Debug)] enum ImdsCredentialsResponse<'a> { Success { access_key_id: Cow<'a, str>, secret_access_key: Cow<'a, str>, session_token: Option<Cow<'a, str>>, expiration: SystemTime, r#type: Cow<'a, str>, }, Error { code: Cow<'a, str>, message: Cow<'a, str>, }, } impl From<EscapeError> for InvalidImdsResponse { fn from(err: EscapeError) -> Self { InvalidImdsResponse::JsonError(err.into()) } } impl From<smithy_json::deserialize::Error> for InvalidImdsResponse { fn from(err: smithy_json::deserialize::Error) -> Self { InvalidImdsResponse::JsonError(err.into()) } } #[derive(Debug)] enum InvalidImdsResponse { JsonError(Box<dyn Error + Send + Sync>), Custom(Cow<'static, str>), MissingField(&'static str), } impl Display for InvalidImdsResponse { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { InvalidImdsResponse::Custom(msg) => write!(f, "{}", msg), InvalidImdsResponse::MissingField(field) => write!( f, "Expected field `{}` in IMDS response but it was missing", field ), InvalidImdsResponse::JsonError(json) => { write!(f, "invalid JSON in IMDS response: {}", json) } } } } impl Error for InvalidImdsResponse {} /// Deserialize an IMDS response from a string /// /// There are two levels of error here: the top level distinguishes between a successfully parsed /// response from IMDS vs. something invalid / unexpected. The inner error distinguishes between /// a successful request to IMDS that includes credentials vs. an error with a code. fn parse_imds_credentials( credentials_response: &str, ) -> Result<ImdsCredentialsResponse, InvalidImdsResponse> { let mut tokens = json_token_iter(credentials_response.as_bytes()).peekable(); let mut code = None; let mut tpe = None; let mut access_key_id = None; let mut secret_access_key = None; let mut session_token = None; let mut expiration = None; let mut message = None; if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) { return Err(InvalidImdsResponse::JsonError( "expected a JSON document starting with `{`".into(), )); } loop { match tokens.next().transpose()? { Some(Token::EndObject { .. }) => break, Some(Token::ObjectKey { key, .. }) => { if let Some(Ok(Token::ValueString { value, .. })) = tokens.peek() { match key.as_escaped_str() { /* "Code": "Success", "Type": "AWS-HMAC", "AccessKeyId" : "accessKey", "SecretAccessKey" : "secret", "Token" : "token", "Expiration" : "....", "LastUpdated" : "2009-11-23T0:00:00Z" */ "Code" => code = Some(value.to_unescaped()?), "Type" => tpe = Some(value.to_unescaped()?), "AccessKeyId" => access_key_id = Some(value.to_unescaped()?), "SecretAccessKey" => secret_access_key = Some(value.to_unescaped()?), "Token" => session_token = Some(value.to_unescaped()?), "Expiration" => expiration = Some(value.to_unescaped()?), // Error case handling: message will be set "Message" => message = Some(value.to_unescaped()?), _ => {} } } skip_value(&mut tokens)?; } other => { return Err(InvalidImdsResponse::Custom( format!("expected object key, found: {:?}", other,).into(), )); } } } if tokens.next().is_some() { return Err(InvalidImdsResponse::Custom( "found more JSON tokens after completing parsing".into(), )); } match code { // IMDS does not appear to reply with an `Code` missing, but documentation indicates it // may be possible None | Some(Cow::Borrowed("Success")) => { let tpe = tpe.ok_or(InvalidImdsResponse::MissingField("Type"))?; let access_key_id = access_key_id.ok_or(InvalidImdsResponse::MissingField("AccessKeyId"))?; let secret_access_key = secret_access_key.ok_or(InvalidImdsResponse::MissingField("SecretAccessKey"))?; let expiration = expiration.ok_or(InvalidImdsResponse::MissingField("Expiration"))?; let expiration = Instant::from_str(expiration.as_ref(), Format::DateTime) .map_err(|err| { InvalidImdsResponse::Custom(format!("invalid date: {}", err).into()) })? .to_system_time() .ok_or_else(|| { InvalidImdsResponse::Custom("invalid expiration (prior to unix epoch)".into()) })?; Ok(ImdsCredentialsResponse::Success { access_key_id, secret_access_key, r#type: tpe, session_token, expiration, }) } Some(other) => Ok(ImdsCredentialsResponse::Error { code: other, message: message.unwrap_or_else(|| "no message".into()), }), } } #[cfg(test)] mod test { use crate::imds::credentials::{ parse_imds_credentials, ImdsCredentialsResponse, InvalidImdsResponse, }; use std::time::{Duration, UNIX_EPOCH}; #[test] fn imds_success_response() { let response = r#" { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_imds_credentials(response).expect("valid JSON"); assert_eq!( parsed, ImdsCredentialsResponse::Success { r#type: "AWS-HMAC".into(), access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: Some("IQote///test".into()), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn imds_invalid_json() { let error = parse_imds_credentials("404: not found").expect_err("no json"); match error { InvalidImdsResponse::JsonError(_) => {} // ok. err => panic!("incorrect error: {:?}", err), } } #[test] fn imds_not_json_object() { let error = parse_imds_credentials("[1,2,3]").expect_err("no json"); match error { InvalidImdsResponse::JsonError(_) => {} // ok. _ => panic!("incorrect error"), } } #[test] fn imds_missing_code() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_imds_credentials(resp).expect("code not required"); assert_eq!( parsed, ImdsCredentialsResponse::Success { r#type: "AWS-HMAC".into(), access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: Some("IQote///test".into()), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn imds_optional_session_token() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_imds_credentials(resp).expect("code not required"); assert_eq!( parsed, ImdsCredentialsResponse::Success { r#type: "AWS-HMAC".into(), access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: None, expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn imds_missing_akid() { let resp = r#"{ "Code": "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; match parse_imds_credentials(resp).expect_err("no code") { InvalidImdsResponse::MissingField("AccessKeyId") => {} // ok resp => panic!("incorrect imds response: {:?}", resp), } } #[test] fn imds_error_response() { let response = r#"{ "Code" : "AssumeRoleUnauthorizedAccess", "Message" : "EC2 cannot assume the role integration-test.", "LastUpdated" : "2021-09-17T20:46:56Z" }"#; let parsed = parse_imds_credentials(response).expect("valid JSON"); assert_eq!( parsed, ImdsCredentialsResponse::Error { code: "AssumeRoleUnauthorizedAccess".into(), message: "EC2 cannot assume the role integration-test.".into(), } ); } }
aws/rust-runtime/aws-config/src/json_credentials.rs 0 → 100644 +299 −0 Original line number Diff line number Diff line use smithy_json::deserialize::token::skip_value; use smithy_json::deserialize::{json_token_iter, EscapeError, Token}; use smithy_types::instant::Format; use smithy_types::Instant; use std::borrow::Cow; use std::error::Error; use std::fmt::{Display, Formatter}; use std::time::SystemTime; #[derive(Debug)] pub(crate) enum InvalidJsonCredentials { /// The response did not contain valid JSON JsonError(Box<dyn Error + Send + Sync>), /// The response was missing a required field MissingField(&'static str), /// Another unhandled error occured Other(Cow<'static, str>), } impl From<EscapeError> for InvalidJsonCredentials { fn from(err: EscapeError) -> Self { InvalidJsonCredentials::JsonError(err.into()) } } impl From<smithy_json::deserialize::Error> for InvalidJsonCredentials { fn from(err: smithy_json::deserialize::Error) -> Self { InvalidJsonCredentials::JsonError(err.into()) } } impl Display for InvalidJsonCredentials { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { InvalidJsonCredentials::JsonError(json) => { write!(f, "invalid JSON in response: {}", json) } InvalidJsonCredentials::MissingField(field) => write!( f, "Expected field `{}` in response but it was missing", field ), InvalidJsonCredentials::Other(msg) => write!(f, "{}", msg), } } } impl Error for InvalidJsonCredentials {} #[non_exhaustive] #[derive(Debug, PartialEq, Eq)] pub(crate) enum JsonCredentials<'a> { RefreshableCredentials { access_key_id: Cow<'a, str>, secret_access_key: Cow<'a, str>, session_token: Cow<'a, str>, expiration: SystemTime, }, Error { code: Cow<'a, str>, message: Cow<'a, str>, }, // TODO(GeneralizedHttpCredentials): Add support for static credentials: // { // "AccessKeyId" : "MUA...", // "SecretAccessKey" : "/7PC5om...." // } // TODO(GeneralizedHttpCredentials): Add support for Assume role credentials: // { // // fields to construct STS client: // "Region": "sts-region-name", // "AccessKeyId" : "MUA...", // "Expiration" : "2016-02-25T06:03:31Z", // optional // "SecretAccessKey" : "/7PC5om....", // "Token" : "AQoDY....=", // optional // // fields controlling the STS role: // "RoleArn": "...", // required // "RoleSessionName": "...", // required // // and also: DurationSeconds, ExternalId, SerialNumber, TokenCode, Policy // ... // } } /// Deserialize an IMDS response from a string /// /// There are two levels of error here: the top level distinguishes between a successfully parsed /// response from the credential provider vs. something invalid / unexpected. The inner error /// distinguishes between a successful response that contains credentials vs. an error with a code and /// error message. pub(crate) fn parse_json_credentials( credentials_response: &str, ) -> Result<JsonCredentials, InvalidJsonCredentials> { let mut tokens = json_token_iter(credentials_response.as_bytes()).peekable(); let mut code = None; let mut access_key_id = None; let mut secret_access_key = None; let mut session_token = None; let mut expiration = None; let mut message = None; if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) { return Err(InvalidJsonCredentials::JsonError( "expected a JSON document starting with `{`".into(), )); } loop { match tokens.next().transpose()? { Some(Token::EndObject { .. }) => break, Some(Token::ObjectKey { key, .. }) => { if let Some(Ok(Token::ValueString { value, .. })) = tokens.peek() { match key.as_escaped_str() { /* "Code": "Success", "Type": "AWS-HMAC", "AccessKeyId" : "accessKey", "SecretAccessKey" : "secret", "Token" : "token", "Expiration" : "....", "LastUpdated" : "2009-11-23T0:00:00Z" */ "Code" => code = Some(value.to_unescaped()?), "AccessKeyId" => access_key_id = Some(value.to_unescaped()?), "SecretAccessKey" => secret_access_key = Some(value.to_unescaped()?), "Token" => session_token = Some(value.to_unescaped()?), "Expiration" => expiration = Some(value.to_unescaped()?), // Error case handling: message will be set "Message" => message = Some(value.to_unescaped()?), _ => {} } } skip_value(&mut tokens)?; } other => { return Err(InvalidJsonCredentials::Other( format!("expected object key, found: {:?}", other,).into(), )); } } } if tokens.next().is_some() { return Err(InvalidJsonCredentials::Other( "found more JSON tokens after completing parsing".into(), )); } match code { // IMDS does not appear to reply with an `Code` missing, but documentation indicates it // may be possible None | Some(Cow::Borrowed("Success")) => { let access_key_id = access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?; let secret_access_key = secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?; let session_token = session_token.ok_or(InvalidJsonCredentials::MissingField("Token"))?; let expiration = expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?; let expiration = Instant::from_str(expiration.as_ref(), Format::DateTime) .map_err(|err| { InvalidJsonCredentials::Other(format!("invalid date: {}", err).into()) })? .to_system_time() .ok_or_else(|| { InvalidJsonCredentials::Other("invalid expiration (prior to unix epoch)".into()) })?; Ok(JsonCredentials::RefreshableCredentials { access_key_id, secret_access_key, session_token, expiration, }) } Some(other) => Ok(JsonCredentials::Error { code: other, message: message.unwrap_or_else(|| "no message".into()), }), } } #[cfg(test)] mod test { use crate::json_credentials::{ parse_json_credentials, InvalidJsonCredentials, JsonCredentials, }; use std::time::{Duration, UNIX_EPOCH}; #[test] fn json_credentials_success_response() { let response = r#" { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_json_credentials(response).expect("valid JSON"); assert_eq!( parsed, JsonCredentials::RefreshableCredentials { access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: "IQote///test".into(), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn json_credentials_invalid_json() { let error = parse_json_credentials("404: not found").expect_err("no json"); match error { InvalidJsonCredentials::JsonError(_) => {} // ok. err => panic!("incorrect error: {:?}", err), } } #[test] fn json_credentials_not_json_object() { let error = parse_json_credentials("[1,2,3]").expect_err("no json"); match error { InvalidJsonCredentials::JsonError(_) => {} // ok. _ => panic!("incorrect error"), } } #[test] fn json_credentials_missing_code() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_json_credentials(resp).expect("code not required"); assert_eq!( parsed, JsonCredentials::RefreshableCredentials { access_key_id: "ASIARTEST".into(), secret_access_key: "xjtest".into(), session_token: "IQote///test".into(), expiration: UNIX_EPOCH + Duration::from_secs(1631935916), } ) } #[test] fn json_credentials_required_session_token() { let resp = r#"{ "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Expiration" : "2021-09-18T03:31:56Z" }"#; let parsed = parse_json_credentials(resp).expect_err("token missing"); assert_eq!( format!("{}", parsed), "Expected field `Token` in response but it was missing" ); } #[test] fn json_credentials_missing_akid() { let resp = r#"{ "Code": "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }"#; match parse_json_credentials(resp).expect_err("no code") { InvalidJsonCredentials::MissingField("AccessKeyId") => {} // ok resp => panic!("incorrect json_credentials response: {:?}", resp), } } #[test] fn json_credentials_error_response() { let response = r#"{ "Code" : "AssumeRoleUnauthorizedAccess", "Message" : "EC2 cannot assume the role integration-test.", "LastUpdated" : "2021-09-17T20:46:56Z" }"#; let parsed = parse_json_credentials(response).expect("valid JSON"); assert_eq!( parsed, JsonCredentials::Error { code: "AssumeRoleUnauthorizedAccess".into(), message: "EC2 cannot assume the role integration-test.".into(), } ); } }
aws/rust-runtime/aws-config/src/lib.rs +1 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ pub mod provider_config; mod cache; #[cfg(feature = "imds")] pub mod imds; mod json_credentials; /// Create an environment loader for AWS Configuration /// Loading