Skip to main content

typesec_odrl/
constraint.rs

1//! Constraint evaluation for ODRL rules.
2//!
3//! Constraints are runtime conditions: purpose, date/time, count, etc.
4//! This module evaluates them given a [`ConstraintContext`].
5
6use chrono::{DateTime, Utc};
7use tracing::debug;
8
9use crate::model::{ConstraintOperator, OdrlConstraint};
10
11/// Context provided when evaluating constraints.
12///
13/// The context carries *all* information about the current request that
14/// constraints might need to evaluate. Add fields here as your policies grow.
15#[derive(Debug, Clone, Default)]
16pub struct ConstraintContext {
17    /// The purpose of this access request (e.g., `"analytics"`, `"audit"`).
18    pub purpose: Option<String>,
19    /// The current time (defaults to `Utc::now()` if not specified).
20    pub now: Option<DateTime<Utc>>,
21    /// An arbitrary key-value store for custom constraint operands.
22    pub custom: std::collections::HashMap<String, String>,
23}
24
25impl ConstraintContext {
26    /// Create a context with a given purpose.
27    pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
28        self.purpose = Some(purpose.into());
29        self
30    }
31
32    /// Create a context with a specific time (useful for testing).
33    pub fn with_time(mut self, t: DateTime<Utc>) -> Self {
34        self.now = Some(t);
35        self
36    }
37
38    /// Add a custom key-value pair.
39    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
49/// Evaluate a single ODRL constraint against a context.
50///
51/// Returns `true` if the constraint is satisfied, `false` otherwise.
52pub 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            // Fall back to custom context values.
66            if let Some(val) = ctx.custom.get(other) {
67                evaluate_string_op(&constraint.operator, val, &constraint.right_operand)
68            } else {
69                // Unknown operand — be conservative and deny.
70                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, // doesn't apply to dates
107    }
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        // Lexicographic ordering for strings (reasonable for simple tags).
116        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}