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()
}