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 HBAC to control which hosts users can access and RBAC to control what actions they can perform within applications. These are separate concerns, but they need to work together: a developer should only deploy to production if HBAC allows them on the host and their RBAC role grants the deploy:production permission.

ComposedPolicy unifies HBAC and RBAC evaluation into a single decision.

Creating a composed policy

ComposedPolicy::new takes an HbacPolicy, an RbacPolicy, and a CompositionMode that determines how the two results are combined:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;
use acls_rs::prelude::*;

// HBAC: who can access which hosts
let mut hbac = HbacPolicy::new();
let ssh_rule = HbacRule::builder("dev_ssh")
    .user_group("developers")
    .host_group("dev-servers")
    .service("sshd")
    .enabled(true)
    .build()
    .unwrap();
hbac.add_rule(ssh_rule);

// RBAC: what actions users can perform
let mut rbac = RbacPolicy::new();
let dev_perms = GrantDenialPair::new(
    PermissionSet::from([
        AtomicPermission::new("code", "read"),
        AtomicPermission::new("code", "write"),
    ]),
    PermissionSet::new(),
);
rbac.add_role(Role::new("developer", dev_perms));

// Compose: both must agree
let policy = ComposedPolicy::new(hbac, rbac, CompositionMode::And);
}

Composition modes

ModeBehavior
AndBoth HBAC and RBAC must allow. Use for defense in depth.
OrEither policy allowing is sufficient. Use for convenience-first environments.
HbacFirstHBAC is evaluated first. If HBAC denies, RBAC is not checked.
RbacFirstRBAC is evaluated first. If RBAC denies, HBAC is not checked.

And mode

The strictest mode. The user must be allowed by HBAC (correct host + service) and their RBAC roles must grant the required permissions:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;

let policy = ComposedPolicy::new(hbac, rbac, CompositionMode::And);

let mut dev = Subject::new("alice");
dev.roles.push("developers".to_string());
let request = HbacRequest::new(dev, "dev-01.example.com", "sshd");

// Allowed only if both HBAC and RBAC agree
let result = policy.evaluate(&request);
}

Or mode

The most permissive mode. Access is granted if either policy allows:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;

let policy = ComposedPolicy::new(hbac, rbac, CompositionMode::Or);
}

HbacFirst / RbacFirst

Fallback modes that prefer one policy over the other:

HbacFirst: Use HBAC’s decision if any rules matched; otherwise fall back to RBAC. This is useful when host-based access control is the primary gatekeeper:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;

// Prefer HBAC rules when they apply, fall back to RBAC permissions
// if no HBAC rules matched
let policy = ComposedPolicy::new(hbac, rbac, CompositionMode::HbacFirst);
}

RbacFirst: Use RBAC’s decision if the user has any permissions; otherwise fall back to HBAC. This is useful when role-based permissions are the primary access control:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;

// Prefer RBAC permissions when user has any, fall back to HBAC rules
// if user has no effective permissions
let policy = ComposedPolicy::new(hbac, rbac, CompositionMode::RbacFirst);
}

Evaluating

ComposedPolicy provides the same evaluation methods as HbacPolicy:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;

let mut dev = Subject::new("alice");
dev.roles.push("developers".to_string());
let request = HbacRequest::new(dev, "dev-01.example.com", "sshd");

// Full result
let result = policy.evaluate(&request);
if result.is_allowed() {
    println!("Access granted by: {:?}", result.matched_rules);
}
// Check for degraded decisions (e.g. RBAC role resolution failure)
if result.has_warnings() {
    eprintln!("Warning: decision may be incomplete: {:?}", result.warnings);
}

// Fast boolean check
let allowed: bool = policy.check_access(&request);
}

RBAC role resolution failures

If RBAC role resolution fails (circular role dependency, missing role definition, depth limit exceeded), evaluate() returns deny and records the reason in result.warnings. This is a fail-safe: silently falling back to direct-permission evaluation could allow privilege escalation via a crafted role graph.

Always check result.has_warnings() if you need to distinguish a clean deny from one caused by a configuration error:

#![allow(unused)]
fn main() {
let result = policy.evaluate(&request);
if result.is_denied() && result.has_warnings() {
    // Denied because RBAC is misconfigured — worth alerting
    for w in &result.warnings {
        eprintln!("Policy warning: {w}");
    }
}
}

Accessing inner policies

You can modify the HBAC or RBAC policy after composition:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;

let mut policy = ComposedPolicy::new(hbac, rbac, CompositionMode::And);

// Add a new HBAC rule
let new_rule = HbacRule::builder("ops_all")
    .user_group("ops")
    .host_category_all()
    .service_category_all()
    .enabled(true)
    .build()
    .unwrap();
policy.hbac_policy_mut().add_rule(new_rule);

// Add a new RBAC role
let ops_perms = GrantDenialPair::new(
    PermissionSet::from([AtomicPermission::new("system", "admin")]),
    PermissionSet::new(),
);
policy.rbac_policy_mut().add_role(Role::new("ops", ops_perms));
}

Layering HBAC with ABAC manually

ComposedPolicy combines HBAC and RBAC. If you also need ABAC (context-aware refinement), layer it manually:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use hbac_rs::prelude::*;
use acls_rs::prelude::*;

// Step 1: Check HBAC host access
let request = HbacRequest::new(
    Subject::new("alice"),
    "prod-01.example.com",
    "deploy-service",
);
let hbac_result = hbac_policy.evaluate(&request);

if !hbac_result.is_allowed() {
    println!("Denied: no host access");
    return;
}

// Step 2: Resolve RBAC permissions
let roles = vec!["developer".to_string()];
let role_perms = rbac_policy.effective_permissions(&roles).unwrap();

// Step 3: Evaluate ABAC context
let mut context: HashMap<String, String> = HashMap::new();
context.insert("network".into(), "corporate".into());
context.insert("time_of_day".into(), "business-hours".into());

let abac_result = abac_policy.evaluate(&context);
let context_perms = abac_result.effective_permissions();

// Step 4: Intersect RBAC and ABAC results
let effective = role_perms.meet(context_perms);

if effective.contains(&AtomicPermission::new("deploy", "production")) {
    println!("Allowed: host access + role + context all agree");
}
}

This pattern evaluates coarse-to-fine: HBAC (host-level) first, then RBAC (role-level), then ABAC (context-level). Short-circuiting at any layer avoids unnecessary work.

Choosing a composition strategy

ScenarioRecommended mode
High-security environmentAnd – defense in depth
User convenience is priorityOr – either policy suffices
Host access is the bottleneckHbacFirst – skip RBAC when host is unreachable
Most denials are permission-basedRbacFirst – skip HBAC when role lacks permissions
Need ABAC tooManual layering (HBAC + RBAC + ABAC)

Next steps

Your access control system is complete: permissions, roles, attributes, time windows, host-based access, and policy composition. Before deploying to production, validate that performance meets your SLA under realistic load.

See Performance Testing Framework.