Skip to main content

actrpc_interceptor/interceptors/policy/
engine.rs

1use crate::interceptors::policy::{
2    config::{MatchExpr, PolicyApply, PolicyCondition, PolicyConfig, PolicyEffect, PolicyRule},
3    error::PolicyError,
4    fact::PolicyFacts,
5    matcher::CompiledPolicyMatcher,
6};
7use actrpc_core::interception::InterceptionRequest;
8use std::collections::HashSet;
9
10#[derive(Debug, Clone)]
11pub struct PolicyEngine {
12    rules: Vec<CompiledPolicyRule>,
13}
14
15#[derive(Debug, Clone)]
16struct CompiledPolicyRule {
17    name: String,
18    match_expr: CompiledMatchExpr,
19    apply: PolicyApply,
20}
21
22#[derive(Debug, Clone)]
23enum CompiledMatchExpr {
24    All(Vec<CompiledMatchExpr>),
25    Any(Vec<CompiledMatchExpr>),
26    Condition(CompiledPolicyCondition),
27}
28
29#[derive(Debug, Clone)]
30struct CompiledPolicyCondition {
31    condition: PolicyCondition,
32    matcher: CompiledPolicyMatcher,
33}
34
35#[derive(Debug, Clone, Default)]
36pub struct PolicyDecision {
37    pub matched_rules: Vec<MatchedPolicyRule>,
38}
39
40#[derive(Debug, Clone)]
41pub struct MatchedPolicyRule {
42    pub name: String,
43    pub apply: PolicyApply,
44}
45
46impl PolicyEngine {
47    pub fn compile(config: PolicyConfig) -> Result<Self, PolicyError> {
48        validate_config(&config)?;
49
50        let mut rules = Vec::with_capacity(config.rules.len());
51
52        for rule in config.rules {
53            rules.push(CompiledPolicyRule {
54                name: rule.name.clone(),
55                match_expr: compile_match_expr(&rule.name, rule.match_expr)?,
56                apply: rule.apply,
57            });
58        }
59
60        Ok(Self { rules })
61    }
62
63    pub fn evaluate(&self, request: &InterceptionRequest) -> PolicyDecision {
64        let facts = PolicyFacts::new(request);
65        let mut decision = PolicyDecision::default();
66
67        for rule in &self.rules {
68            if !rule.match_expr.matches(&facts) {
69                continue;
70            }
71
72            decision.matched_rules.push(MatchedPolicyRule {
73                name: rule.name.clone(),
74                apply: rule.apply.clone(),
75            });
76        }
77
78        decision
79    }
80}
81
82impl CompiledMatchExpr {
83    fn matches(&self, facts: &PolicyFacts<'_>) -> bool {
84        match self {
85            CompiledMatchExpr::All(expressions) => expressions
86                .iter()
87                .all(|expression| expression.matches(facts)),
88
89            CompiledMatchExpr::Any(expressions) => expressions
90                .iter()
91                .any(|expression| expression.matches(facts)),
92
93            CompiledMatchExpr::Condition(condition) => {
94                let value = facts.get_string(&condition.condition.fact);
95                condition.matcher.matches(value.as_deref())
96            }
97        }
98    }
99}
100
101fn compile_match_expr(rule: &str, expr: MatchExpr) -> Result<CompiledMatchExpr, PolicyError> {
102    match expr {
103        MatchExpr::All { all } => Ok(CompiledMatchExpr::All(
104            all.into_iter()
105                .map(|expr| compile_match_expr(rule, expr))
106                .collect::<Result<Vec<_>, _>>()?,
107        )),
108
109        MatchExpr::Any { any } => Ok(CompiledMatchExpr::Any(
110            any.into_iter()
111                .map(|expr| compile_match_expr(rule, expr))
112                .collect::<Result<Vec<_>, _>>()?,
113        )),
114
115        MatchExpr::Condition { condition } => {
116            let matcher = CompiledPolicyMatcher::compile(rule, &condition.matcher)?;
117
118            Ok(CompiledMatchExpr::Condition(CompiledPolicyCondition {
119                condition,
120                matcher,
121            }))
122        }
123    }
124}
125
126fn validate_config(config: &PolicyConfig) -> Result<(), PolicyError> {
127    let mut names = HashSet::new();
128
129    for rule in &config.rules {
130        validate_rule(rule)?;
131
132        if !names.insert(rule.name.clone()) {
133            return Err(PolicyError::DuplicateRuleName {
134                name: rule.name.clone(),
135            });
136        }
137    }
138
139    Ok(())
140}
141
142fn validate_rule(rule: &PolicyRule) -> Result<(), PolicyError> {
143    if rule.name.trim().is_empty() {
144        return Err(PolicyError::InvalidConfig {
145            message: "policy rule name cannot be empty".to_owned(),
146        });
147    }
148
149    validate_match_expr(&rule.name, &rule.match_expr)?;
150
151    if rule.apply.immediate.is_empty() && rule.apply.review.is_none() {
152        return Err(PolicyError::InvalidConfig {
153            message: format!("policy rule {} has no effects", rule.name),
154        });
155    }
156
157    for effect in &rule.apply.immediate {
158        validate_effect(&rule.name, effect)?;
159    }
160
161    if let Some(review) = &rule.apply.review {
162        for effect in &review.on_approve {
163            validate_effect(&rule.name, effect)?;
164        }
165
166        for effect in &review.on_deny {
167            validate_effect(&rule.name, effect)?;
168        }
169    }
170
171    Ok(())
172}
173
174fn validate_match_expr(rule: &str, expr: &MatchExpr) -> Result<(), PolicyError> {
175    match expr {
176        MatchExpr::All { all } => {
177            if all.is_empty() {
178                return Err(PolicyError::InvalidConfig {
179                    message: format!("policy rule {rule} contains empty all expression"),
180                });
181            }
182
183            for expr in all {
184                validate_match_expr(rule, expr)?;
185            }
186        }
187
188        MatchExpr::Any { any } => {
189            if any.is_empty() {
190                return Err(PolicyError::InvalidConfig {
191                    message: format!("policy rule {rule} contains empty any expression"),
192                });
193            }
194
195            for expr in any {
196                validate_match_expr(rule, expr)?;
197            }
198        }
199
200        MatchExpr::Condition { condition } => {
201            if condition.fact.as_str().trim().is_empty() {
202                return Err(PolicyError::InvalidConfig {
203                    message: format!("policy rule {rule} has empty fact path"),
204                });
205            }
206        }
207    }
208
209    Ok(())
210}
211
212fn validate_effect(rule: &str, effect: &PolicyEffect) -> Result<(), PolicyError> {
213    match effect {
214        PolicyEffect::ExcludeInterceptors {
215            exclude_interceptors,
216        } => {
217            if exclude_interceptors.names.is_empty() {
218                return Err(PolicyError::InvalidConfig {
219                    message: format!(
220                        "policy rule {rule} has exclude_interceptors effect with empty names"
221                    ),
222                });
223            }
224        }
225
226        PolicyEffect::RejectCall { .. } => {}
227    }
228
229    Ok(())
230}