use aws_smithy_json::deserialize::token::skip_value;
use aws_smithy_json::deserialize::{json_token_iter, EscapeError, Token};
use aws_smithy_types::date_time::Format;
use aws_smithy_types::DateTime;
use std::borrow::Cow;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::time::SystemTime;
#[derive(Debug)]
pub(crate) enum InvalidJsonCredentials {
JsonError(Box<dyn Error + Send + Sync>),
MissingField(&'static str),
InvalidField {
field: &'static str,
err: Box<dyn Error + Send + Sync>,
},
Other(Cow<'static, str>),
}
impl From<EscapeError> for InvalidJsonCredentials {
fn from(err: EscapeError) -> Self {
InvalidJsonCredentials::JsonError(err.into())
}
}
impl From<aws_smithy_json::deserialize::error::DeserializeError> for InvalidJsonCredentials {
fn from(err: aws_smithy_json::deserialize::error::DeserializeError) -> 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),
InvalidJsonCredentials::InvalidField { field, err } => {
write!(f, "Invalid field in response: `{}`. {}", field, err)
}
}
}
}
impl Error for InvalidJsonCredentials {}
#[derive(PartialEq, Eq)]
pub(crate) struct RefreshableCredentials<'a> {
pub(crate) access_key_id: Cow<'a, str>,
pub(crate) secret_access_key: Cow<'a, str>,
pub(crate) session_token: Cow<'a, str>,
pub(crate) expiration: SystemTime,
}
impl<'a> fmt::Debug for RefreshableCredentials<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("RefreshableCredentials")
.field("access_key_id", &self.access_key_id)
.field("secret_access_key", &"** redacted **")
.field("session_token", &"** redacted **")
.field("expiration", &self.expiration)
.finish()
}
}
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum JsonCredentials<'a> {
RefreshableCredentials(RefreshableCredentials<'a>),
Error {
code: Cow<'a, str>,
message: Cow<'a, str>,
}, }
pub(crate) fn parse_json_credentials(
credentials_response: &str,
) -> Result<JsonCredentials<'_>, InvalidJsonCredentials> {
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;
json_parse_loop(credentials_response.as_bytes(), |key, value| {
match (key, value) {
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Code") => {
code = Some(value.to_unescaped()?);
}
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccessKeyId") => {
access_key_id = Some(value.to_unescaped()?);
}
(key, Token::ValueString { value, .. })
if key.eq_ignore_ascii_case("SecretAccessKey") =>
{
secret_access_key = Some(value.to_unescaped()?);
}
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Token") => {
session_token = Some(value.to_unescaped()?);
}
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Expiration") => {
expiration = Some(value.to_unescaped()?);
}
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Message") => {
message = Some(value.to_unescaped()?);
}
_ => {}
};
Ok(())
})?;
match code {
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 = SystemTime::try_from(
DateTime::from_str(expiration.as_ref(), Format::DateTime).map_err(|err| {
InvalidJsonCredentials::InvalidField {
field: "Expiration",
err: err.into(),
}
})?,
)
.map_err(|_| {
InvalidJsonCredentials::Other(
"credential expiration time cannot be represented by a SystemTime".into(),
)
})?;
Ok(JsonCredentials::RefreshableCredentials(
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()),
}),
}
}
pub(crate) fn json_parse_loop<'a>(
input: &'a [u8],
mut f: impl FnMut(Cow<'a, str>, &Token<'a>) -> Result<(), InvalidJsonCredentials>,
) -> Result<(), InvalidJsonCredentials> {
let mut tokens = json_token_iter(input).peekable();
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)) = tokens.peek() {
let key = key.to_unescaped()?;
f(key, token)?
}
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(),
));
}
Ok(())
}
#[cfg(test)]
mod test {
use crate::json_credentials::{
parse_json_credentials, InvalidJsonCredentials, JsonCredentials, RefreshableCredentials,
};
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(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(_) => {} 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(_) => {} _ => 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(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") => {} 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(),
}
);
}
#[test]
fn json_credentials_ecs() {
let response = r#"{
"RoleArn":"arn:aws:iam::123456789:role/ecs-task-role",
"AccessKeyId":"ASIARTEST",
"SecretAccessKey":"SECRETTEST",
"Token":"tokenEaCXVzLXdlc3QtMiJGMEQCIHt47W18eF4dYfSlmKGiwuJnqmIS3LMXNYfODBCEhcnaAiAnuhGOpcdIDxin4QFzhtgaCR2MpcVqR8NFJdMgOt0/xyrnAwhhEAEaDDEzNDA5NTA2NTg1NiIM9M9GT+c5UfV/8r7PKsQDUa9xE9Eprz5N+jgxbFSD2aJR2iyXCcP9Q1cOh4fdZhyw2WNmq9XnIa2tkzrreiQ5R2t+kzergJHO1KRZPfesarfJ879aWJCSocsEKh7xXwwzTsVXrNo5eWkpwTh64q+Ksz15eoaBhtrvnGvPx6SmXv7SToi/DTHFafJlT/T9jITACZvZXSE9zfLka26Rna3rI4g0ugowha//j1f/c1XuKloqshpZvMKc561om9Y5fqBv1fRiS2KhetGTcmz3wUqNQAk8Dq9oINS7cCtdIO0atqCK69UaKeJ9uKY8mzY9dFWw2IrkpOoXmA9r955iU0NOz/95jVJiPZ/8aE8vb0t67gQfzBUCfky+mGSGWAfPRXQlFa5AEulCTHPd7IcTVCtasG033oKEKgB8QnTxvM2LaPlwaaHo7MHGYXeUKbn9NRKd8m1ShwmAlr4oKp1vQp6cPHDTsdTfPTzh/ZAjUPs+ljQbAwqXbPQdUUPpOk0vltY8k6Im9EA0pf80iUNoqrixpmPsR2hzI/ybUwdh+QhvCSBx+J8KHqF6X92u4qAVYIxLy/LGZKT9YC6Kr9Gywn+Ro+EK/xl3axHPzNpbjRDJnbW3HrMw5LmmiwY6pgGWgmD6IOq4QYUtu1uhaLQZyoI5o5PWn+d3kqqxifu8D0ykldB3lQGdlJ2rjKJjCdx8fce1SoXao9cc4hiwn39hUPuTqzVwv2zbzCKmNggIpXP6gqyRtUCakf6tI7ZwqTb2S8KF3t4ElIP8i4cPdNoI0JHSC+sT4LDPpUcX1CjGxfvo55mBHJedW3LXve8TRj4UckFXT1gLuTnzqPMrC5AHz4TAt+uv",
"Expiration" : "2009-02-13T23:31:30Z"
}"#;
let parsed = parse_json_credentials(response).expect("valid JSON");
use std::borrow::Cow;
assert!(
matches!(
&parsed,
JsonCredentials::RefreshableCredentials(RefreshableCredentials{
access_key_id: Cow::Borrowed("ASIARTEST"),
secret_access_key: Cow::Borrowed("SECRETTEST"),
session_token,
expiration
}) if session_token.starts_with("token") && *expiration == UNIX_EPOCH + Duration::from_secs(1234567890)
),
"{:?}",
parsed
);
}
#[test]
fn case_insensitive_code_parsing() {
let response = r#"{
"code" : "AssumeRoleUnauthorizedAccess",
"message" : "EC2 cannot assume the role integration-test."
}"#;
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(),
}
);
}
}