auth_policy/
policy.rs

1use crate::{
2    decision::Effect,
3    error::{Error, Result},
4    request::Request,
5};
6
7#[derive(Debug, Clone)]
8pub struct Policy {
9    id: String,
10    target: Target,
11    conditions: Vec<Condition>,
12    effect: Effect,
13}
14
15impl Policy {
16    pub fn builder(id: impl Into<String>) -> PolicyBuilder {
17        PolicyBuilder {
18            id: id.into(),
19            target: None,
20            conditions: Vec::new(),
21            effect: None,
22        }
23    }
24
25    pub fn id(&self) -> &str {
26        &self.id
27    }
28
29    pub(crate) fn evaluate(&self, request: &Request) -> Result<Option<Effect>> {
30        if !self.target.matches(request) {
31            return Ok(None);
32        }
33
34        for condition in &self.conditions {
35            if !condition.is_satisfied(request)? {
36                return Ok(None);
37            }
38        }
39
40        Ok(Some(self.effect))
41    }
42}
43
44pub struct PolicyBuilder {
45    id: String,
46    target: Option<Target>,
47    conditions: Vec<Condition>,
48    effect: Option<Effect>,
49}
50
51impl PolicyBuilder {
52    pub fn target(mut self, target: Target) -> Self {
53        self.target = Some(target);
54        self
55    }
56
57    pub fn condition(mut self, condition: Condition) -> Self {
58        self.conditions.push(condition);
59        self
60    }
61
62    pub fn effect(mut self, effect: Effect) -> Self {
63        self.effect = Some(effect);
64        self
65    }
66
67    pub fn build(self) -> Result<Policy> {
68        let target = self.target.unwrap_or_else(Target::any);
69        let effect = self
70            .effect
71            .ok_or_else(|| Error::InvalidPolicy("effect must be specified".into()))?;
72
73        Ok(Policy {
74            id: self.id,
75            target,
76            conditions: self.conditions,
77            effect,
78        })
79    }
80}
81
82#[derive(Debug, Clone)]
83pub enum Target {
84    Any,
85    Action(String),
86}
87
88impl Target {
89    pub fn any() -> Self {
90        Target::Any
91    }
92
93    pub fn action(action: impl Into<String>) -> Self {
94        Target::Action(action.into())
95    }
96
97    pub(crate) fn matches(&self, request: &Request) -> bool {
98        match self {
99            Target::Any => true,
100            Target::Action(expected) => request.action_name() == expected,
101        }
102    }
103}
104
105#[derive(Debug, Clone)]
106pub enum Condition {
107    Equals(String, String),
108}
109
110impl Condition {
111    pub fn equals(left: impl Into<String>, right: impl Into<String>) -> Self {
112        Condition::Equals(left.into(), right.into())
113    }
114
115    pub(crate) fn is_satisfied(&self, request: &Request) -> Result<bool> {
116        match self {
117            Condition::Equals(lhs, rhs) => {
118                let left = resolve_operand(request, lhs)?;
119                let right = resolve_operand(request, rhs)?;
120                Ok(left == right)
121            }
122        }
123    }
124}
125
126fn resolve_operand(request: &Request, operand: &str) -> Result<String> {
127    if let Some((namespace, key)) = parse_path(operand) {
128        request
129            .lookup(namespace, key.as_deref())
130            .map(str::to_owned)
131            .ok_or_else(|| Error::MissingAttribute(format!("{namespace}{}", key_to_suffix(&key))))
132    } else {
133        Ok(operand.to_owned())
134    }
135}
136
137fn parse_path(raw: &str) -> Option<(&str, Option<String>)> {
138    if let Some((namespace, key)) = raw.split_once('.') {
139        Some((namespace, Some(key.to_string())))
140    } else {
141        match raw {
142            "actor" | "resource" | "environment" | "action" => {
143                Some((raw, None))
144            }
145            _ => None,
146        }
147    }
148}
149
150fn key_to_suffix(key: &Option<String>) -> String {
151    key.as_ref()
152        .map(|k| format!(".{k}"))
153        .unwrap_or_default()
154}