typesec_odrl/
constraint.rs1use chrono::{DateTime, Utc};
7use tracing::debug;
8
9use crate::model::{ConstraintOperator, OdrlConstraint};
10
11#[derive(Debug, Clone, Default)]
16pub struct ConstraintContext {
17 pub purpose: Option<String>,
19 pub now: Option<DateTime<Utc>>,
21 pub custom: std::collections::HashMap<String, String>,
23}
24
25impl ConstraintContext {
26 pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
28 self.purpose = Some(purpose.into());
29 self
30 }
31
32 pub fn with_time(mut self, t: DateTime<Utc>) -> Self {
34 self.now = Some(t);
35 self
36 }
37
38 pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
40 self.custom.insert(key.into(), value.into());
41 self
42 }
43
44 fn effective_now(&self) -> DateTime<Utc> {
45 self.now.unwrap_or_else(Utc::now)
46 }
47}
48
49pub fn evaluate(constraint: &OdrlConstraint, ctx: &ConstraintContext) -> bool {
53 debug!(
54 left = %constraint.left_operand,
55 op = ?constraint.operator,
56 right = %constraint.right_operand,
57 "evaluating constraint"
58 );
59
60 match constraint.left_operand.as_str() {
61 "purpose" => evaluate_purpose(constraint, ctx),
62 "dateTime" => evaluate_datetime(constraint, ctx),
63 "date" => evaluate_datetime(constraint, ctx),
64 other => {
65 if let Some(val) = ctx.custom.get(other) {
67 evaluate_string_op(&constraint.operator, val, &constraint.right_operand)
68 } else {
69 debug!("unknown constraint left operand '{other}' — failing closed");
71 false
72 }
73 }
74 }
75}
76
77fn evaluate_purpose(constraint: &OdrlConstraint, ctx: &ConstraintContext) -> bool {
78 let actual = match ctx.purpose.as_deref() {
79 Some(p) => p,
80 None => return false,
81 };
82 evaluate_string_op(&constraint.operator, actual, &constraint.right_operand)
83}
84
85fn evaluate_datetime(constraint: &OdrlConstraint, ctx: &ConstraintContext) -> bool {
86 let now = ctx.effective_now();
87
88 let rhs = match constraint.right_operand.parse::<DateTime<Utc>>() {
89 Ok(dt) => dt,
90 Err(e) => {
91 debug!(
92 "could not parse dateTime '{}': {e}",
93 constraint.right_operand
94 );
95 return false;
96 }
97 };
98
99 match constraint.operator {
100 ConstraintOperator::Lt => now < rhs,
101 ConstraintOperator::Lteq => now <= rhs,
102 ConstraintOperator::Gt => now > rhs,
103 ConstraintOperator::Gteq => now >= rhs,
104 ConstraintOperator::Eq => now == rhs,
105 ConstraintOperator::Neq => now != rhs,
106 ConstraintOperator::IsPartOf => false, }
108}
109
110fn evaluate_string_op(op: &ConstraintOperator, actual: &str, expected: &str) -> bool {
111 match op {
112 ConstraintOperator::Eq => actual == expected,
113 ConstraintOperator::Neq => actual != expected,
114 ConstraintOperator::IsPartOf => expected.split(',').any(|v| v.trim() == actual),
115 ConstraintOperator::Lt => actual < expected,
117 ConstraintOperator::Lteq => actual <= expected,
118 ConstraintOperator::Gt => actual > expected,
119 ConstraintOperator::Gteq => actual >= expected,
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::model::{ConstraintOperator, OdrlConstraint};
127
128 fn make_constraint(left: &str, op: ConstraintOperator, right: &str) -> OdrlConstraint {
129 OdrlConstraint {
130 left_operand: left.into(),
131 operator: op,
132 right_operand: right.into(),
133 }
134 }
135
136 #[test]
137 fn purpose_eq_passes() {
138 let c = make_constraint("purpose", ConstraintOperator::Eq, "analytics");
139 let ctx = ConstraintContext::default().with_purpose("analytics");
140 assert!(evaluate(&c, &ctx));
141 }
142
143 #[test]
144 fn purpose_eq_fails_wrong_value() {
145 let c = make_constraint("purpose", ConstraintOperator::Eq, "analytics");
146 let ctx = ConstraintContext::default().with_purpose("billing");
147 assert!(!evaluate(&c, &ctx));
148 }
149
150 #[test]
151 fn purpose_is_part_of() {
152 let c = make_constraint(
153 "purpose",
154 ConstraintOperator::IsPartOf,
155 "analytics, audit, reporting",
156 );
157 let ctx = ConstraintContext::default().with_purpose("audit");
158 assert!(evaluate(&c, &ctx));
159 }
160
161 #[test]
162 fn datetime_lt_passes_when_before() {
163 let future = "2099-01-01T00:00:00Z";
164 let c = make_constraint("dateTime", ConstraintOperator::Lt, future);
165 let ctx = ConstraintContext::default();
166 assert!(evaluate(&c, &ctx));
167 }
168
169 #[test]
170 fn datetime_lt_fails_when_past() {
171 let past = "2000-01-01T00:00:00Z";
172 let c = make_constraint("dateTime", ConstraintOperator::Lt, past);
173 let ctx = ConstraintContext::default();
174 assert!(!evaluate(&c, &ctx));
175 }
176}