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}