use std::fmt;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Effect {
Grant,
Deny,
}
impl fmt::Display for Effect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Effect::Grant => write!(f, "grant"),
Effect::Deny => write!(f, "deny"),
}
}
}
impl std::str::FromStr for Effect {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"grant" => Ok(Effect::Grant),
"deny" => Ok(Effect::Deny),
_ => Err(anyhow::anyhow!("Invalid effect: {}", s)),
}
}
}
#[derive(Debug, Clone)]
pub struct ResourcePattern {
segments: Vec<String>,
}
impl ResourcePattern {
pub fn parse(pattern: &str) -> Self {
Self {
segments: pattern.split(':').map(|s| s.to_string()).collect(),
}
}
fn specificity(&self) -> usize {
self.segments.iter().filter(|s| *s != "*").count()
}
fn matches(&self, resource: &ResourcePattern) -> bool {
if self.segments.len() != resource.segments.len() {
return false;
}
self.segments
.iter()
.zip(resource.segments.iter())
.all(|(pattern_seg, resource_seg)| pattern_seg == "*" || pattern_seg == resource_seg)
}
}
impl fmt::Display for ResourcePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.segments.join(":"))
}
}
#[derive(Debug, Clone)]
pub struct Permission {
pub effect: Effect,
pub resource: ResourcePattern,
}
impl Permission {
pub fn new(effect: Effect, resource: &str) -> Self {
Self {
effect,
resource: ResourcePattern::parse(resource),
}
}
pub fn grant(resource: &str) -> Self {
Self::new(Effect::Grant, resource)
}
pub fn deny(resource: &str) -> Self {
Self::new(Effect::Deny, resource)
}
}
pub fn evaluate(permissions: &[Permission], resource: &str) -> bool {
let resource_pattern = ResourcePattern::parse(resource);
let matching: Vec<&Permission> = permissions
.iter()
.filter(|p| p.resource.matches(&resource_pattern))
.collect();
if matching.is_empty() {
return false; }
let max_specificity = matching
.iter()
.map(|p| p.resource.specificity())
.max()
.unwrap();
let most_specific: Vec<&&Permission> = matching
.iter()
.filter(|p| p.resource.specificity() == max_specificity)
.collect();
let has_deny = most_specific.iter().any(|p| p.effect == Effect::Deny);
if has_deny {
return false;
}
true
}