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:
- Check all deny rules – if any match, return
Decision::Deny. - Check all allow rules – if any match, return
Decision::Allow. - Default to
Decision::Denyif 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):
| Rules | Throughput | Mean Latency | Memory |
|---|---|---|---|
| 100 | 1.50M r/s | 668 ns | 7.8 KB |
| 1,000 | 4.76M r/s | 210 ns | 78 KB |
| 10,000 | 2.92M r/s | 343 ns | 781 KB |
| 100,000 | 1.19M r/s | 843 ns | 7.6 MB |
| 1,000,000 | 156K r/s | 6.43 us | 76 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.
What to read next
- ABAC Rules – creating rules with multiple dimensions.
- Policy Evaluation – understanding the optimization pipeline.
- Multi-type Attributes – using Integer, IpAddr, and custom types.
- Custom Matchers – implementing CIDR matching, numeric ranges, and predicates.
- Performance Results – benchmarks and scaling characteristics.