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

RBAC Policies

Your platform now has 50 employees across engineering, ops, and product teams. Granting file:read and api:write to each person individually does not scale. Role-Based Access Control (RBAC) solves this: define roles that carry permissions, then assign roles to users.

Creating roles

A Role bundles a GrantDenialPair (the permissions the role carries) with a name. Construct the permissions first, then wrap them in a role:

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

let viewer_perms = GrantDenialPair::new(
    PermissionSet::from([
        AtomicPermission::new("file", "read"),
        AtomicPermission::new("api", "read"),
    ]),
    PermissionSet::new(), // no denials
);
let viewer = Role::new("viewer", viewer_perms);

let editor_perms = GrantDenialPair::new(
    PermissionSet::from([
        AtomicPermission::new("file", "read"),
        AtomicPermission::new("file", "write"),
        AtomicPermission::new("api", "read"),
        AtomicPermission::new("api", "write"),
    ]),
    PermissionSet::new(),
);
let editor = Role::new("editor", editor_perms);
}

Building a policy

An RbacPolicy holds a set of roles and resolves the effective permissions for any combination of role names.

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

// (viewer and editor defined as above)

let mut policy = RbacPolicy::new();
policy.add_role(viewer);
policy.add_role(editor);

// Resolve permissions for a user with the "editor" role
let alice_roles = vec!["editor".to_string()];
let alice_perms = policy.resolve_permissions(&alice_roles).unwrap();

assert!(alice_perms.has_permission(&AtomicPermission::new("file", "write")));
}

For a quick check of whether a set of roles grants a specific permission, use effective_permissions:

#![allow(unused)]
fn main() {
let effective = policy.effective_permissions(&alice_roles).unwrap();
assert!(effective.contains(&AtomicPermission::new("api", "write")));
}

Role hierarchy

Roles can inherit from parent roles. This avoids duplicating permissions across related roles. On your platform, a senior developer has everything a developer has, plus the ability to deploy to production:

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

let developer_perms = GrantDenialPair::new(
    PermissionSet::from([
        AtomicPermission::new("code", "read"),
        AtomicPermission::new("code", "write"),
        AtomicPermission::new("ci", "trigger"),
    ]),
    PermissionSet::new(),
);
let developer = Role::new("developer", developer_perms);

let senior_perms = GrantDenialPair::new(
    PermissionSet::from([
        AtomicPermission::new("deploy", "production"),
    ]),
    PermissionSet::new(),
);
let senior = Role::new("senior-developer", senior_perms)
    .with_parent("developer");

let mut policy = RbacPolicy::new();
policy.add_role(developer);
policy.add_role(senior);

// A user with "senior-developer" gets both sets of permissions
let roles = vec!["senior-developer".to_string()];
let perms = policy.effective_permissions(&roles).unwrap();

assert!(perms.contains(&AtomicPermission::new("code", "read")));
assert!(perms.contains(&AtomicPermission::new("deploy", "production")));
}

Multiple role assignment

Users can hold more than one role. The effective permissions are the union of all assigned roles:

#![allow(unused)]
fn main() {
let roles = vec!["developer".to_string(), "reviewer".to_string()];
let perms = policy.resolve_permissions(&roles).unwrap();
}

Cycle detection

RBAC prevents circular inheritance. If role A inherits from B and B inherits from A, resolving permissions returns an error:

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

let role_a = Role::new("a", GrantDenialPair::empty())
    .with_parent("b");
let role_b = Role::new("b", GrantDenialPair::empty())
    .with_parent("a");

let mut policy = RbacPolicy::new();
policy.add_role(role_a);
policy.add_role(role_b);

let roles = vec!["a".to_string()];
match policy.resolve_permissions(&roles) {
    Err(RbacError::CyclicDependency) => {
        // Cycle detected -- the policy is misconfigured
    }
    _ => panic!("expected cycle detection"),
}
}

Conflict resolution

When a user holds multiple roles and one role grants a permission while another denies it, denials take precedence. This is the same rule as GrantDenialPair: denials always win.

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

let ops = Role::new("ops", GrantDenialPair::new(
    PermissionSet::from([AtomicPermission::new("data", "delete")]),
    PermissionSet::new(),
));
let safety = Role::new("safety-lock", GrantDenialPair::new(
    PermissionSet::new(),
    PermissionSet::from([AtomicPermission::new("data", "delete")]),
));

let mut policy = RbacPolicy::new();
policy.add_role(ops);
policy.add_role(safety);

let roles = vec!["ops".to_string(), "safety-lock".to_string()];
let perms = policy.resolve_permissions(&roles).unwrap();

// Denial from safety-lock overrides the grant from ops
assert!(!perms.has_permission(&AtomicPermission::new("data", "delete")));
}

Next steps

Roles handle static permission assignments well. But your platform now supports remote work, and the security team requires that certain operations (database writes, production deployments) are only allowed from the corporate network during business hours. Static roles cannot express this constraint. The next chapter introduces attribute-based access control for context-aware decisions.

See ABAC Policies.