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

Permissions and Subjects

Your platform’s first task is straightforward: an internal file-sharing service where some people can read documents and others can also write them. This chapter introduces the types that model that requirement.

Atomic permissions

An AtomicPermission is the smallest unit of access control – a single action on a single resource namespace.

#![allow(unused)]
fn main() {
use acls_rs::permission::AtomicPermission;

let read_file = AtomicPermission::new("file", "read");
let write_db  = AtomicPermission::new("database", "write");

// String representation is "namespace:action"
assert_eq!(read_file.to_string(), "file:read");
}

You can also parse permissions from strings:

#![allow(unused)]
fn main() {
use std::str::FromStr;
use acls_rs::permission::AtomicPermission;

let perm = AtomicPermission::from_str("server:admin").unwrap();
assert_eq!(perm.namespace(), "server");
assert_eq!(perm.action(), "admin");
}

Choose namespaces that reflect your domain (file, database, deploy, api) and actions that are clear verbs (read, write, delete, admin).

Permission sets

A PermissionSet collects atomic permissions into a set with algebraic operations. Two teams on your platform – frontend and backend developers – have overlapping permission needs. Sets let you compute what they share and where they differ.

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

let frontend = PermissionSet::from([
    AtomicPermission::new("ui", "deploy"),
    AtomicPermission::new("api", "read"),
]);

let backend = PermissionSet::from([
    AtomicPermission::new("api", "read"),
    AtomicPermission::new("api", "write"),
    AtomicPermission::new("database", "query"),
]);

// Union: everything either team can do
let combined = frontend.clone().combine(backend.clone());
assert!(combined.contains(&AtomicPermission::new("ui", "deploy")));
assert!(combined.contains(&AtomicPermission::new("database", "query")));

// Intersection: what both teams share
let shared = frontend.clone().meet(backend.clone());
assert!(shared.contains(&AtomicPermission::new("api", "read")));
assert!(!shared.contains(&AtomicPermission::new("ui", "deploy")));

// Difference: what frontend has that backend does not
let frontend_only = frontend.difference(&backend);
assert!(frontend_only.contains(&AtomicPermission::new("ui", "deploy")));
}

The same operations are available as operators: | for union, & for intersection, - for difference.

Grant/denial pairs

Sometimes you need to explicitly block a permission even when other rules would grant it. A GrantDenialPair separates granted and denied permissions, with a simple precedence rule: denials always win.

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

// Start with an empty pair
let mut pair = GrantDenialPair::empty();

// Grant read and delete
let read = AtomicPermission::new("file", "read");
let delete = AtomicPermission::new("file", "delete");
pair.grants.extend(PermissionSet::from([read.clone(), delete.clone()]));

// Deny delete -- overrides the grant
pair.denials.extend(PermissionSet::from([delete.clone()]));

// Effective permissions: only read survives
assert!(pair.has_permission(&read));
assert!(!pair.has_permission(&delete));

let effective = pair.effective_permissions();
assert!(effective.contains(&read));
assert!(!effective.contains(&delete));
}

You can also construct a pair directly:

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

let grants = PermissionSet::from([
    AtomicPermission::new("file", "read"),
    AtomicPermission::new("file", "write"),
]);
let denials = PermissionSet::from([
    AtomicPermission::new("file", "delete"),
]);
let pair = GrantDenialPair::new(grants, denials);
}

Subjects

A Subject represents a user (or service account) with permissions and role memberships. The builder pattern is the idiomatic way to create subjects:

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

let alice = Subject::builder()
    .id("alice")
    .grant(AtomicPermission::new("file", "read"))
    .grant(AtomicPermission::new("file", "write"))
    .role("developers")
    .role("team-leads")
    .build()
    .unwrap();

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

You can also create a subject and modify it incrementally:

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

let mut bob = Subject::new("bob");
bob.grant(AtomicPermission::new("file", "read"));
bob.deny(AtomicPermission::new("file", "delete"));
bob.roles.push("interns".to_string());

// Bob can read but not delete
assert!(bob.has_permission(&AtomicPermission::new("file", "read")));
assert!(!bob.has_permission(&AtomicPermission::new("file", "delete")));
}

Resources

A Resource models a protected object with required permissions. It answers the question “can this subject access this resource?” by checking whether the subject holds every required permission.

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

let alice = Subject::builder()
    .id("alice")
    .grant(AtomicPermission::new("file", "read"))
    .build()
    .unwrap();

let report = Resource::new("quarterly-report.pdf")
    .requires(AtomicPermission::new("file", "read"))
    .resource_type("file");

assert!(report.can_access(&alice));

// If a resource requires more than the subject has, access is denied
let admin_panel = Resource::new("admin-panel")
    .requires(AtomicPermission::new("system", "admin"));

assert!(!admin_panel.can_access(&alice));

// missing_permissions tells you exactly what is lacking
let missing = admin_panel.missing_permissions(&alice);
assert!(missing.contains(&AtomicPermission::new("system", "admin")));
}

Permission deltas

When you need to describe a batch change – “add write access, remove read access” – PermissionDelta captures the transformation:

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

let delta = PermissionDelta::builder()
    .grant_str("file", "write")
    .remove_str("file", "read")
    .build();

let before = PermissionSet::from([
    AtomicPermission::new("file", "read"),
]);

let after = delta.apply_to(before);
assert!(after.contains(&AtomicPermission::new("file", "write")));
assert!(!after.contains(&AtomicPermission::new("file", "read")));
}

Next steps

Individual permissions work when you have a handful of users. As your platform grows to dozens of people across engineering, ops, and product teams, managing permissions per person becomes unwieldy. The next chapter introduces roles – named bundles of permissions that you assign to users instead.

See RBAC Policies.