auth-policy 0.0.2

Rust crate for evaluating authorization decisions against declarative policies
Documentation
use crate::{
    decision::Effect,
    error::{Error, Result},
    request::Request,
};

#[derive(Debug, Clone)]
pub struct Policy {
    id: String,
    target: Target,
    conditions: Vec<Condition>,
    effect: Effect,
}

impl Policy {
    pub fn builder(id: impl Into<String>) -> PolicyBuilder {
        PolicyBuilder {
            id: id.into(),
            target: None,
            conditions: Vec::new(),
            effect: None,
        }
    }

    pub fn id(&self) -> &str {
        &self.id
    }

    pub(crate) fn evaluate(&self, request: &Request) -> Result<Option<Effect>> {
        if !self.target.matches(request) {
            return Ok(None);
        }

        for condition in &self.conditions {
            if !condition.is_satisfied(request)? {
                return Ok(None);
            }
        }

        Ok(Some(self.effect))
    }
}

pub struct PolicyBuilder {
    id: String,
    target: Option<Target>,
    conditions: Vec<Condition>,
    effect: Option<Effect>,
}

impl PolicyBuilder {
    pub fn target(mut self, target: Target) -> Self {
        self.target = Some(target);
        self
    }

    pub fn condition(mut self, condition: Condition) -> Self {
        self.conditions.push(condition);
        self
    }

    pub fn effect(mut self, effect: Effect) -> Self {
        self.effect = Some(effect);
        self
    }

    pub fn build(self) -> Result<Policy> {
        let target = self.target.unwrap_or_else(Target::any);
        let effect = self
            .effect
            .ok_or_else(|| Error::InvalidPolicy("effect must be specified".into()))?;

        Ok(Policy {
            id: self.id,
            target,
            conditions: self.conditions,
            effect,
        })
    }
}

#[derive(Debug, Clone)]
pub enum Target {
    Any,
    Action(String),
}

impl Target {
    pub fn any() -> Self {
        Target::Any
    }

    pub fn action(action: impl Into<String>) -> Self {
        Target::Action(action.into())
    }

    pub(crate) fn matches(&self, request: &Request) -> bool {
        match self {
            Target::Any => true,
            Target::Action(expected) => request.action_name() == expected,
        }
    }
}

#[derive(Debug, Clone)]
pub enum Condition {
    Equals(String, String),
}

impl Condition {
    pub fn equals(left: impl Into<String>, right: impl Into<String>) -> Self {
        Condition::Equals(left.into(), right.into())
    }

    pub(crate) fn is_satisfied(&self, request: &Request) -> Result<bool> {
        match self {
            Condition::Equals(lhs, rhs) => {
                let left = resolve_operand(request, lhs)?;
                let right = resolve_operand(request, rhs)?;
                Ok(left == right)
            }
        }
    }
}

fn resolve_operand(request: &Request, operand: &str) -> Result<String> {
    if let Some((namespace, key)) = parse_path(operand) {
        request
            .lookup(namespace, key.as_deref())
            .map(str::to_owned)
            .ok_or_else(|| Error::MissingAttribute(format!("{namespace}{}", key_to_suffix(&key))))
    } else {
        Ok(operand.to_owned())
    }
}

fn parse_path(raw: &str) -> Option<(&str, Option<String>)> {
    if let Some((namespace, key)) = raw.split_once('.') {
        Some((namespace, Some(key.to_string())))
    } else {
        match raw {
            "actor" | "resource" | "environment" | "action" => {
                Some((raw, None))
            }
            _ => None,
        }
    }
}

fn key_to_suffix(key: &Option<String>) -> String {
    key.as_ref()
        .map(|k| format!(".{k}"))
        .unwrap_or_default()
}