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;
8use typesec_core::policy::RequestContext;
9
10use crate::model::{ConstraintOperand, ConstraintOperator, OdrlConstraint};
11
12/// Context provided when evaluating constraints.
13///
14/// The context carries *all* information about the current request that
15/// constraints might need to evaluate. Add fields here as your policies grow.
16#[derive(Debug, Clone, Default)]
17pub struct ConstraintContext {
18    /// The purpose of this access request (e.g., `"analytics"`, `"audit"`).
19    pub purpose: Option<String>,
20    /// The current time (defaults to `Utc::now()` if not specified).
21    pub now: Option<DateTime<Utc>>,
22    /// An arbitrary key-value store for custom constraint operands.
23    pub custom: std::collections::HashMap<String, String>,
24}
25
26impl ConstraintContext {
27    /// Create a context with a given purpose.
28    pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
29        self.purpose = Some(purpose.into());
30        self
31    }
32
33    /// Create a context with a specific time (useful for testing).
34    pub fn with_time(mut self, t: DateTime<Utc>) -> Self {
35        self.now = Some(t);
36        self
37    }
38
39    /// Add a custom key-value pair.
40    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
60/// Evaluate a single ODRL constraint against a context.
61///
62/// Returns `true` if the constraint is satisfied, `false` otherwise.
63pub 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, // doesn't apply to dates
122    }
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        // Ordering operators compare numerically when *both* sides parse as
131        // numbers — so `count lteq 5` is arithmetic (10 is not <= 5), not
132        // lexicographic (where "10" < "5"). Non-numeric operands (string tags)
133        // fall back to lexicographic ordering.
134        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
144/// Apply an ordering operator to two comparable values. Eq/Neq/IsPartOf are
145/// handled by the caller and never reach here.
146fn 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;