use std::fmt;
use aws_config::SdkConfig;
use aws_sdk_sts::{
error::SdkError, operation::decode_authorization_message::DecodeAuthorizationMessageError,
};
use lazy_static::lazy_static;
use regex::Regex;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StatusReason<'a>(Option<&'a str>);
impl<'a> StatusReason<'a> {
pub(crate) fn new(status_reason: Option<&'a str>) -> Self {
Self(status_reason)
}
#[must_use]
pub fn inner(&self) -> Option<&'a str> {
self.0
}
pub fn detail(&self) -> Option<StatusReasonDetail<'a>> {
self.0.and_then(StatusReasonDetail::new)
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum StatusReasonDetail<'a> {
CreationCancelled,
MissingPermission(MissingPermission<'a>),
AuthorizationFailure(EncodedAuthorizationMessage<'a>),
ResourceErrors(ResourceErrors<'a>),
}
impl<'a> StatusReasonDetail<'a> {
fn new(status_reason: &'a str) -> Option<Self> {
lazy_static! {
static ref CREATION_CANCELLED: Regex =
Regex::new(r"(?i)Resource creation cancelled").unwrap();
static ref MISSING_PERMISSION_1: Regex =
Regex::new(r"(?i)API: (?P<permission>[a-z0-9]+:[a-z0-9]+)\b").unwrap();
static ref MISSING_PERMISSION_2: Regex =
Regex::new(r"(?i)User: (?P<principal>[a-z0-9:/-]+) is not authorized to perform: (?P<permission>[a-z0-9]+:[a-z0-9]+)").unwrap();
static ref RESOURCE_ERRORS: Regex =
Regex::new(r"(?i)The following resource\(s\) failed to (?:create|delete|update): \[(?P<logical_resource_ids>[a-z0-9]+(?:, *[a-z0-9]+)*)\]").unwrap();
static ref ENCODED_AUTHORIZATION_MESSAGE: Regex =
Regex::new("(?i)Encoded authorization failure message: (?P<encoded_authorization_message>[a-z0-9_-]+)").unwrap();
}
if CREATION_CANCELLED.is_match(status_reason) {
return Some(Self::CreationCancelled);
}
let encoded_authorization_message = ENCODED_AUTHORIZATION_MESSAGE
.captures(status_reason)
.map(|captures| {
EncodedAuthorizationMessage::new(
captures
.name("encoded_authorization_message")
.unwrap()
.as_str(),
)
});
if let Some(detail) = MISSING_PERMISSION_1.captures(status_reason) {
return Some(Self::MissingPermission(MissingPermission {
permission: detail.name("permission").unwrap().as_str(),
principal: None,
encoded_authorization_message,
}));
}
if let Some(detail) = MISSING_PERMISSION_2.captures(status_reason) {
return Some(Self::MissingPermission(MissingPermission {
permission: detail.name("permission").unwrap().as_str(),
principal: Some(detail.name("principal").unwrap().as_str()),
encoded_authorization_message,
}));
}
if let Some(encoded_authorization_message) = encoded_authorization_message {
return Some(Self::AuthorizationFailure(encoded_authorization_message));
}
if let Some(detail) = RESOURCE_ERRORS.captures(status_reason) {
return Some(Self::ResourceErrors(ResourceErrors {
logical_resource_ids: detail.name("logical_resource_ids").unwrap().as_str(),
}));
}
None
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MissingPermission<'a> {
pub permission: &'a str,
pub principal: Option<&'a str>,
pub encoded_authorization_message: Option<EncodedAuthorizationMessage<'a>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EncodedAuthorizationMessage<'a>(&'a str);
impl<'a> EncodedAuthorizationMessage<'a> {
pub(crate) fn new(message: &'a str) -> Self {
Self(message)
}
#[must_use]
pub fn inner(&self) -> &'a str {
self.0
}
pub async fn decode(
&self,
config: &SdkConfig,
) -> Result<serde_json::Value, EncodedAuthorizationMessageDecodeError> {
let sts = aws_sdk_sts::Client::new(config);
let output = sts
.decode_authorization_message()
.encoded_message(self.0.to_owned())
.send()
.await
.map_err(EncodedAuthorizationMessageDecodeError::from_sdk)?;
let message = output
.decoded_message
.expect("decode authorization message response without decoded_message");
Ok(serde_json::from_str(&message).expect("decoded authorization message isn't JSON"))
}
}
#[derive(Debug)]
pub struct EncodedAuthorizationMessageDecodeError(Box<dyn std::error::Error>);
impl EncodedAuthorizationMessageDecodeError {
fn from_sdk(error: SdkError<DecodeAuthorizationMessageError>) -> Self {
Self(error.into())
}
}
impl fmt::Display for EncodedAuthorizationMessageDecodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for EncodedAuthorizationMessageDecodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
}
#[derive(Clone, Debug, Eq)]
pub struct ResourceErrors<'a> {
logical_resource_ids: &'a str,
}
impl<'a> ResourceErrors<'a> {
pub fn logical_resource_ids(&self) -> impl Iterator<Item = &'a str> {
lazy_static! {
static ref LOGICAL_RESOURCE_ID: Regex = Regex::new("(?i)[a-z0-9]+").unwrap();
}
LOGICAL_RESOURCE_ID
.find_iter(self.logical_resource_ids)
.map(|m| m.as_str())
}
}
impl PartialEq for ResourceErrors<'_> {
fn eq(&self, other: &Self) -> bool {
self.logical_resource_ids().eq(other.logical_resource_ids())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_reason_detail() {
#![allow(clippy::shadow_unrelated)]
let example = r"Resource creation cancelled";
assert_eq!(
StatusReasonDetail::new(example),
Some(StatusReasonDetail::CreationCancelled)
);
let example = r"API: ec2:ModifyVpcAttribute You are not authorized to perform this operation. Encoded authorization failure message: g1-YvnBabE1x9q868e9rU4VX9gFjPpt31dEvX6uYDMWmdkou9pGLq85c3Wy4IAr3CwKrF8Jqu0aIkiy0TBM5SU22pSjE-gzZuP1dg5rvyhI1fl5DBB4DiDyRmZpOjovE2w0MMxuM4QFqf6zAtlbCtwdCYVxHwTpKrlkQAJEr40twnTPWe1_Vh-YRfprV9RBis8nReUcf87GV1oGFxjLujid4oOAinD-NmpIUR5VLCw2ycoOZihPR_unBC9stRioVeYiBg-Q1T5IU-J-xEQK092YuR-H4vqMm5Nwg4l1kN10t8pbFb_YopmILVfvh-ViLBbzE0cO6ZlvLvcMcB8crsbgLP10H05hPtHDIGUMwc_xM-y_9SUAcrVUfPKdM4JeMvNMLkFfuLcgMIjTivxG1y3DwligaBXrSwKVkkMB4XfswrU7nYT6PO0cIyD_v7vw5kPJP1EafEZGVMJrJJEwS43FVFkLCMIi6eSxyFTYRF4GUbkuXbTpfMxYdivdFdiofA6_JsC-AZXwcE3qXAHpJ3PrH6lYfWm8z0m8PATAQKTqlcEMIYNngNnmnqasBQ_anBj-C7BT4V_B67wOOhc_Vwheq6xKnsI7XfsTgzsmHdFZDVIBCrdw";
assert_eq!(
StatusReasonDetail::new(example),
Some(StatusReasonDetail::MissingPermission(MissingPermission {
permission: "ec2:ModifyVpcAttribute",
principal: None,
encoded_authorization_message: Some(EncodedAuthorizationMessage::new("g1-YvnBabE1x9q868e9rU4VX9gFjPpt31dEvX6uYDMWmdkou9pGLq85c3Wy4IAr3CwKrF8Jqu0aIkiy0TBM5SU22pSjE-gzZuP1dg5rvyhI1fl5DBB4DiDyRmZpOjovE2w0MMxuM4QFqf6zAtlbCtwdCYVxHwTpKrlkQAJEr40twnTPWe1_Vh-YRfprV9RBis8nReUcf87GV1oGFxjLujid4oOAinD-NmpIUR5VLCw2ycoOZihPR_unBC9stRioVeYiBg-Q1T5IU-J-xEQK092YuR-H4vqMm5Nwg4l1kN10t8pbFb_YopmILVfvh-ViLBbzE0cO6ZlvLvcMcB8crsbgLP10H05hPtHDIGUMwc_xM-y_9SUAcrVUfPKdM4JeMvNMLkFfuLcgMIjTivxG1y3DwligaBXrSwKVkkMB4XfswrU7nYT6PO0cIyD_v7vw5kPJP1EafEZGVMJrJJEwS43FVFkLCMIi6eSxyFTYRF4GUbkuXbTpfMxYdivdFdiofA6_JsC-AZXwcE3qXAHpJ3PrH6lYfWm8z0m8PATAQKTqlcEMIYNngNnmnqasBQ_anBj-C7BT4V_B67wOOhc_Vwheq6xKnsI7XfsTgzsmHdFZDVIBCrdw")),
}))
);
let example = r"API: s3:CreateBucket Access Denied";
assert_eq!(
StatusReasonDetail::new(example),
Some(StatusReasonDetail::MissingPermission(MissingPermission {
permission: "s3:CreateBucket",
principal: None,
encoded_authorization_message: None,
}))
);
let example = r#"Resource handler returned message: "User: arn:aws:iam::012345678910:user/cloudformatious-cli-testing is not authorized to perform: elasticfilesystem:CreateFileSystem on the specified resource (Service: Efs, Status Code: 403, Request ID: fedb2f85-ff52-496c-b7be-207a23072587, Extended Request ID: null)" (RequestToken: ccd41719-eae9-3614-3b35-1d1cc3ad55da, HandlerErrorCode: GeneralServiceException)"#;
assert_eq!(
StatusReasonDetail::new(example),
Some(StatusReasonDetail::MissingPermission(MissingPermission {
permission: "elasticfilesystem:CreateFileSystem",
principal: Some("arn:aws:iam::012345678910:user/cloudformatious-cli-testing"),
encoded_authorization_message: None,
}))
);
let example =
r"The following resource(s) failed to create: [Vpc, Fs]. Rollback requested by user.";
let detail = StatusReasonDetail::new(example).unwrap();
assert_eq!(
detail,
StatusReasonDetail::ResourceErrors(ResourceErrors {
logical_resource_ids: "Vpc, Fs",
})
);
if let StatusReasonDetail::ResourceErrors(resource_errors) = detail {
assert_eq!(
resource_errors.logical_resource_ids().collect::<Vec<_>>(),
vec!["Vpc", "Fs"]
);
} else {
unreachable!()
}
}
}