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

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

ModeBehavior
AndBoth ABAC and RBAC must allow. Use for defense in depth.
OrEither policy allowing is sufficient. Use for convenience-first environments.
AbacFirstABAC is evaluated first. If ABAC has matching rules, use that decision. Otherwise fall back to RBAC.
RbacFirstRBAC 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 (uses Mutex for caches), can be shared across threads
  • ComposedPolicyLocal - Single-threaded (uses RefCell for caches), faster but not Send + Sync

Both use the generic ComposedPolicyCore<A, P> type where:

  • A: CacheLock - Lock type for ABAC evaluation cache
  • P: 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:

  • resource attribute → permission namespace
  • action attribute → 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:

  1. ABAC evaluation cache - LRU cache of request → decision mappings
  2. 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