Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Policy Evaluation

The ABAC policy engine evaluates access requests through a multi-layer optimization pipeline. Each layer short-circuits on success, so most requests never reach the sequential evaluation fallback.

Basic evaluation

#![allow(unused)]
fn main() {
use abac_rs::{AbacPolicy, AbacRequest, AttributeType};

let mut policy = AbacPolicy::new();
// ... add rules ...

let mut request = AbacRequest::new();
request.add_attribute("user", AttributeType::String("alice".into()), vec![]);
request.add_attribute("resource", AttributeType::String("db-01".into()), vec![]);
request.add_attribute("action", AttributeType::String("read".into()), vec![]);

let decision = policy.evaluate(&request);
if decision.is_allowed() {
    // Grant access
}
}

Constructing a policy with PolicyBuilder

For one-shot configuration – custom cache size, rule limit, matchers, and an initial rule set – use AbacPolicy::builder() instead of AbacPolicy::new() plus sequential add_rule()/register_matcher() calls. This avoids redundant index rebuilds from applying each setting one at a time:

#![allow(unused)]
fn main() {
use abac_rs::{AbacPolicy, AbacRule, AttributeType};

let policy: AbacPolicy = AbacPolicy::builder()
    .max_rules(10_000)
    .cache_size(2048)
    .rules(vec![
        AbacRule::builder("allow-read")
            .dimension_all("user")
            .dimension_values("action", vec![AttributeType::String("read".into())])
            .enabled(true)
            .build(),
    ])
    .build()
    .unwrap();

assert_eq!(policy.rule_count(), 1);
}

max_rules (default 1,000,000) bounds how many rules a policy will accept; exceeding it returns PolicyError::TooManyRules instead of allowing unbounded growth from an untrusted or externally-controlled rule source.

Optimization pipeline

  1. Layer 0: Constant result (~15 ns) – universal allow/deny detection
  2. Layer 1: Bitmap deny index – u64 bitmask intersection when universal allow exists
  3. Layer 2: LRU cache (sub-μs) – memoization of recent evaluations
  4. Layer 3: AHash – 2-3x faster than SipHash for non-cryptographic use
  5. Layer 4: Compiled evaluator – pre-extracted attributes + array indexing
  6. Layer 5: Composite index (O(log n)) – candidate selection fallback
  7. Layer 6: Deny-only indexing – skip allow rules when universal allow exists

The compiled evaluator (Layer 4) is automatically detected and applied when all rules use consistent dimensions. It pre-extracts request attributes once (3 HashMap lookups) instead of per-candidate (600+ lookups), providing a 62x speedup at large scales.

Deny-only indexing: When a universal allow rule exists, only deny rules are indexed. This is valid because:

  1. Deny rules are checked first (deny-override semantics)
  2. If no deny matches, universal allow guarantees Allow decision
  3. Individual allow rules become irrelevant

This optimization reduces index size and evaluation time dramatically in typical deployments where most rules are allow and only a few are deny exceptions.

See ABAC Performance for detailed layer breakdown.

Lazy index building

Indexes are built on first evaluation after rule changes, not during add_rule(). This eliminates O(n²) overhead during batch rule loading.

Deny-override semantics

  1. Check all deny rules – if any match, return Deny
  2. Check all allow rules – if any match, return Allow
  3. Default to Deny if no rules match

Denials always take precedence over allows.

Explained evaluation

evaluate() is optimized for the hot path and does not report why a decision was made. For audit logging and diagnostics, use evaluate_explained() (or evaluate_explained_at() for a specific timestamp) instead:

#![allow(unused)]
fn main() {
use abac_rs::{AbacPolicy, AbacRequest, AttributeType};

let mut policy = AbacPolicy::new();
// ... add rules ...

let mut request = AbacRequest::new();
request.add_attribute("user", AttributeType::String("alice".into()), vec![]);
request.add_attribute("resource", AttributeType::String("db-01".into()), vec![]);
request.add_attribute("action", AttributeType::String("read".into()), vec![]);

let explained = policy.evaluate_explained(&request);
println!("{:?} ({} rules evaluated)", explained.decision, explained.rules_evaluated);
for rule_match in &explained.matched_rules {
    println!("matched: {} ({:?}, temporal={})", rule_match.name, rule_match.rule_type, rule_match.temporal);
}
}

evaluate_explained() bypasses the constant-result, deny-index, compiled-evaluator, and LRU cache layers so it can collect every matching deny rule (not just the first) and record which allow rule ultimately won. It is intentionally slower than evaluate() and is meant for audit logging and interactive tooling, not the hot evaluation path.

See also