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

Architecture Overview

BAC Rules is organized as a Rust workspace with five crates.

Crate Structure

graph LR
    CORE[bac-python-core<br/>Python shared types] --> ACLS[acls-rs<br/>Foundation]
    CORE --> HBAC[hbac-rs<br/>HBAC implementation]
    CORE --> ABAC[abac-rs<br/>ABAC implementation]
    ACLS --> HBAC
    ACLS --> ABAC
    ACLS --> PERF[perf-testing<br/>Performance tooling]
    HBAC --> PERF
    ABAC --> PERF

bac-python-core (Python Shared Types)

Shared infrastructure for Python bindings across all crates. Enables cross-module type sharing via Python capsules (PyCapsule API).

Key components:

  • capsule module – Safe PyCapsule creation and import helpers
    • create_capsule<T>() – Export C-ABI compatible type via capsule
    • import_capsule<T>() – Import capsule from another Python module
  • rbac module – RbacPolicyAPI for sharing RbacPolicy across modules
  • subject module – SubjectAPI for sharing Subject across modules
  • Enables abac_rs.ComposedPolicy to accept acls_rs.RbacPolicy and hbac_rs.Subject via FFI

acls-rs (Foundation)

Algebraically-correct access control primitives. Minimal runtime dependencies (log, thiserror).

Key types:

  • Subject – user with permissions, roles, and temporal permissions
  • AtomicPermission – single namespace:action permission
  • PermissionSet – collection with algebraic operations (combine, meet, join, difference)
  • GrantDenialPair – separate grants and denials with denial precedence
  • Role – named permission bundle with optional parent hierarchy
  • RbacPolicy – role resolution with cycle detection
  • AbacPolicy – attribute-based evaluation with pluggable conflict resolvers
  • TemporalPermission – time-windowed permission

Shared abstractions:

  • sync::SyncStrategy<T> – Generic interior mutability trait over Mutex<T> and RefCell<T>
  • policy::PolicyError – Shared error type for DoS protection violations
  • policy::RuleLimitedPolicy – Trait for enforcing maximum rule counts

hbac-rs (HBAC Implementation)

FreeIPA HBAC built on acls-rs. Three-dimensional access control (user x host x service) with a six-layer optimization pipeline.

Key types:

  • HbacRule – three-dimensional rule with allow/deny semantics (name: Arc<str>, categories use Box<[Arc<str>]>)
  • HbacCategoryAll or Specific { names: Box<[Arc<str>]>, groups: Box<[Arc<str>]> }
  • HbacCompiledEvaluator – pre-compiled evaluator with AHashSet lookups
  • HbacPolicyCore<L> – evaluation engine parameterized by lock type
  • HbacPolicy / HbacPolicyLocal – thread-safe and single-threaded aliases
  • RulePipeline – cached evaluation with Bloom filter, decision tree, bitmap-based deny-rule index, and compiled evaluator
  • ComposedPolicy – HBAC+RBAC composition with four modes
  • TemporalHbacRule – time-windowed HBAC rule

abac-rs (ABAC Implementation)

Generic attribute-based access control built on acls-rs. N-dimensional access control with multi-type attributes and pluggable matchers.

Key types:

  • AbacRule – attribute-based rule with allow/deny semantics
  • AttributeValue – multi-type value (String, Integer, Float, IpAddr, IpCidr, custom)
  • AttributeMatcher – pluggable matcher for custom matching logic
  • AbacCompiledEvaluator – pre-compiled evaluator with pre-extracted attributes
  • AbacPolicy / AbacPolicyLocal – thread-safe and single-threaded aliases
  • AbacRequest – evaluation request with attribute context
  • Bitmap-based deny index with u64 bitmask intersection (3.2x less memory than HBAC)

perf-testing (Tooling)

Performance testing framework and bac-perf CLI. Depends on acls-rs, hbac-rs, and abac-rs.

Key components:

  • Rule synthesis with configurable distribution presets
  • Fixture management (compressed JSON)
  • Benchmark scenarios (latency, throughput, build time)
  • Result comparison for regression detection

Directory Structure

graph TD
    A[bac-rules/] --> B[crates/]
    A --> C[docs/]
    A --> D[fixtures/]

    B --> B0[bac-python-core/]
    B --> B1[acls-rs/]
    B --> B2[hbac-rs/]
    B --> B3[abac-rs/]
    B --> B4[perf-testing/]

