1use 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 => actual < expected,
132 ConstraintOperator::Lteq => actual <= expected,
133 ConstraintOperator::Gt => actual > expected,
134 ConstraintOperator::Gteq => actual >= expected,
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::model::{ConstraintOperand, ConstraintOperator, OdrlConstraint};
142
143 fn make_constraint(left: &str, op: ConstraintOperator, right: &str) -> OdrlConstraint {
144 OdrlConstraint {
145 left_operand: ConstraintOperand::parse(left),
146 operator: op,
147 right_operand: right.into(),
148 }
149 }
150
151 #[test]
152 fn purpose_eq_passes() {
153 let c = make_constraint("purpose", ConstraintOperator::Eq, "analytics");
154 let ctx = ConstraintContext::default().with_purpose("analytics");
155 assert!(evaluate(&c, &ctx));
156 }
157
158 #[test]
159 fn purpose_eq_fails_wrong_value() {
160 let c = make_constraint("purpose", ConstraintOperator::Eq, "analytics");
161 let ctx = ConstraintContext::default().with_purpose("billing");
162 assert!(!evaluate(&c, &ctx));
163 }
164
165 #[test]
166 fn purpose_is_part_of() {
167 let c = make_constraint(
168 "purpose",
169 ConstraintOperator::IsPartOf,
170 "analytics, audit, reporting",
171 );
172 let ctx = ConstraintContext::default().with_purpose("audit");
173 assert!(evaluate(&c, &ctx));
174 }
175
176 #[test]
177 fn datetime_lt_passes_when_before() {
178 let future = "2099-01-01T00:00:00Z";
179 let c = make_constraint("dateTime", ConstraintOperator::Lt, future);
180 let ctx = ConstraintContext::default();
181 assert!(evaluate(&c, &ctx));
182 }
183
184 #[test]
185 fn datetime_lt_fails_when_past() {
186 let past = "2000-01-01T00:00:00Z";
187 let c = make_constraint("dateTime", ConstraintOperator::Lt, past);
188 let ctx = ConstraintContext::default();
189 assert!(!evaluate(&c, &ctx));
190 }
191
192 #[test]
193 fn count_operand_reads_custom_context() {
194 let c = make_constraint("count", ConstraintOperator::Lteq, "5");
195 let ctx = ConstraintContext::default().with("count", "3");
196 assert!(evaluate(&c, &ctx));
197 }
198
199 #[test]
200 fn custom_operand_reads_custom_context() {
201 let c = make_constraint("region", ConstraintOperator::Eq, "eu");
202 let ctx = ConstraintContext::default().with("region", "eu");
203 assert!(evaluate(&c, &ctx));
204 }
205
206 #[test]
207 fn unknown_custom_operand_fails_closed() {
208 let c = make_constraint("region", ConstraintOperator::Eq, "eu");
209 let ctx = ConstraintContext::default();
210 assert!(!evaluate(&c, &ctx));
211 }
212}