use std::fmt;
use std::ops::Deref;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonEmpty<T>(Vec<T>);
impl<T> NonEmpty<T> {
pub fn new(items: Vec<T>) -> Result<Self, Vec<T>> {
if items.is_empty() {
Err(items)
} else {
Ok(Self(items))
}
}
pub fn one(item: T) -> Self {
Self(vec![item])
}
pub fn first(&self) -> &T {
&self.0[0]
}
pub fn into_vec(self) -> Vec<T> {
self.0
}
}
impl<T> Deref for NonEmpty<T> {
type Target = [T];
fn deref(&self) -> &[T] {
&self.0
}
}
impl<T> IntoIterator for NonEmpty<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a, T> IntoIterator for &'a NonEmpty<T> {
type Item = &'a T;
type IntoIter = std::slice::Iter<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PolicySource {
UserManaged {
arn: String,
},
UserInline {
name: String,
},
GroupManaged {
group: String,
arn: String,
},
GroupInline {
group: String,
name: String,
},
RoleManaged {
role: String,
arn: String,
},
RoleInline {
role: String,
name: String,
},
PermissionsBoundary {
arn: String,
},
}
impl fmt::Display for PolicySource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UserManaged { arn } => write!(f, "user managed policy {arn}"),
Self::UserInline { name } => write!(f, "user inline policy {name}"),
Self::GroupManaged { group, arn } => {
write!(f, "managed policy {arn} of group {group}")
}
Self::GroupInline { group, name } => {
write!(f, "inline policy {name} of group {group}")
}
Self::RoleManaged { role, arn } => write!(f, "managed policy {arn} of role {role}"),
Self::RoleInline { role, name } => write!(f, "inline policy {name} of role {role}"),
Self::PermissionsBoundary { arn } => write!(f, "permissions boundary {arn}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct StatementRef {
pub policy: PolicySource,
pub sid: Option<String>,
pub index: usize,
}
impl fmt::Display for StatementRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.sid {
Some(sid) => write!(f, "statement `{sid}` of {}", self.policy),
None => write!(f, "statement #{} of {}", self.index, self.policy),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AllowReason {
RootBypass,
Statements(NonEmpty<StatementRef>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DenyReason {
NoMatch,
Statements(NonEmpty<StatementRef>),
BoundaryRestricted {
arn: String,
allowed_by: NonEmpty<StatementRef>,
denied_by: Vec<StatementRef>,
},
BoundaryMissing {
arn: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow(AllowReason),
Deny(DenyReason),
}
impl Decision {
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Allow(_))
}
}
impl fmt::Display for Decision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Allow(AllowReason::RootBypass) => write!(f, "allowed: caller is root"),
Self::Allow(AllowReason::Statements(refs)) => {
write!(f, "allowed by {}", refs.first())
}
Self::Deny(DenyReason::NoMatch) => {
write!(f, "denied: no statement matches this action and resource")
}
Self::Deny(DenyReason::Statements(refs)) => write!(f, "denied by {}", refs.first()),
Self::Deny(DenyReason::BoundaryRestricted { arn, denied_by, .. }) => {
match denied_by.first() {
Some(statement) => write!(
f,
"denied by {statement}: permissions boundary {arn} refuses this action"
),
None => write!(
f,
"denied: permissions boundary {arn} does not cover this action"
),
}
}
Self::Deny(DenyReason::BoundaryMissing { arn }) => {
write!(f, "denied: permissions boundary {arn} was not found")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn statement(policy: PolicySource, sid: Option<&str>) -> StatementRef {
StatementRef {
policy,
sid: sid.map(str::to_string),
index: 0,
}
}
#[test]
fn an_empty_list_cannot_become_non_empty() {
assert!(NonEmpty::<u8>::new(vec![]).is_err());
assert_eq!(NonEmpty::new(vec![1, 2]).unwrap().first(), &1);
}
#[test]
fn sources_sort_in_evaluation_order_not_store_order() {
let mut shuffled = vec![
PolicySource::RoleInline {
role: "r".into(),
name: "n".into(),
},
PolicySource::UserManaged { arn: "a".into() },
PolicySource::GroupInline {
group: "g".into(),
name: "n".into(),
},
PolicySource::UserInline { name: "n".into() },
];
shuffled.sort();
assert_eq!(
shuffled,
vec![
PolicySource::UserManaged { arn: "a".into() },
PolicySource::UserInline { name: "n".into() },
PolicySource::GroupInline {
group: "g".into(),
name: "n".into()
},
PolicySource::RoleInline {
role: "r".into(),
name: "n".into()
},
]
);
}
#[test]
fn a_denied_user_is_told_what_to_fix() {
let allowed = NonEmpty::one(statement(
PolicySource::UserManaged { arn: "p".into() },
Some("AllowAll"),
));
let uncovered = Decision::Deny(DenyReason::BoundaryRestricted {
arn: "b".into(),
allowed_by: allowed.clone(),
denied_by: vec![],
});
assert!(uncovered.to_string().contains("does not cover"));
let refused = Decision::Deny(DenyReason::BoundaryRestricted {
arn: "b".into(),
allowed_by: allowed,
denied_by: vec![statement(
PolicySource::PermissionsBoundary { arn: "b".into() },
Some("DenyWrites"),
)],
});
assert!(refused.to_string().contains("DenyWrites"));
assert!(refused.to_string().contains("refuses this action"));
}
#[test]
fn a_statement_without_a_sid_falls_back_to_its_index() {
let named = statement(PolicySource::UserInline { name: "p".into() }, Some("Sid1"));
assert!(named.to_string().contains("`Sid1`"));
let anonymous = StatementRef {
policy: PolicySource::UserInline { name: "p".into() },
sid: None,
index: 3,
};
assert!(anonymous.to_string().contains("#3"));
}
#[test]
fn every_source_renders_for_an_audit_line() {
let cases = [
(
PolicySource::UserManaged { arn: "a".into() },
"user managed policy a",
),
(
PolicySource::UserInline { name: "n".into() },
"user inline policy n",
),
(
PolicySource::GroupManaged {
group: "g".into(),
arn: "a".into(),
},
"managed policy a of group g",
),
(
PolicySource::GroupInline {
group: "g".into(),
name: "n".into(),
},
"inline policy n of group g",
),
(
PolicySource::RoleManaged {
role: "r".into(),
arn: "a".into(),
},
"managed policy a of role r",
),
(
PolicySource::RoleInline {
role: "r".into(),
name: "n".into(),
},
"inline policy n of role r",
),
(
PolicySource::PermissionsBoundary { arn: "b".into() },
"permissions boundary b",
),
];
for (source, expected) in cases {
assert_eq!(source.to_string(), expected);
}
}
#[test]
fn every_decision_renders_for_an_audit_line() {
let one = NonEmpty::one(statement(
PolicySource::UserInline { name: "p".into() },
Some("S"),
));
assert!(Decision::Allow(AllowReason::RootBypass)
.to_string()
.contains("caller is root"));
assert!(Decision::Allow(AllowReason::Statements(one.clone()))
.to_string()
.starts_with("allowed by"));
assert!(Decision::Deny(DenyReason::NoMatch)
.to_string()
.contains("no statement matches"));
assert!(Decision::Deny(DenyReason::Statements(one))
.to_string()
.starts_with("denied by"));
assert!(
Decision::Deny(DenyReason::BoundaryMissing { arn: "b".into() })
.to_string()
.contains("was not found")
);
}
#[test]
fn a_non_empty_list_behaves_like_the_slice_it_wraps() {
let items = NonEmpty::new(vec![1, 2, 3]).unwrap();
assert_eq!(items.len(), 3);
assert_eq!(items.iter().sum::<i32>(), 6);
assert_eq!((&items).into_iter().copied().max(), Some(3));
assert_eq!(items.clone().into_iter().count(), 3);
assert_eq!(items.into_vec(), vec![1, 2, 3]);
}
#[test]
fn is_allowed_matches_the_variant() {
assert!(Decision::Allow(AllowReason::RootBypass).is_allowed());
assert!(!Decision::Deny(DenyReason::NoMatch).is_allowed());
}
}