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.