typesec_odrl/
constraint.rs1use chrono::{DateTime, Utc};
7use tracing::debug;
8use typesec_core::policy::RequestContext;
9
10use crate::model::{ConstraintOperand, ConstraintOperator, OdrlConstraint};
11
12#[derive(Debug, Clone, Default)]
17pub struct ConstraintContext {
18 pub purpose: Option<String>,
20 pub now: Option<DateTime<Utc>>,
22 pub custom: std::collections::HashMap<String, String>,
24}
25
26impl ConstraintContext {
27 pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
29 self.purpose = Some(purpose.into());
30 self
31 }
32
33 pub fn with_time(mut self, t: DateTime<Utc>) -> Self {
35 self.now = Some(t);
36 self
37 }
38
39 pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
41 self.custom.insert(key.into(), value.into());
42 self
43 }
44
45 fn effective_now(&self) -> DateTime<Utc> {
46 self.now.unwrap_or_else(Utc::now)
47 }
48}
49
50impl From<&RequestContext> for ConstraintContext {
51 fn from(ctx: &RequestContext) -> Self {
52 Self {
53 purpose: ctx.purpose.clone(),
54 now: None,
55 custom: ctx.custom.clone(),
56 }
57 }
58}
59
60pub fn evaluate(constraint: &OdrlConstraint, ctx: &ConstraintContext) -> bool {
64 debug!(
65 left = %constraint.left_operand,
66 op = ?constraint.operator,
67 right = %constraint.right_operand,
68 "evaluating constraint"
69 );
70
71 match &constraint.left_operand {
72 ConstraintOperand::Purpose => evaluate_purpose(constraint, ctx),
73 ConstraintOperand::DateTime => evaluate_datetime(constraint, ctx),
74 ConstraintOperand::Count => evaluate_custom_operand("count", constraint, ctx),
75 ConstraintOperand::Custom(name) => evaluate_custom_operand(name, constraint, ctx),
76 }
77}
78
79fn evaluate_custom_operand(
80 operand: &str,
81 constraint: &OdrlConstraint,
82 ctx: &ConstraintContext,
83) -> bool {
84 if let Some(val) = ctx.custom.get(operand) {
85 evaluate_string_op(&constraint.operator, val, &constraint.right_operand)
86 } else {
87 debug!("unknown constraint left operand '{operand}' — failing closed");
88 false
89 }
90}
91
92fn evaluate_purpose(constraint: &OdrlConstraint, ctx: &ConstraintContext) -> bool {
93 let actual = match ctx.purpose.as_deref() {
94 Some(p) => p,
95 None => return false,
96 };
97 evaluate_string_op(&constraint.operator, actual, &constraint.right_operand)
98}
99
100fn evaluate_datetime(constraint: &OdrlConstraint, ctx: &ConstraintContext) -> bool {
101 let now = ctx.effective_now();
102
103 let rhs = match constraint.right_operand.parse::<DateTime<Utc>>() {
104 Ok(dt) => dt,
105 Err(e) => {
106 debug!(
107 "could not parse dateTime '{}': {e}",
108 constraint.right_operand
109 );
110 return false;
111 }
112 };
113
114 match constraint.operator {
115 ConstraintOperator::Lt => now < rhs,
116 ConstraintOperator::Lteq => now <= rhs,
117 ConstraintOperator::Gt => now > rhs,
118 ConstraintOperator::Gteq => now >= rhs,
119 ConstraintOperator::Eq => now == rhs,
120 ConstraintOperator::Neq => now != rhs,
121 ConstraintOperator::IsPartOf => false, }
123}
124
125fn evaluate_string_op(op: &ConstraintOperator, actual: &str, expected: &str) -> bool {
126 match op {
127 ConstraintOperator::Eq => actual == expected,
128 ConstraintOperator::Neq => actual != expected,
129 ConstraintOperator::IsPartOf => expected.split(',').any(|v| v.trim() == actual),
130 ConstraintOperator::Lt
135 | ConstraintOperator::Lteq
136 | ConstraintOperator::Gt
137 | ConstraintOperator::Gteq => match (actual.parse::<f64>(), expected.parse::<f64>()) {
138 (Ok(a), Ok(b)) => apply_ordering(op, &a, &b),
139 _ => apply_ordering(op, &actual, &expected),
140 },
141 }
142}
143
144fn apply_ordering<T: PartialOrd>(op: &ConstraintOperator, a: &T, b: &T) -> bool {
147 match op {
148 ConstraintOperator::Lt => a < b,
149 ConstraintOperator::Lteq => a <= b,
150 ConstraintOperator::Gt => a > b,
151 ConstraintOperator::Gteq => a >= b,
152 _ => false,
153 }
154}
155
156#[cfg(test)]
157mod tests;