Design Principles

  1. Layered architecture: acls-rs provides primitives, hbac-rs and abac-rs build domain-specific evaluation, perf-testing validates performance.
  2. Separation of concerns: pure evaluation logic vs. caching vs. tooling.
  3. Composability: HBAC, RBAC, and ABAC policies can be combined.
  4. Lazy construction: expensive indexes are deferred until first evaluation.
  5. Interior mutability: cache updates happen through an immutable policy API.

HBAC Optimization Architecture

HbacPolicy uses a six-layer optimization stack. Each layer is tried in order; a result from any layer short-circuits the rest.

Evaluation Flow

flowchart TD
    REQ[evaluate request] --> DIRTY{indexes<br/>dirty?}
    DIRTY -->|yes| REBUILD[rebuild indexes]
    REBUILD --> L0
    DIRTY -->|no| L0

    L0{Layer 0<br/>constant result?} -->|yes| RET0[return stored result]
    L0 -->|no| L1{Layer 1<br/>LRU cache hit?}
    L1 -->|hit| RET1[return cached result]
    L1 -->|miss| L2{Layer 2<br/>Bloom filter}
    L2 -->|definite no-match| RET2[return deny]
    L2 -->|maybe match| L6

    L6{Layer 6<br/>JIT compiled?} -->|hit| RET6[return JIT result]
    L6 -->|no JIT| L4

    L4{Layer 4<br/>deny index<br/>available?} -->|yes| DENY[bitmask intersection<br/>check candidates]
    DENY --> RET4[return result]
    L4 -->|no| L3[Layer 3<br/>decision tree]
    L3 --> RET3[return result]

    RET2 --> CACHE[store in LRU]
    RET6 --> CACHE
    RET4 --> CACHE
    RET3 --> CACHE

    style L6 stroke-dasharray: 5 5

Layer 0: Constant-Result Fast Path

When the decision tree collapses to a constant (e.g., a universal deny rule exists, or a universal allow with no deny rules), the result is stored at build time and returned immediately. Bypasses all other layers.

Layer 1: LRU Memoization Cache

#![allow(unused)]
fn main() {
eval_cache: LruCache<u64, bool>  // 1,024 entries, u64 hash key
}

Keyed by a u64 hash of (user, host, service, groups). Group hashing uses order-independent wrapping addition. Zero heap allocations per lookup.

Layer 2: Bloom Filter Pre-screening

#![allow(unused)]
fn main() {
pub struct BloomFilterCache {
    user_filter: BloomFilter,
    host_filter: BloomFilter,
    service_filter: BloomFilter,
    has_user_all: bool,
    has_host_all: bool,
    has_service_all: bool,
}
}

Fast negative check with a 1% false positive rate. Tracks category=all rules separately to avoid false negatives.

Layer 3: Decision Tree Compilation

#![allow(unused)]
fn main() {
pub enum DecisionNode {
    UserCheck { user_id, if_match, if_no_match },
    HostCheck { host, if_match, if_no_match },
    ServiceCheck { service, if_match, if_no_match },
    UserGroupCheck { group, if_match, if_no_match },
    Result { allow: bool },
    Sequential { rule_indices: Vec<usize>, has_deny },
    SequentialWithDefault { rule_indices: Vec<usize>, default_allow, has_deny },
}
}

Rules are compiled into a tree at load time. The Sequential and SequentialWithDefault variants store Vec<usize> indices into the canonical rule slice rather than cloned rules. When a universal allow exists alongside specific deny rules, the tree stores only the deny rule indices (SequentialWithDefault { default_allow: true }), reducing evaluation from O(n) to O(d).

Layer 4: Deny-Rule Index

Indexes deny rules by user and host dimensions. On evaluation, intersects user candidates with host candidates using bitmask operations (u64 words), then checks service matching only on the intersection. Narrows the candidate set from tens of deny rules to 2-3.

flowchart LR
    U[User candidates<br/>bitmask] --> INT[Bitmask<br/>intersection]
    H[Host candidates<br/>bitmask] --> INT
    INT --> SVC{Service<br/>match?}
    SVC -->|match| DENY[Deny]
    SVC -->|no match| ALLOW[Default allow]

Built only when the decision tree produces SequentialWithDefault { default_allow: true }.

Layer 5: Composite Indexing (Fallback)

O(1) user+host lookup via CompositeIndex. Used when the decision tree is not available.

