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

acls-rs: Algebraic Access Control

Every application that serves more than one user eventually needs access control. You could scatter if user == "admin" checks throughout your code, but those checks multiply, contradict each other, and resist refactoring. acls-rs provides a foundation of composable building blocks that make permission decisions predictable by construction.

This chapter uses a running example: you are building an internal developer platform where employees deploy services, access servers, and manage infrastructure. The platform starts small and grows, and the access control requirements grow with it.

What acls-rs gives you

Four layers of access control, each building on the previous:

  1. Permissions and subjects – define what actions exist (file:read, database:write) and which users hold them. Permissions form algebraic sets with well-defined union, intersection, and difference operations. Denials explicitly override grants.

  2. RBAC policies – group permissions into named roles (developer, admin, ops) with inheritance hierarchies. Assign roles to users instead of managing permissions individually.

  3. ABAC policies – make access decisions based on runtime attributes (department, network location, clearance level). Useful when static roles cannot capture the policy you need.

  4. Temporal permissions – grant access that activates at a future time or expires automatically. Contractor access, maintenance windows, and emergency overrides all fit this model.

Quick start

Add acls-rs to your Cargo.toml:

[dependencies]
acls-rs = "0.1"

With serde support:

[dependencies]
acls-rs = { version = "0.1", features = ["serde"] }

A minimal example that creates a user, grants permissions, and checks access against a protected resource:

use acls_rs::prelude::*;

fn main() {
    // Define two permissions for a file-sharing service
    let read = AtomicPermission::new("file", "read");
    let write = AtomicPermission::new("file", "write");

    // Create a subject (user) with those permissions
    let alice = Subject::builder()
        .id("alice")
        .grant(read.clone())
        .grant(write.clone())
        .build()
        .unwrap();

    // Define a resource that requires read access
    let document = Resource::new("quarterly-report.pdf")
        .requires(read.clone())
        .resource_type("file");

    // Check whether Alice can access the document
    assert!(document.can_access(&alice));
}

Algebraic guarantees

All permission operations satisfy formal mathematical properties:

  • Associativity: combining three permission sets produces the same result regardless of grouping.
  • Commutativity: the order of combination does not matter.
  • Idempotence: combining a set with itself is a no-op.
  • Identity: combining with the empty set returns the original.

These properties mean you can compose permission sets freely – merge permissions from multiple roles, intersect them with context-based restrictions, subtract revocations – and the result is always well-defined. The library encodes these as Rust traits (Semigroup, Monoid, MeetSemilattice, JoinSemilattice) with comprehensive property-based tests.

  • Permissions and Subjects – the fundamental building blocks: atomic permissions, permission sets, grant/denial pairs, subjects, and resources.
  • RBAC Policies – role-based access control with inheritance and cycle detection.
  • ABAC Policies – attribute-based access control for context-sensitive decisions.
  • Temporal Permissions – time-limited access with automatic expiration.