Caching System
Your FreeIPA deployment has 5,000 HBAC rules across 200 hosts. Evaluating every rule on every login attempt is too slow. The caching pipeline filters, indexes, and stores only the rules that matter for the local host, bringing evaluation time down to sub-microsecond latency.
Architecture
The pipeline has four stages:
graph TD
A[RuleSource] -->|fetch rules| B[RuleFilter]
B -->|keep applicable rules| C[IndexedCache]
C -->|fast indexed lookup| D[Evaluator]
- RuleSource fetches rules from an external backend (LDAP, file, API).
- RuleFilter keeps only rules applicable to this host.
- IndexedCache stores rules with multi-dimensional indexes for fast lookup.
- Evaluator uses the indexed cache for sub-microsecond decisions.
Building a pipeline
RulePipelineBuilder assembles the stages:
#![allow(unused)]
fn main() {
use hbac_rs::cache::*;
let pipeline = RulePipeline::builder()
.with_filter(
HostFilter::for_host("web01.example.com")
.with_group("web-servers")
)
.with_cache(IndexedCache::new())
.build();
}
You can also attach a source for automatic fetching:
#![allow(unused)]
fn main() {
use hbac_rs::cache::*;
let source = MemorySource::new(rules);
let pipeline = RulePipeline::builder()
.with_source(Box::new(source))
.with_filter(HostFilter::for_host("web01.example.com"))
.with_cache(IndexedCache::new())
.build();
}
Sources
The RuleSource trait abstracts where rules come from:
#![allow(unused)]
fn main() {
pub trait RuleSource {
fn fetch_all(&mut self) -> Result<Vec<HbacRule>, RuleSourceError>;
fn fetch_updated_since(&mut self, timestamp: u64) -> Result<Vec<HbacRule>, RuleSourceError>;
fn is_available(&self) -> bool;
}
}
Two implementations are provided:
MemorySource– holds rules in memory. Useful for testing and bootstrapping.CompositeSource– tries multiple sources in order. If the primary source is unavailable, falls back to secondary sources.
#![allow(unused)]
fn main() {
use hbac_rs::cache::*;
let mut composite = CompositeSource::new();
composite.add_source(Box::new(primary_ldap_source));
composite.add_source(Box::new(fallback_file_source));
}
For production deployments, implement RuleSource for your LDAP library.
See SSSD Integration for an example.
Filters
The RuleFilter trait determines which rules enter the cache:
#![allow(unused)]
fn main() {
pub trait RuleFilter {
fn should_cache(&self, rule: &HbacRule) -> bool;
}
}
Available filters:
| Filter | Purpose |
|---|---|
HostFilter | Keep rules whose host dimension matches this host or its groups |
ApplicabilityFilter | Keep only global rules (global_only) or all enabled rules (all_enabled) |
AcceptAllFilter | Keep everything (no filtering) |
AndFilter | Require all child filters to accept |
OrFilter | Accept if any child filter accepts |
HostFilter is the most impactful: on a host that only matches 100 out of
5,000 rules, it reduces memory usage by 98%.
#![allow(unused)]
fn main() {
use hbac_rs::cache::*;
let filter = HostFilter::for_host("web01.example.com")
.with_group("web-servers")
.with_group("dmz-hosts");
}
Compose filters for more precise control:
#![allow(unused)]
fn main() {
use hbac_rs::cache::*;
let filter = AndFilter::new()
.with(Box::new(
HostFilter::for_host("web01.example.com")
.with_group("web-servers")
))
.with(Box::new(ApplicabilityFilter::all_enabled()));
}
Cache and indexes
IndexedCache stores rules in memory with multi-dimensional indexes for
fast lookup:
#![allow(unused)]
fn main() {
use hbac_rs::cache::IndexedCache;
let mut cache = IndexedCache::new();
// Find rules matching a specific user
let user_rules = cache.find_by_user("alice", &["developers".into()]);
// Find rules matching a specific host
let host_rules = cache.find_by_host("web01", &["web-servers".into()]);
// Combined: narrow candidates by both dimensions
let candidates = cache.find_by_user_and_host(
"alice", &["developers".into()],
"web01", &["web-servers".into()],
);
}
For disk persistence (requires the serde feature):
#![allow(unused)]
fn main() {
use hbac_rs::cache::DiskCache;
let mut cache = DiskCache::new("/var/cache/hbac/rules.json")?;
// ... use as any other RuleCache ...
cache.flush()?; // persist to disk
}
Loading and refreshing
Load rules manually or let the pipeline fetch from its source:
#![allow(unused)]
fn main() {
use hbac_rs::cache::*;
let mut pipeline = RulePipeline::builder()
.with_source(Box::new(source))
.with_filter(HostFilter::for_host("web01"))
.with_cache(IndexedCache::new())
.build();
// Full refresh from source
let count = pipeline.refresh()?;
// Incremental update (only fetch rules changed since last refresh)
let updated = pipeline.incremental_update()?;
// Or load directly without a source
pipeline.load(rules);
}
Monitoring
CacheStats tracks cache health:
#![allow(unused)]
fn main() {
let stats = pipeline.stats();
println!(
"Rules: {}, Memory: {} bytes, Hit rate: {:.1}%",
stats.rule_count,
stats.memory_bytes,
stats.hit_rate() * 100.0,
);
}
Fields:
| Field | Description |
|---|---|
rule_count | Number of rules in the cache |
memory_bytes | Estimated memory consumption |
hit_count | Number of cache hits |
miss_count | Number of cache misses |
The hit_rate() method returns a value between 0.0 and 1.0.
Memory optimization
Host-specific filtering is the single most effective optimization. On a host where only 100 out of 5,000 rules apply, filtering reduces memory from megabytes to tens of kilobytes. This makes hbac-rs practical for resource-constrained environments like embedded Linux systems and containers.
See Performance Results for measured memory usage at different rule counts.
Next steps
The pipeline needs rules from somewhere. In a FreeIPA environment, that somewhere is LDAP, synchronized by SSSD. The next page shows how to integrate with SSSD deployments.
See SSSD Integration.