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
- Layer 0: Constant result (~15 ns) – universal allow/deny detection
- Layer 1: Bitmap deny index – u64 bitmask intersection when universal allow exists
- Layer 2: LRU cache (sub-μs) – memoization of recent evaluations
- Layer 3: AHash – 2-3x faster than SipHash for non-cryptographic use
- Layer 4: Compiled evaluator – pre-extracted attributes + array indexing
- Layer 5: Composite index (O(log n)) – candidate selection fallback
- 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:
- Deny rules are checked first (deny-override semantics)
- If no deny matches, universal allow guarantees Allow decision
- 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
- Check all deny rules – if any match, return
Deny - Check all allow rules – if any match, return
Allow - Default to
Denyif 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.