mod index;
use tracing::debug;
use typesec_core::{
ResourceId, SubjectId,
policy::{PolicyEngine, PolicyResult, RequestContext},
};
use crate::{
audit::{ConstraintEval, OdrlAuditEvent, OdrlVerdict},
constraint::{ConstraintContext, evaluate},
model::{OdrlDocument, OdrlRuleType},
};
use index::{RuleIndex, RuleRef, WildcardActionIndex, build_rule_index, target_matches};
struct RuleMatch {
policy_uid: String,
evals: Vec<ConstraintEval>,
}
struct ConstraintFailure {
policy_uid: String,
rule_type: OdrlRuleType,
evals: Vec<ConstraintEval>,
}
type ProhibitionMatch = (String, String, Vec<ConstraintEval>);
struct ScanResult {
permission_matches: Vec<RuleMatch>,
prohibition_match: Option<ProhibitionMatch>,
constraint_failures: Vec<ConstraintFailure>,
}
pub struct OdrlEngine {
doc: OdrlDocument,
exact_rules: RuleIndex,
wildcard_action_rules: WildcardActionIndex,
default_context: ConstraintContext,
}
impl OdrlEngine {
pub fn new(doc: OdrlDocument) -> Self {
let (exact_rules, wildcard_action_rules) = build_rule_index(&doc);
Self {
doc,
exact_rules,
wildcard_action_rules,
default_context: ConstraintContext::default(),
}
}
pub fn from_yaml(yaml: &str) -> Result<Self, String> {
let doc =
OdrlDocument::from_yaml(yaml).map_err(|e| format!("ODRL YAML parse error: {e}"))?;
Ok(Self::new(doc))
}
pub fn with_context(mut self, ctx: ConstraintContext) -> Self {
self.default_context = ctx;
self
}
pub fn check_with_context(
&self,
subject: &str,
action: &str,
resource: &str,
ctx: &ConstraintContext,
) -> PolicyResult {
let (verdict, events) = self.decide(subject, action, resource, ctx);
for event in &events {
event.log();
}
verdict
}
fn decide(
&self,
subject: &str,
action: &str,
resource: &str,
ctx: &ConstraintContext,
) -> (PolicyResult, Vec<OdrlAuditEvent>) {
let candidates = self.candidate_rules(subject, action);
debug!(
subject,
action,
resource,
n_candidates = candidates.len(),
"odrl check"
);
let scan = self.scan_candidates(&candidates, action, resource, ctx);
build_decision(scan, subject, action, resource)
}
fn scan_candidates(
&self,
candidates: &[RuleRef],
action: &str,
resource: &str,
ctx: &ConstraintContext,
) -> ScanResult {
let mut permission_matches: Vec<RuleMatch> = Vec::new();
let mut prohibition_match: Option<ProhibitionMatch> = None;
let mut constraint_failures: Vec<ConstraintFailure> = Vec::new();
for rule_ref in candidates {
let policy = &self.doc.policies[rule_ref.policy_index];
let rule = &policy.rules[rule_ref.rule_index];
if !target_matches(&rule.target, resource) {
continue;
}
let constraint_evals: Vec<ConstraintEval> = rule
.constraints
.iter()
.map(|c| ConstraintEval {
operand: c.left_operand.clone(),
passed: evaluate(c, ctx),
})
.collect();
let all_passed = constraint_evals.iter().all(|e| e.passed);
match rule.rule_type {
OdrlRuleType::Prohibition if all_passed => {
let reason = format!(
"prohibited by policy '{}' (action '{}' on '{}')",
policy.uid, action, resource
);
if prohibition_match.is_none() {
prohibition_match = Some((policy.uid.clone(), reason, constraint_evals));
}
}
OdrlRuleType::Permission if all_passed => {
permission_matches.push(RuleMatch {
policy_uid: policy.uid.clone(),
evals: constraint_evals,
});
}
OdrlRuleType::Duty => {
}
_ => {
constraint_failures.push(ConstraintFailure {
policy_uid: policy.uid.clone(),
rule_type: rule.rule_type,
evals: constraint_evals,
});
}
}
}
ScanResult {
permission_matches,
prohibition_match,
constraint_failures,
}
}
fn candidate_rules(&self, subject: &str, action: &str) -> Vec<RuleRef> {
let mut candidates = Vec::new();
if let Some(exact) = self
.exact_rules
.get(&(subject.to_owned(), action.to_owned()))
{
candidates.extend_from_slice(exact);
}
if let Some(wildcard) = self.wildcard_action_rules.get(subject) {
candidates.extend_from_slice(wildcard);
}
if candidates.len() > 1 {
candidates.sort_by_key(|rule_ref| rule_ref.ordinal);
}
candidates
}
}
fn build_decision(
scan: ScanResult,
subject: &str,
action: &str,
resource: &str,
) -> (PolicyResult, Vec<OdrlAuditEvent>) {
let ScanResult {
permission_matches,
prohibition_match,
constraint_failures,
} = scan;
let mut events = Vec::new();
let event_for = |policy_uid, matched_rule, verdict, constraint_results| OdrlAuditEvent {
policy_uid,
matched_rule,
subject: subject.to_owned(),
action: action.to_owned(),
target: resource.to_owned(),
verdict,
constraint_results,
};
for failure in constraint_failures {
let failed: Vec<String> = failure
.evals
.iter()
.filter(|e| !e.passed)
.map(|e| e.operand.to_string())
.collect();
events.push(event_for(
failure.policy_uid,
Some(failure.rule_type),
OdrlVerdict::ConstraintFailed {
constraint: failed.join(", "),
},
failure.evals,
));
}
if let Some((policy_uid, reason, evals)) = prohibition_match {
for permission_match in permission_matches {
events.push(event_for(
permission_match.policy_uid,
Some(OdrlRuleType::Permission),
OdrlVerdict::Overridden {
by_policy: policy_uid.clone(),
reason: reason.clone(),
},
permission_match.evals,
));
}
events.push(event_for(
policy_uid,
Some(OdrlRuleType::Prohibition),
OdrlVerdict::Prohibited {
reason: reason.clone(),
},
evals,
));
return (PolicyResult::Deny(reason), events);
}
if !permission_matches.is_empty() {
for permission_match in permission_matches {
events.push(event_for(
permission_match.policy_uid,
Some(OdrlRuleType::Permission),
OdrlVerdict::Permitted,
permission_match.evals,
));
}
return (PolicyResult::Allow, events);
}
events.push(event_for(
"<none>".to_owned(),
None,
OdrlVerdict::NotApplicable,
vec![],
));
(
PolicyResult::delegate("odrl", "no matching ODRL rule"),
events,
)
}
impl PolicyEngine for OdrlEngine {
fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
self.check_with_context(
subject.as_str(),
action,
resource.as_str(),
&self.default_context,
)
}
fn check_with_context(
&self,
subject: &SubjectId,
action: &str,
resource: &ResourceId,
ctx: &RequestContext,
) -> PolicyResult {
let ctx = ConstraintContext::from(ctx);
self.check_with_context(subject.as_str(), action, resource.as_str(), &ctx)
}
}
#[cfg(test)]
mod tests;