Policy Composition
Your platform uses ABAC to control access based on multiple attributes (user groups, resource types, actions, time windows, IP ranges) and RBAC to manage role-based permissions. These complement each other: ABAC provides fine-grained, multi-dimensional control while RBAC provides role hierarchy and simple permission management.
ComposedPolicy unifies ABAC and RBAC evaluation into a single decision with configurable composition modes.
Creating a composed policy
ComposedPolicy::new takes an AbacPolicy, an RbacPolicy, and a CompositionMode:
#![allow(unused)]
fn main() {
use abac_rs::{AbacPolicy, AbacRule, AbacRequest, AttributeType};
use abac_rs::{ComposedPolicy, CompositionMode};
use acls_rs::policy::RbacPolicy;
use acls_rs::permission::{AtomicPermission, PermissionSet, GrantDenialPair};
use acls_rs::policy::Role;
use acls_rs::Subject;
// ABAC: attribute-based rules
let mut abac = AbacPolicy::new();
let rule = AbacRule::builder("allow_engineers")
.dimension_values("user", vec![
AttributeType::String("group:engineers".into()),
])
.dimension_values("resource", vec![
AttributeType::String("prod:database".into()),
])
.dimension_values("action", vec![
AttributeType::String("read".into()),
])
.enabled(true)
.build();
abac.add_rule(rule).unwrap();
// RBAC: role-based permissions
let mut rbac = RbacPolicy::new();
let engineer_perms = GrantDenialPair::new(
PermissionSet::from([
AtomicPermission::new("database", "read"),
AtomicPermission::new("database", "write"),
]),
PermissionSet::new(),
);
rbac.add_role(Role::new("engineer", engineer_perms));
// Compose: both must agree
let mut policy = ComposedPolicy::new(abac, rbac, CompositionMode::And);
}
Composition modes
| Mode | Behavior |
|---|---|
And | Both ABAC and RBAC must allow. Use for defense in depth. |
Or | Either policy allowing is sufficient. Use for convenience-first environments. |
AbacFirst | ABAC is evaluated first. If ABAC has matching rules, use that decision. Otherwise fall back to RBAC. |
RbacFirst | RBAC is evaluated first. If RBAC has applicable permissions, use that decision. Otherwise fall back to ABAC. |
And mode
The strictest mode. The user must satisfy ABAC attribute requirements and their RBAC roles must grant the required permission:
#![allow(unused)]
fn main() {
use abac_rs::{ComposedPolicy, CompositionMode};
let mut policy = ComposedPolicy::new(abac, rbac, CompositionMode::And);
let mut user = Subject::new("alice");
user.roles.push("engineer".to_string());
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:database".into()), vec![]);
request.add_attribute("action", AttributeType::String("read".into()), vec![]);
// Allowed only if both ABAC and RBAC agree
let result = policy.evaluate(&mut request, &user);
}
Or mode
The most permissive mode. Access is granted if either policy allows:
#![allow(unused)]
fn main() {
let mut policy = ComposedPolicy::new(abac, rbac, CompositionMode::Or);
// Access granted if ABAC OR RBAC allows
let result = policy.evaluate(&mut request, &user);
}
This is useful when you want to grant access through either fine-grained ABAC rules or broad RBAC roles.
AbacFirst mode
ABAC takes precedence. If the ABAC policy has enabled rules for the request dimensions, use that decision. Otherwise fall back to RBAC:
#![allow(unused)]
fn main() {
let mut policy = ComposedPolicy::new(abac, rbac, CompositionMode::AbacFirst);
// If ABAC has matching rules, use ABAC decision
// If ABAC has no matching rules, fall back to RBAC
let result = policy.evaluate(&mut request, &user);
}
This is useful when you want fine-grained control via ABAC where rules exist, but default RBAC permissions elsewhere.
RbacFirst mode
RBAC takes precedence. If the user has roles with relevant permissions, use that decision. Otherwise fall back to ABAC:
#![allow(unused)]
fn main() {
let mut policy = ComposedPolicy::new(abac, rbac, CompositionMode::RbacFirst);
// If user has roles, use RBAC decision
// Otherwise fall back to ABAC rules
let result = policy.evaluate(&mut request, &user);
}
Thread safety
ComposedPolicy comes in two variants:
ComposedPolicy- Thread-safe (usesMutexfor caches), can be shared across threadsComposedPolicyLocal- Single-threaded (usesRefCellfor caches), faster but notSend + Sync
Both use the generic ComposedPolicyCore<A, P> type where:
A: CacheLock- Lock type for ABAC evaluation cacheP: PermissionCacheLock- Lock type for RBAC permission cache
#![allow(unused)]
fn main() {
use abac_rs::{ComposedPolicy, ComposedPolicyLocal, CompositionMode};
// Thread-safe variant (Mutex-based)
let policy = ComposedPolicy::new(abac, rbac, CompositionMode::And);
// Single-threaded variant (RefCell-based)
let policy_local = ComposedPolicyLocal::new(abac_local, rbac, CompositionMode::And);
}
RBAC permission mapping
For RBAC evaluation, ComposedPolicy extracts resource and action attributes from the ABAC request:
resourceattribute → permission namespaceactionattribute → permission action
For example, a request with resource="database" and action="read" maps to checking the database:read permission in RBAC.
If these attributes are missing from the request, RBAC evaluation is skipped and only the ABAC result is used.
Performance
ComposedPolicy maintains two caches:
- ABAC evaluation cache - LRU cache of request → decision mappings
- RBAC permission cache - LRU cache of role sets → resolved permissions
You can configure the permission cache size:
#![allow(unused)]
fn main() {
let policy = ComposedPolicy::with_cache_size(
abac,
rbac,
CompositionMode::And,
200, // cache up to 200 unique role combinations
);
}
Call invalidate_permission_cache() when the RBAC policy or role hierarchy changes:
#![allow(unused)]
fn main() {
// Modify RBAC policy
policy.rbac_policy_mut().add_role(new_role);
// Cache was automatically invalidated by rbac_policy_mut()
}
See also
- RBAC Policies - Role-based access control
- ABAC Rules - Attribute-based rules
- Policy Evaluation - ABAC evaluation pipeline
- Python Bindings - Using ComposedPolicy from Python