core_policy/context_expr.rs
1//! # ABAC Context Expression Parser & Evaluator
2//!
3//! This module provides boolean expression parsing and evaluation for
4//! Attribute-Based Access Control (ABAC).
5//!
6//! ## Features
7//!
8//! - **Boolean Operators**: AND, OR, NOT
9//! - **Comparison Operators**: ==, !=, <, <=, >, >=
10//! - **Attribute Queries**: HasAttribute, GetAttribute
11//! - **Recursion Limits**: Prevents stack overflow from malicious expressions
12//! - **Deterministic Evaluation**: O(N) complexity where N = expression size
13//!
14//! ## Syntax Examples
15//!
16//! ```text
17//! role == "admin"
18//! role == "admin" AND department == "IT"
19//! (role == "admin" OR role == "moderator") AND timestamp < "1000"
20//! NOT (status == "banned")
21//! role == "user" AND (age >= "18" OR has_parent_consent == "true")
22//! ```
23//!
24//! ## Security
25//!
26//! - Maximum expression depth: 32 (prevents stack overflow)
27//! - Maximum expression length: 1024 characters (DoS prevention)
28//! - Iterative evaluation where possible (reduces stack usage)
29
30use alloc::boxed::Box;
31use alloc::collections::BTreeMap;
32use alloc::format;
33use alloc::string::String;
34use alloc::vec::Vec;
35use core::fmt;
36use serde::{Deserialize, Serialize};
37
38use crate::error::{PolicyError, Result};
39
40/// Maximum depth of nested expressions (prevents stack overflow)
41pub const MAX_EXPR_DEPTH: usize = 32;
42
43/// Maximum length of expression string (DoS prevention)
44pub const MAX_EXPR_LENGTH: usize = 1024;
45
46/// Context expression for ABAC evaluation
47///
48/// This enum represents a boolean expression tree that can be evaluated
49/// against a context (attribute map) to determine if conditions are met.
50///
51/// ## Design Rationale
52///
53/// - **Recursive Structure**: Allows complex nested conditions
54/// - **Type Safety**: Rust's type system prevents malformed expressions
55/// - **Deterministic**: No floating point, no random operations
56/// - **Serializable**: Can be stored in policy files (YAML/JSON)
57///
58/// ## Example
59///
60/// ```
61/// extern crate alloc;
62/// use core_policy::context_expr::{ContextExpr, CompareOp};
63/// use alloc::collections::BTreeMap;
64/// use alloc::string::ToString;
65///
66/// let expr = ContextExpr::And(
67/// Box::new(ContextExpr::Compare {
68/// key: "role".into(),
69/// op: CompareOp::Equal,
70/// value: "admin".into(),
71/// }),
72/// Box::new(ContextExpr::Compare {
73/// key: "department".into(),
74/// op: CompareOp::Equal,
75/// value: "IT".into(),
76/// }),
77/// );
78///
79/// let mut context = BTreeMap::new();
80/// context.insert("role".to_string(), "admin".to_string());
81/// context.insert("department".to_string(), "IT".to_string());
82///
83/// assert!(expr.evaluate(&context, 0).unwrap());
84/// ```
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub enum ContextExpr {
87 /// Logical AND (both operands must be true)
88 And(Box<ContextExpr>, Box<ContextExpr>),
89
90 /// Logical OR (at least one operand must be true)
91 Or(Box<ContextExpr>, Box<ContextExpr>),
92
93 /// Logical NOT (negates the operand)
94 Not(Box<ContextExpr>),
95
96 /// Check if an attribute exists in the context
97 HasAttribute(String),
98
99 /// Compare an attribute value with a constant
100 Compare {
101 /// Attribute key to compare
102 key: String,
103 /// Comparison operator
104 op: CompareOp,
105 /// Value to compare against (as string)
106 value: String,
107 },
108
109 /// Always true (useful for testing and default cases)
110 True,
111
112 /// Always false
113 False,
114}
115
116/// Comparison operators for attribute values
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum CompareOp {
119 /// Equal (==)
120 Equal,
121 /// Not equal (!=)
122 NotEqual,
123 /// Less than (<)
124 LessThan,
125 /// Less than or equal (<=)
126 LessThanOrEqual,
127 /// Greater than (>)
128 GreaterThan,
129 /// Greater than or equal (>=)
130 GreaterThanOrEqual,
131}
132
133impl fmt::Display for CompareOp {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 CompareOp::Equal => write!(f, "=="),
137 CompareOp::NotEqual => write!(f, "!="),
138 CompareOp::LessThan => write!(f, "<"),
139 CompareOp::LessThanOrEqual => write!(f, "<="),
140 CompareOp::GreaterThan => write!(f, ">"),
141 CompareOp::GreaterThanOrEqual => write!(f, ">="),
142 }
143 }
144}
145
146impl ContextExpr {
147 /// Evaluate this expression against a context
148 ///
149 /// # Arguments
150 ///
151 /// * `context` - Attribute map to evaluate against
152 /// * `depth` - Current recursion depth (prevents stack overflow)
153 ///
154 /// # Returns
155 ///
156 /// * `Ok(true)` - Expression evaluates to true
157 /// * `Ok(false)` - Expression evaluates to false
158 /// * `Err(PolicyError::ExpressionTooDeep)` - Recursion limit exceeded
159 ///
160 /// # Example
161 ///
162 /// ```
163 /// extern crate alloc;
164 /// use core_policy::context_expr::{ContextExpr, CompareOp};
165 /// use alloc::collections::BTreeMap;
166 /// use alloc::string::ToString;
167 ///
168 /// let expr = ContextExpr::Compare {
169 /// key: "role".into(),
170 /// op: CompareOp::Equal,
171 /// value: "admin".into(),
172 /// };
173 ///
174 /// let mut context = BTreeMap::new();
175 /// context.insert("role".to_string(), "admin".to_string());
176 ///
177 /// assert!(expr.evaluate(&context, 0).unwrap());
178 /// ```
179 pub fn evaluate(&self, context: &BTreeMap<String, String>, depth: usize) -> Result<bool> {
180 // Prevent stack overflow from deeply nested expressions
181 if depth > MAX_EXPR_DEPTH {
182 return Err(PolicyError::ExpressionTooDeep {
183 max: MAX_EXPR_DEPTH,
184 });
185 }
186
187 match self {
188 ContextExpr::True => Ok(true),
189 ContextExpr::False => Ok(false),
190
191 ContextExpr::And(left, right) => {
192 // Short-circuit evaluation: if left is false, don't evaluate right
193 let left_result = left.evaluate(context, depth + 1)?;
194 if !left_result {
195 return Ok(false);
196 }
197 right.evaluate(context, depth + 1)
198 }
199
200 ContextExpr::Or(left, right) => {
201 // Short-circuit evaluation: if left is true, don't evaluate right
202 let left_result = left.evaluate(context, depth + 1)?;
203 if left_result {
204 return Ok(true);
205 }
206 right.evaluate(context, depth + 1)
207 }
208
209 ContextExpr::Not(expr) => {
210 let result = expr.evaluate(context, depth + 1)?;
211 Ok(!result)
212 }
213
214 ContextExpr::HasAttribute(key) => Ok(context.contains_key(key)),
215
216 ContextExpr::Compare { key, op, value } => {
217 // If attribute doesn't exist, comparison fails
218 let actual = match context.get(key) {
219 Some(v) => v,
220 None => return Ok(false),
221 };
222
223 // Compare as strings (lexicographic order)
224 // For numeric comparison, values should be zero-padded or use explicit numeric parsing
225 Ok(compare_values(actual, value, *op))
226 }
227 }
228 }
229
230 /// Parse a context expression from a string
231 ///
232 /// # Grammar (simplified)
233 ///
234 /// ```text
235 /// expr ::= or_expr
236 /// or_expr ::= and_expr (OR and_expr)*
237 /// and_expr ::= not_expr (AND not_expr)*
238 /// not_expr ::= NOT primary | primary
239 /// primary ::= HAS key | key op value | (expr) | TRUE | FALSE
240 /// op ::= == | != | < | <= | > | >=
241 /// ```
242 ///
243 /// # Examples
244 ///
245 /// ```
246 /// use core_policy::context_expr::ContextExpr;
247 ///
248 /// let expr = ContextExpr::parse("role == \"admin\"").unwrap();
249 /// let expr = ContextExpr::parse("role == \"admin\" AND department == \"IT\"").unwrap();
250 /// let expr = ContextExpr::parse("(role == \"admin\" OR role == \"moderator\") AND active == \"true\"").unwrap();
251 /// let expr = ContextExpr::parse("NOT (status == \"banned\")").unwrap();
252 /// let expr = ContextExpr::parse("HAS role").unwrap();
253 /// ```
254 ///
255 /// # Errors
256 ///
257 /// * `PolicyError::InvalidExpression` - Syntax error in expression
258 /// * `PolicyError::ExpressionTooLong` - Expression exceeds MAX_EXPR_LENGTH
259 pub fn parse(input: &str) -> Result<Self> {
260 // DoS prevention: limit expression length
261 if input.len() > MAX_EXPR_LENGTH {
262 return Err(PolicyError::ExpressionTooLong {
263 max: MAX_EXPR_LENGTH,
264 length: input.len(),
265 });
266 }
267
268 let tokens = tokenize(input)?;
269 let mut parser = Parser::new(&tokens);
270 parser.parse_expr()
271 }
272}
273
274/// Helper function to compare two string values with an operator
275fn compare_values(left: &str, right: &str, op: CompareOp) -> bool {
276 match op {
277 CompareOp::Equal => left == right,
278 CompareOp::NotEqual => left != right,
279 CompareOp::LessThan => left < right,
280 CompareOp::LessThanOrEqual => left <= right,
281 CompareOp::GreaterThan => left > right,
282 CompareOp::GreaterThanOrEqual => left >= right,
283 }
284}
285
286// ===== TOKENIZER =====
287
288#[derive(Debug, Clone, PartialEq, Eq)]
289enum Token {
290 And,
291 Or,
292 Not,
293 Has,
294 True,
295 False,
296 LeftParen,
297 RightParen,
298 Equal,
299 NotEqual,
300 LessThan,
301 LessThanOrEqual,
302 GreaterThan,
303 GreaterThanOrEqual,
304 Identifier(String),
305 StringLiteral(String),
306}
307
308/// Tokenize input string into tokens
309fn tokenize(input: &str) -> Result<Vec<Token>> {
310 let mut tokens = Vec::new();
311 let mut chars = input.chars().peekable();
312
313 while let Some(&ch) = chars.peek() {
314 match ch {
315 ' ' | '\t' | '\n' | '\r' => {
316 chars.next();
317 }
318 '(' => {
319 tokens.push(Token::LeftParen);
320 chars.next();
321 }
322 ')' => {
323 tokens.push(Token::RightParen);
324 chars.next();
325 }
326 '=' => {
327 chars.next();
328 if chars.peek() == Some(&'=') {
329 chars.next();
330 tokens.push(Token::Equal);
331 } else {
332 return Err(PolicyError::InvalidExpression(
333 "Single '=' not allowed, use '=='".into(),
334 ));
335 }
336 }
337 '!' => {
338 chars.next();
339 if chars.peek() == Some(&'=') {
340 chars.next();
341 tokens.push(Token::NotEqual);
342 } else {
343 return Err(PolicyError::InvalidExpression(
344 "Single '!' not allowed, use '!=' or 'NOT'".into(),
345 ));
346 }
347 }
348 '<' => {
349 chars.next();
350 if chars.peek() == Some(&'=') {
351 chars.next();
352 tokens.push(Token::LessThanOrEqual);
353 } else {
354 tokens.push(Token::LessThan);
355 }
356 }
357 '>' => {
358 chars.next();
359 if chars.peek() == Some(&'=') {
360 chars.next();
361 tokens.push(Token::GreaterThanOrEqual);
362 } else {
363 tokens.push(Token::GreaterThan);
364 }
365 }
366 '"' => {
367 chars.next();
368 let mut value = String::new();
369 let mut escaped = false;
370 loop {
371 match chars.next() {
372 Some('\\') if !escaped => escaped = true,
373 Some('"') if !escaped => break,
374 Some(c) => {
375 value.push(c);
376 escaped = false;
377 }
378 None => {
379 return Err(PolicyError::InvalidExpression(
380 "Unterminated string literal".into(),
381 ))
382 }
383 }
384 }
385 tokens.push(Token::StringLiteral(value));
386 }
387 c if c.is_alphabetic() || c == '_' => {
388 let mut ident = String::new();
389 while let Some(&ch) = chars.peek() {
390 if ch.is_alphanumeric() || ch == '_' {
391 ident.push(ch);
392 chars.next();
393 } else {
394 break;
395 }
396 }
397 // Check for keywords
398 let token = match ident.as_str() {
399 "AND" => Token::And,
400 "OR" => Token::Or,
401 "NOT" => Token::Not,
402 "HAS" => Token::Has,
403 "TRUE" => Token::True,
404 "FALSE" => Token::False,
405 _ => Token::Identifier(ident),
406 };
407 tokens.push(token);
408 }
409 _ => {
410 return Err(PolicyError::InvalidExpression(format!(
411 "Unexpected character: '{}'",
412 ch
413 )))
414 }
415 }
416 }
417
418 Ok(tokens)
419}
420
421// ===== PARSER =====
422
423struct Parser<'a> {
424 tokens: &'a [Token],
425 pos: usize,
426}
427
428impl<'a> Parser<'a> {
429 fn new(tokens: &'a [Token]) -> Self {
430 Self { tokens, pos: 0 }
431 }
432
433 fn current(&self) -> Option<&Token> {
434 self.tokens.get(self.pos)
435 }
436
437 fn advance(&mut self) -> Option<&Token> {
438 let token = self.tokens.get(self.pos);
439 self.pos += 1;
440 token
441 }
442
443 fn expect(&mut self, expected: Token) -> Result<()> {
444 match self.advance() {
445 Some(token) if token == &expected => Ok(()),
446 Some(token) => Err(PolicyError::InvalidExpression(format!(
447 "Expected {:?}, got {:?}",
448 expected, token
449 ))),
450 None => Err(PolicyError::InvalidExpression(format!(
451 "Expected {:?}, got EOF",
452 expected
453 ))),
454 }
455 }
456
457 // expr ::= or_expr
458 fn parse_expr(&mut self) -> Result<ContextExpr> {
459 self.parse_or()
460 }
461
462 // or_expr ::= and_expr (OR and_expr)*
463 fn parse_or(&mut self) -> Result<ContextExpr> {
464 let mut left = self.parse_and()?;
465
466 while matches!(self.current(), Some(Token::Or)) {
467 self.advance();
468 let right = self.parse_and()?;
469 left = ContextExpr::Or(Box::new(left), Box::new(right));
470 }
471
472 Ok(left)
473 }
474
475 // and_expr ::= not_expr (AND not_expr)*
476 fn parse_and(&mut self) -> Result<ContextExpr> {
477 let mut left = self.parse_not()?;
478
479 while matches!(self.current(), Some(Token::And)) {
480 self.advance();
481 let right = self.parse_not()?;
482 left = ContextExpr::And(Box::new(left), Box::new(right));
483 }
484
485 Ok(left)
486 }
487
488 // not_expr ::= NOT primary | primary
489 fn parse_not(&mut self) -> Result<ContextExpr> {
490 if matches!(self.current(), Some(Token::Not)) {
491 self.advance();
492 let expr = self.parse_primary()?;
493 Ok(ContextExpr::Not(Box::new(expr)))
494 } else {
495 self.parse_primary()
496 }
497 }
498
499 // primary ::= HAS key | key op value | (expr) | TRUE | FALSE
500 fn parse_primary(&mut self) -> Result<ContextExpr> {
501 match self.current() {
502 Some(Token::True) => {
503 self.advance();
504 Ok(ContextExpr::True)
505 }
506 Some(Token::False) => {
507 self.advance();
508 Ok(ContextExpr::False)
509 }
510 Some(Token::Has) => {
511 self.advance();
512 match self.advance() {
513 Some(Token::Identifier(key)) => Ok(ContextExpr::HasAttribute(key.clone())),
514 _ => Err(PolicyError::InvalidExpression(
515 "Expected identifier after HAS".into(),
516 )),
517 }
518 }
519 Some(Token::LeftParen) => {
520 self.advance();
521 let expr = self.parse_expr()?;
522 self.expect(Token::RightParen)?;
523 Ok(expr)
524 }
525 Some(Token::Identifier(key)) => {
526 let key = key.clone();
527 self.advance();
528
529 // Parse comparison operator
530 let op = match self.current() {
531 Some(Token::Equal) => CompareOp::Equal,
532 Some(Token::NotEqual) => CompareOp::NotEqual,
533 Some(Token::LessThan) => CompareOp::LessThan,
534 Some(Token::LessThanOrEqual) => CompareOp::LessThanOrEqual,
535 Some(Token::GreaterThan) => CompareOp::GreaterThan,
536 Some(Token::GreaterThanOrEqual) => CompareOp::GreaterThanOrEqual,
537 _ => {
538 return Err(PolicyError::InvalidExpression(
539 "Expected comparison operator".into(),
540 ))
541 }
542 };
543 self.advance();
544
545 // Parse value
546 let value = match self.advance() {
547 Some(Token::StringLiteral(v)) => v.clone(),
548 Some(Token::Identifier(v)) => v.clone(), // Allow unquoted values
549 _ => {
550 return Err(PolicyError::InvalidExpression(
551 "Expected value after comparison operator".into(),
552 ))
553 }
554 };
555
556 Ok(ContextExpr::Compare { key, op, value })
557 }
558 _ => Err(PolicyError::InvalidExpression("Expected expression".into())),
559 }
560 }
561}
562
563// ============================================================================
564// Kani Formal Verification Proofs (simplified)
565// ============================================================================
566
567/// Formal verification proofs using Kani.
568/// Run with: `cargo kani --package core-policy`
569///
570/// These proofs use concrete values instead of `kani::any()` to avoid the
571/// requirement of `kani::Arbitrary` for `ContextExpr`, `BTreeMap` and `String`.
572///
573/// NOTE: Unwind bounds are kept low to prevent CBMC from exhausting memory.
574/// The proofs verify absence of panics for simple, representative inputs.
575#[cfg(kani)]
576mod kani_proofs {
577 use super::*;
578
579 // 1. evaluate() never panics on a simple expression with bounded depth
580 // Using low unwind (10) since we only evaluate ContextExpr::True
581 #[kani::proof]
582 #[kani::unwind(10)]
583 fn proof_evaluate_never_panics() {
584 let expr = ContextExpr::True;
585 let mut ctx = BTreeMap::new();
586 ctx.insert(String::from("key"), String::from("value"));
587 let _ = expr.evaluate(&ctx, 0);
588 }
589
590 // 2. depth limit is always enforced – when depth > MAX, evaluate returns error
591 // No recursion needed, so unwind(0)
592 #[kani::proof]
593 #[kani::unwind(0)]
594 fn proof_depth_limit_enforced() {
595 let expr = ContextExpr::True;
596 let ctx = BTreeMap::new();
597 let result = expr.evaluate(&ctx, MAX_EXPR_DEPTH + 1);
598 kani::assert(result.is_err(), "Depth > MAX must always fail");
599 }
600
601 // 3. depth + 1 does not overflow (checked for a value within the limit)
602 // Pure arithmetic, no loops
603 #[kani::proof]
604 #[kani::unwind(0)]
605 fn proof_depth_no_overflow() {
606 let depth = MAX_EXPR_DEPTH;
607 let new_depth = depth + 1;
608 kani::assert(new_depth <= MAX_EXPR_DEPTH + 1, "Depth increment safe");
609 }
610
611 // NOTE: proof_parse_never_panics was removed because the parser uses
612 // recursive descent with Vec allocations that cause CBMC to exhaust memory.
613 // Parser correctness is covered by existing unit tests and fuzz testing.
614}