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:
capsulemodule – Safe PyCapsule creation and import helperscreate_capsule<T>()– Export C-ABI compatible type via capsuleimport_capsule<T>()– Import capsule from another Python module
rbacmodule –RbacPolicyAPIfor sharingRbacPolicyacross modulessubjectmodule –SubjectAPIfor sharingSubjectacross modules- Enables
abac_rs.ComposedPolicyto acceptacls_rs.RbacPolicyandhbac_rs.Subjectvia FFI
acls-rs (Foundation)
Algebraically-correct access control primitives. Minimal runtime dependencies (log, thiserror).
Key types:
Subject– user with permissions, roles, and temporal permissionsAtomicPermission– singlenamespace:actionpermissionPermissionSet– collection with algebraic operations (combine,meet,join,difference)GrantDenialPair– separate grants and denials with denial precedenceRole– named permission bundle with optional parent hierarchyRbacPolicy– role resolution with cycle detectionAbacPolicy– attribute-based evaluation with pluggable conflict resolversTemporalPermission– time-windowed permission
Shared abstractions:
sync::SyncStrategy<T>– Generic interior mutability trait overMutex<T>andRefCell<T>policy::PolicyError– Shared error type for DoS protection violationspolicy::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 useBox<[Arc<str>]>)HbacCategory–AllorSpecific { names: Box<[Arc<str>]>, groups: Box<[Arc<str>]> }HbacCompiledEvaluator– pre-compiled evaluator with AHashSet lookupsHbacPolicyCore<L>– evaluation engine parameterized by lock typeHbacPolicy/HbacPolicyLocal– thread-safe and single-threaded aliasesRulePipeline– cached evaluation with Bloom filter, decision tree, bitmap-based deny-rule index, and compiled evaluatorComposedPolicy– HBAC+RBAC composition with four modesTemporalHbacRule– 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 semanticsAttributeValue– multi-type value (String, Integer, Float, IpAddr, IpCidr, custom)AttributeMatcher– pluggable matcher for custom matching logicAbacCompiledEvaluator– pre-compiled evaluator with pre-extracted attributesAbacPolicy/AbacPolicyLocal– thread-safe and single-threaded aliasesAbacRequest– 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
- Layered architecture: acls-rs provides primitives, hbac-rs and abac-rs build domain-specific evaluation, perf-testing validates performance.
- Separation of concerns: pure evaluation logic vs. caching vs. tooling.
- Composability: HBAC, RBAC, and ABAC policies can be combined.
- Lazy construction: expensive indexes are deferred until first evaluation.
- 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:
| Pattern | Strategy | Speedup |
|---|---|---|
category=all | Constant return | Negligible (already fast) |
| Single user | Integer comparison (<=8 bytes) or memcmp | Measurable |
| Group match | Unrolled comparison loop | Measurable |
| Complex | Falls back to interpreter | None |
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 viaArc<HbacPolicy>across threads.HbacPolicyLocal(RefCell): zero mutex overhead for single-threaded use (SSSD, benchmarks).- Shared implementation in
acls_rs::sync::SyncStrategyeliminates 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
- Performance Results – benchmark data
- Caching Architecture – pipeline internals
- API Reference – complete type listings