1mod index;
4
5use std::collections::HashMap;
6
7use tracing::debug;
8use typesec_core::{
9 ResourceId, SubjectId,
10 policy::{PolicyEngine, PolicyResult, RequestContext},
11};
12
13use crate::{
14 audit::{ConstraintEval, OdrlAuditEvent, OdrlVerdict},
15 constraint::{ConstraintContext, evaluate},
16 model::{OdrlDocument, OdrlRuleType},
17};
18use index::{
19 CompiledTarget, RuleIndex, RuleRef, WildcardActionIndex, build_rule_index, compile_targets,
20};
21
22struct RuleMatch {
23 policy_uid: String,
24 evals: Vec<ConstraintEval>,
25}
26
27struct ConstraintFailure {
29 policy_uid: String,
30 rule_type: OdrlRuleType,
31 evals: Vec<ConstraintEval>,
32}
33
34type ProhibitionMatch = (String, String, Vec<ConstraintEval>);
36
37struct ScanResult {
39 permission_matches: Vec<RuleMatch>,
40 prohibition_match: Option<ProhibitionMatch>,
41 constraint_failures: Vec<ConstraintFailure>,
42}
43
44pub struct OdrlEngine {
52 doc: OdrlDocument,
53 exact_rules: RuleIndex,
55 wildcard_action_rules: WildcardActionIndex,
57 compiled_targets: HashMap<(usize, usize), CompiledTarget>,
60 default_context: ConstraintContext,
62}
63
64impl OdrlEngine {
65 pub fn new(doc: OdrlDocument) -> Self {
67 let (exact_rules, wildcard_action_rules) = build_rule_index(&doc);
68 let compiled_targets = compile_targets(&doc);
69 Self {
70 doc,
71 exact_rules,
72 wildcard_action_rules,
73 compiled_targets,
74 default_context: ConstraintContext::default(),
75 }
76 }
77
78 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
80 let doc =
81 OdrlDocument::from_yaml(yaml).map_err(|e| format!("ODRL YAML parse error: {e}"))?;
82 Ok(Self::new(doc))
83 }
84
85 pub fn with_context(mut self, ctx: ConstraintContext) -> Self {
87 self.default_context = ctx;
88 self
89 }
90
91 pub fn check_with_context(
93 &self,
94 subject: &str,
95 action: &str,
96 resource: &str,
97 ctx: &ConstraintContext,
98 ) -> PolicyResult {
99 let (verdict, events) = self.decide(subject, action, resource, ctx);
100 for event in &events {
101 event.log();
102 }
103 verdict
104 }
105
106 fn decide(
110 &self,
111 subject: &str,
112 action: &str,
113 resource: &str,
114 ctx: &ConstraintContext,
115 ) -> (PolicyResult, Vec<OdrlAuditEvent>) {
116 let candidates = self.candidate_rules(subject, action);
117 debug!(
118 subject,
119 action,
120 resource,
121 n_candidates = candidates.len(),
122 "odrl check"
123 );
124
125 let scan = self.scan_candidates(&candidates, action, resource, ctx);
126 build_decision(scan, subject, action, resource)
127 }
128
129 fn scan_candidates(
132 &self,
133 candidates: &[RuleRef],
134 action: &str,
135 resource: &str,
136 ctx: &ConstraintContext,
137 ) -> ScanResult {
138 let mut permission_matches: Vec<RuleMatch> = Vec::new();
139 let mut prohibition_match: Option<ProhibitionMatch> = None;
140 let mut constraint_failures: Vec<ConstraintFailure> = Vec::new();
141
142 for rule_ref in candidates {
143 let policy = &self.doc.policies[rule_ref.policy_index];
144 let rule = &policy.rules[rule_ref.rule_index];
145
146 let target_matches = self
148 .compiled_targets
149 .get(&(rule_ref.policy_index, rule_ref.rule_index))
150 .is_some_and(|target| target.matches(resource));
151 if !target_matches {
152 continue;
153 }
154
155 let constraint_evals: Vec<ConstraintEval> = rule
157 .constraints
158 .iter()
159 .map(|c| ConstraintEval {
160 operand: c.left_operand.clone(),
161 passed: evaluate(c, ctx),
162 })
163 .collect();
164
165 let all_passed = constraint_evals.iter().all(|e| e.passed);
166
167 match rule.rule_type {
168 OdrlRuleType::Prohibition if all_passed => {
169 let reason = format!(
170 "prohibited by policy '{}' (action '{}' on '{}')",
171 policy.uid, action, resource
172 );
173 if prohibition_match.is_none() {
174 prohibition_match = Some((policy.uid.clone(), reason, constraint_evals));
175 }
176 }
179 OdrlRuleType::Permission if all_passed => {
180 permission_matches.push(RuleMatch {
181 policy_uid: policy.uid.clone(),
182 evals: constraint_evals,
183 });
184 }
186 OdrlRuleType::Duty => {
187 }
191 _ => {
195 constraint_failures.push(ConstraintFailure {
196 policy_uid: policy.uid.clone(),
197 rule_type: rule.rule_type,
198 evals: constraint_evals,
199 });
200 }
201 }
202 }
203
204 ScanResult {
205 permission_matches,
206 prohibition_match,
207 constraint_failures,
208 }
209 }
210
211 fn candidate_rules(&self, subject: &str, action: &str) -> Vec<RuleRef> {
212 let mut candidates = Vec::new();
213
214 if let Some(exact) = self
215 .exact_rules
216 .get(&(subject.to_owned(), action.to_owned()))
217 {
218 candidates.extend_from_slice(exact);
219 }
220
221 if let Some(wildcard) = self.wildcard_action_rules.get(subject) {
222 candidates.extend_from_slice(wildcard);
223 }
224
225 if candidates.len() > 1 {
226 candidates.sort_by_key(|rule_ref| rule_ref.ordinal);
227 }
228
229 candidates
230 }
231}
232
233fn build_decision(
239 scan: ScanResult,
240 subject: &str,
241 action: &str,
242 resource: &str,
243) -> (PolicyResult, Vec<OdrlAuditEvent>) {
244 let ScanResult {
245 permission_matches,
246 prohibition_match,
247 constraint_failures,
248 } = scan;
249 let mut events = Vec::new();
250
251 let event_for = |policy_uid, matched_rule, verdict, constraint_results| OdrlAuditEvent {
252 policy_uid,
253 matched_rule,
254 subject: subject.to_owned(),
255 action: action.to_owned(),
256 target: resource.to_owned(),
257 verdict,
258 constraint_results,
259 };
260
261 for failure in constraint_failures {
264 let failed: Vec<String> = failure
265 .evals
266 .iter()
267 .filter(|e| !e.passed)
268 .map(|e| e.operand.to_string())
269 .collect();
270 events.push(event_for(
271 failure.policy_uid,
272 Some(failure.rule_type),
273 OdrlVerdict::ConstraintFailed {
274 constraint: failed.join(", "),
275 },
276 failure.evals,
277 ));
278 }
279
280 if let Some((policy_uid, reason, evals)) = prohibition_match {
282 for permission_match in permission_matches {
283 events.push(event_for(
284 permission_match.policy_uid,
285 Some(OdrlRuleType::Permission),
286 OdrlVerdict::Overridden {
287 by_policy: policy_uid.clone(),
288 reason: reason.clone(),
289 },
290 permission_match.evals,
291 ));
292 }
293 events.push(event_for(
294 policy_uid,
295 Some(OdrlRuleType::Prohibition),
296 OdrlVerdict::Prohibited {
297 reason: reason.clone(),
298 },
299 evals,
300 ));
301 return (PolicyResult::Deny(reason), events);
302 }
303
304 if !permission_matches.is_empty() {
305 for permission_match in permission_matches {
308 events.push(event_for(
309 permission_match.policy_uid,
310 Some(OdrlRuleType::Permission),
311 OdrlVerdict::Permitted,
312 permission_match.evals,
313 ));
314 }
315 return (PolicyResult::Allow, events);
316 }
317
318 events.push(event_for(
320 "<none>".to_owned(),
321 None,
322 OdrlVerdict::NotApplicable,
323 vec![],
324 ));
325 (
326 PolicyResult::delegate("odrl", "no matching ODRL rule"),
327 events,
328 )
329}
330
331impl PolicyEngine for OdrlEngine {
332 fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
333 self.check_with_context(
334 subject.as_str(),
335 action,
336 resource.as_str(),
337 &self.default_context,
338 )
339 }
340
341 fn check_with_context(
342 &self,
343 subject: &SubjectId,
344 action: &str,
345 resource: &ResourceId,
346 ctx: &RequestContext,
347 ) -> PolicyResult {
348 let ctx = ConstraintContext::from(ctx);
349 self.check_with_context(subject.as_str(), action, resource.as_str(), &ctx)
350 }
351}
352
353#[cfg(test)]
354mod tests;