Layer 6: JIT Compilation (optional)

Requires the jit feature flag. Compiles per-rule matching logic to native code via Cranelift.

flowchart TD
    EVAL[evaluate rule] --> HOT{evaluated<br/>>100 times?}
    HOT -->|no| INTERP[interpreter]
    HOT -->|yes| COMPILED{JIT cache<br/>has compiled?}
    COMPILED -->|yes| NATIVE[run native code]
    COMPILED -->|no| COMPILE[compile via Cranelift]
    COMPILE --> NATIVE

Compilation strategies by rule pattern:

PatternStrategySpeedup
category=allConstant returnNegligible (already fast)
Single userInteger comparison (<=8 bytes) or memcmpMeasurable
Group matchUnrolled comparison loopMeasurable
ComplexFalls back to interpreterNone

See Performance Results for measured JIT impact.

Lazy Index Building

sequenceDiagram
    participant App
    participant Policy as HbacPolicy
    participant Pipeline as RulePipeline

    App->>Policy: load_rules(rules)
    Policy->>Pipeline: store rules
    Pipeline->>Pipeline: indexes_dirty = true

    App->>Policy: evaluate(request)
    Policy->>Pipeline: evaluate(request)
    Pipeline->>Pipeline: indexes_dirty? yes
    Pipeline->>Pipeline: rebuild_indexes()
    Note over Pipeline: Build Bloom filter<br/>Build decision tree<br/>Build deny index<br/>Detect constant result
    Pipeline->>Pipeline: indexes_dirty = false
    Pipeline-->>Policy: HbacEvaluationResult

    App->>Policy: evaluate(request)
    Policy->>Pipeline: evaluate(request)
    Pipeline->>Pipeline: indexes_dirty? no
    Pipeline-->>Policy: HbacEvaluationResult (fast path)

This eliminates O(n^2) overhead when loading rules in batch: load_rules() marks indexes dirty and a single rebuild happens on the first evaluate() call.

Interior Mutability Pattern

All policy types (HbacPolicyCore<L>, AbacPolicyCore<L>, ComposedPolicyCore<A, P>) use the SyncStrategy<T> trait for generic interior mutability.

classDiagram
    class SyncStrategy~T~ {
        <<trait>>
        +new(inner: T) Self
        +with(f: impl FnOnce(&mut T) -> R) R
    }

    class Mutex~T~ {
        +lock() MutexGuard~T~
    }

    class RefCell~T~ {
        +borrow_mut() RefMut~T~
    }

    SyncStrategy <|.. Mutex : Thread-safe
    SyncStrategy <|.. RefCell : Single-threaded

    class HbacPolicyCore~L~ {
        -rules: Vec~HbacRule~
        -pipeline: L
        -pipeline_dirty: AtomicBool
        -temporal_rules: Vec~TemporalHbacRule~
        +evaluate(&self, request) HbacEvaluationResult
        +add_rule(&mut self, rule)
    }

    class HbacPolicy {
        L = Mutex~RulePipeline~
        Send + Sync
    }

    class HbacPolicyLocal {
        L = RefCell~RulePipeline~
        !Sync
    }

    HbacPolicyCore <|-- HbacPolicy : L = Mutex
    HbacPolicyCore <|-- HbacPolicyLocal : L = RefCell

evaluate(&self) mutates the internal pipeline (LRU cache updates, lazy index rebuilds) through the SyncStrategy trait. This maintains a natural API where evaluation does not require &mut self, while cache mutations happen internally.

  • HbacPolicy (Mutex): safe to share via Arc<HbacPolicy> across threads.
  • HbacPolicyLocal (RefCell): zero mutex overhead for single-threaded use (SSSD, benchmarks).
  • Shared implementation in acls_rs::sync::SyncStrategy eliminates code duplication across HBAC, ABAC, and composed policies.

Two-Tier Evaluation API

#![allow(unused)]
fn main() {
// Fast path: production use (no match tracking)
pub fn evaluate(&self, request) -> HbacEvaluationResult
// result.matched_rules is empty -- saves allocation overhead

// Detailed: testing/debugging (with match tracking)
pub fn evaluate_detailed(&self, request) -> HbacEvaluationResult
// result.matched_rules is populated
}

Production systems rarely need to know which rules matched. The detailed variant populates matched_rules and not_matched_rules for diagnostic tools.

See Also