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