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

abac-rs: Attribute-Based Access Control

You’ve outgrown the three-dimensional user/host/service model. Your access control needs depend on four, five, or ten attributes – tenant isolation, data classification, time-of-day restrictions, IP CIDR ranges, and custom business logic that can’t be expressed in fixed dimensions.

This is Attribute-Based Access Control (ABAC) – a policy model where decisions are based on arbitrary attribute dimensions and pluggable matching strategies. Every dimension can use exact matching, CIDR matching, numeric comparisons, or custom predicates. Rules can require that any subset of dimensions match, and new dimensions can be added without changing code.

abac-rs provides an N-dimensional policy engine with a multi-layer optimization pipeline: constant-result fast path, bitmap deny index, LRU caching, AHash, Bloom filter pre-screening, compiled evaluation (for consistent dimensions), composite indexing, and deny-only indexing (when universal allow rules exist). It integrates with acls-rs for algebraic correctness and works seamlessly with perf-testing for benchmarking.

Adding abac-rs to your project

[dependencies]
abac-rs = "0.1"
acls-rs = "0.1"

With serde support:

[dependencies]
abac-rs = { version = "0.1", features = ["serde"] }

Basic usage

Create a rule that allows engineers to read production databases, then evaluate a request:

#![allow(unused)]
fn main() {
use abac_rs::{AbacPolicy, AbacRule, AbacRequest, AttributeType};

// Create an ABAC rule
let rule = AbacRule::builder("allow_engineers_prod_read")
    // User dimension: members of the engineers group
    .dimension_values("user", vec![
        AttributeType::String("group:engineers".into()),
    ])
    // Resource dimension: production databases
    .dimension_values("resource", vec![
        AttributeType::String("prod:db-01".into()),
        AttributeType::String("prod:db-02".into()),
    ])
    // Action dimension: read-only access
    .dimension_values("action", vec![
        AttributeType::String("read".into()),
    ])
    .enabled(true)
    .build();

// Build a policy and load the rule
let mut policy = AbacPolicy::new();
policy.add_rule(rule);

// Evaluate an access request
let mut request = AbacRequest::new();
request.add_attribute(
    "user",
    AttributeType::String("alice".into()),
    vec![AttributeType::String("group:engineers".into())],
);
request.add_attribute(
    "resource",
    AttributeType::String("prod:db-01".into()),
    vec![],
);
request.add_attribute(
    "action",
    AttributeType::String("read".into()),
    vec![],
);

let decision = policy.evaluate(&request);
assert!(decision.is_allowed());
}

Unlimited dimensions

Unlike hbac-rs’s fixed user/host/service model, abac-rs supports arbitrary dimensions. Each rule specifies matching criteria on whichever dimensions are relevant:

#![allow(unused)]
fn main() {
// Cloud IAM: four dimensions
let rule = AbacRule::builder("cloud_iam_policy")
    .dimension_values("principal", vec![/* ... */])
    .dimension_values("resource", vec![/* ... */])
    .dimension_values("action", vec![/* ... */])
    .dimension_values("source_ip", vec![/* ... */])
    .enabled(true)
    .build();

// Healthcare: five dimensions
let rule = AbacRule::builder("healthcare_policy")
    .dimension_values("role", vec![/* ... */])
    .dimension_values("patient_age", vec![/* ... */])
    .dimension_values("data_type", vec![/* ... */])
    .dimension_values("purpose", vec![/* ... */])
    .dimension_values("time_of_day", vec![/* ... */])
    .enabled(true)
    .build();

// Multi-tenancy: ten+ dimensions
let rule = AbacRule::builder("multitenant_policy")
    .dimension_values("user", vec![/* ... */])
    .dimension_values("tenant", vec![/* ... */])
    .dimension_values("environment", vec![/* ... */])
    // ... add as many as needed
    .enabled(true)
    .build();
}

A rule matches a request only when all of its dimensions match.

Multi-type attributes

Attributes aren’t limited to strings. abac-rs supports six built-in types plus custom extensions:

#![allow(unused)]
fn main() {
use abac_rs::AttributeType;
use std::net::IpAddr;

// String (most common)
let user = AttributeType::String("alice".into());

// Integer
let age = AttributeType::Integer(25);

// Float
let risk_score = AttributeType::Float(0.85);

// IP address
let ip = AttributeType::IpAddr("10.1.2.3".parse().unwrap());

// IP CIDR range
let network = AttributeType::IpCidr("10.0.0.0/8".parse().unwrap());

// Custom types via trait
let custom = AttributeType::Custom(Arc::new(MyCustomType { /* ... */ }));
}

