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

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]

  1. RuleSource fetches rules from an external backend (LDAP, file, API).
  2. RuleFilter keeps only rules applicable to this host.
  3. IndexedCache stores rules with multi-dimensional indexes for fast lookup.
  4. 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:

FilterPurpose
HostFilterKeep rules whose host dimension matches this host or its groups
ApplicabilityFilterKeep only global rules (global_only) or all enabled rules (all_enabled)
AcceptAllFilterKeep everything (no filtering)
AndFilterRequire all child filters to accept
OrFilterAccept 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:

FieldDescription
rule_countNumber of rules in the cache
memory_bytesEstimated memory consumption
hit_countNumber of cache hits
miss_countNumber 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.