actrpc_interceptor/interceptors/policy/
engine.rs1use 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}