Wildcard matching

Dimensions can use AttributeValue::All to match any value:

#![allow(unused)]
fn main() {
// Allow any user to read public data
let rule = AbacRule::builder("public_read")
    .dimension_all("user")
    .dimension_values("resource", vec![
        AttributeType::String("public:data".into()),
    ])
    .dimension_values("action", vec![
        AttributeType::String("read".into()),
    ])
    .enabled(true)
    .build();
}

Group membership

Requests can specify group memberships for each dimension. The policy checks both the primary value and all groups:

#![allow(unused)]
fn main() {
request.add_attribute(
    "user",
    AttributeType::String("alice".into()),       // primary value
    vec![
        AttributeType::String("group:admins".into()),     // groups
        AttributeType::String("group:developers".into()),
    ],
);
}

A rule that requires group:admins will match this request even though the primary user is alice.

Deny-override semantics

Deny rules take precedence over allow rules. The policy evaluates in this order:

  1. Check all deny rules – if any match, return Decision::Deny.
  2. Check all allow rules – if any match, return Decision::Allow.
  3. Default to Decision::Deny if no rules match.
#![allow(unused)]
fn main() {
let allow_rule = AbacRule::builder("allow_read")
    .dimension_values("action", vec![
        AttributeType::String("read".into()),
    ])
    .enabled(true)
    .build();

let deny_rule = AbacRule::builder("deny_guests")
    .deny()
    .dimension_values("user", vec![
        AttributeType::String("group:guests".into()),
    ])
    .enabled(true)
    .build();

// If a request matches both rules, deny wins
}

Optimization pipeline

abac-rs uses a multi-layer optimization pipeline:

graph TD
    A[Request] --> B{Layer 0<br/>Constant result?}
    B -->|Yes| C[Return ~15 ns]
    B -->|No| D{Layer 1<br/>Bitmap deny index?}
    D -->|Yes| D2[u64 AND intersection]
    D -->|No| E{Layer 2<br/>LRU cache hit?}
    D2 --> RET[Return result]
    E -->|Yes| F[Return sub-μs]
    E -->|No| G{Layer 3<br/>Bloom filter reject?}
    G -->|Yes| H[Return Deny ~50 ns]
    G -->|No| I[Layer 4<br/>Compiled evaluator]
    I --> I2[Pre-extracted attrs]
    I2 --> J{Layer 5<br/>Composite index}
    J --> K[Match O candidates]

Through systematic optimization, abac-rs achieves high throughput across all scales (x86_64, cached, single-threaded):

RulesThroughputMean LatencyMemory
1001.50M r/s668 ns7.8 KB
1,0004.76M r/s210 ns78 KB
10,0002.92M r/s343 ns781 KB
100,0001.19M r/s843 ns7.6 MB
1,000,000156K r/s6.43 us76 MB

HBAC is faster or equal to ABAC at all scales on x86_64 due to bitmap deny index optimization and memory-optimized rule storage in HBAC. ABAC’s advantage is N-dimensional flexibility and 3.2x less memory (e.g., 76 MB vs 241 MB at 1M rules), not throughput. Choose abac-rs when you need more than three dimensions, custom attribute types, or lower memory usage.

See Performance Results for detailed benchmarks and Cross-BAC Benchmarking for direct comparison.

Pluggable matchers

For dimensions that need custom matching logic beyond exact equality, implement the Matcher trait:

#![allow(unused)]
fn main() {
use abac_rs::Matcher;

pub struct IpCidrMatcher;

impl Matcher for IpCidrMatcher {
    fn matches(
        &self,
        rule_value: &AttributeValue,
        request_value: &AttributeType,
        request_groups: &[AttributeType],
    ) -> bool {
        match (rule_value, request_value) {
            (AttributeValue::Specific(cidrs), AttributeType::IpAddr(ip)) => {
                // Check if IP is in any allowed CIDR
                cidrs.iter().any(|cidr| {
                    if let AttributeType::IpCidr(network) = cidr {
                        network.contains(*ip)
                    } else {
                        false
                    }
                })
            }
            (AttributeValue::All, _) => true,
            _ => false,
        }
    }
}
}

See Custom Matchers for complete examples.