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

Temporal Access: From Organisational Model to File Permissions

Your organisation hires external contractor teams under Statements of Work (SOWs) that define both the access scope and the validity window. Alpha-team may access engineering files during Q1; gamma-team may read legal and HR directories only during a two-week compliance audit. When the window closes, their access disappears — no manual cleanup, no forgotten ACL entries.

This page walks through the full pipeline: model the organisational structure, attach time-bounded permission grants using acls-rs temporal types, derive group-based POSIX ACLs at any point in time, and verify per-user effective access using the POSIX.1e algorithm.

The scenario

Four shared NFS mounts serve different data domains:

MountOwnerGroupModePurpose
/srv/data/engineeringsysadmineng-staff0750Source code, builds
/srv/data/legallegal-adminlegal-staff0750Contracts, IP filings
/srv/data/hrhr-adminhr-staff0750Personnel files
/srv/data/reportssysadminstaff0754Business reports

Four contractor teams, each with a different access window:

TeamPOSIX GroupsWindowMounts
Alphaalpha-lead, alpha-devQ1 2026 (Jan–Mar)engineering
Betabeta-teamH1 2026 (Jan–Jun)reports
Gammagamma-teamAudit (Feb 1–Mar 14)legal, hr
Deltadelta-team, delta-lead, delta-engMigration (Mar 1–14)engineering, reports

Different permissions within a team are handled by separate POSIX groups. Alice (alpha-team lead) is in alpha-lead and gets rwx; Bob (developer) is in alpha-dev and gets r-x. The ACL itself only contains group:name:perm entries — no per-user ACEs.

Step 1: Define the temporal grants

Each grant is a TemporalPermissionSet from acls-rs. The three rwx bits are stored as AtomicPermission("posix", "read"), ("posix", "write"), and ("posix", "execute"), each with a valid_from / valid_until window:

#![allow(unused)]
fn main() {
use acls_rs::permission::temporal::{TemporalPermission, TemporalPermissionSet};
use acls_rs::permission::AtomicPermission;

fn tps(bits: u8, from: u64, until: u64) -> TemporalPermissionSet {
    let mut s = TemporalPermissionSet::new();
    if bits & 0b100 != 0 {
        s.add(TemporalPermission::new(
            AtomicPermission::new("posix", "read"), Some(from), Some(until)));
    }
    if bits & 0b010 != 0 {
        s.add(TemporalPermission::new(
            AtomicPermission::new("posix", "write"), Some(from), Some(until)));
    }
    if bits & 0b001 != 0 {
        s.add(TemporalPermission::new(
            AtomicPermission::new("posix", "execute"), Some(from), Some(until)));
    }
    s
}

// Alpha-team leads: rwx on engineering, Q1 2026
let alpha_lead_eng = tps(0b111, Q1_FROM, Q1_UNTIL);

// Gamma-team: read-only on legal, audit window
let gamma_legal = tps(0b100, AUDIT_FROM, AUDIT_UNTIL);
}

At any point in time, effective_at(t) returns only the bits whose windows overlap t. Converting back to a PosixPerm tells you the team’s ceiling:

#![allow(unused)]
fn main() {
use posix_acls::PosixPerm;

let ceiling = PosixPerm::from_permission_set(&alpha_lead_eng.effective_at(time));
// Before Q1: PosixPerm::NONE
// During Q1: PosixPerm::RWX
// After Q1:  PosixPerm::NONE
}

Step 2: Build group-based POSIX ACLs

For each mount at a given time, collect the active grants and produce named-group ACEs:

#![allow(unused)]
fn main() {
use posix_acls::{AclBuilder, PosixPerm};

fn acl_at(mount: &MountDef, grants: &[GroupGrant], time: u64)
    -> posix_acls::PosixAcl
{
    let mut builder = AclBuilder::new(mount.owner, mount.owning_group)
        .owner_perms(mount.owner_perms)
        .owning_group_perms(mount.group_perms)
        .other_perms(mount.other_perms);

    for grant in grants.iter().filter(|g| g.mount == mount.path) {
        let ceiling = grant.ceiling_at(time);
        if ceiling != PosixPerm::NONE {
            builder = builder.add_group(grant.group, ceiling);
        }
    }
    builder.build()
}
}

During the March 10 migration peak this produces, for /srv/data/engineering:

# owner: sysadmin
# group: eng-staff
user::rwx
group::r-x
group:alpha-dev:r-x
group:alpha-lead:rwx
group:delta-team:rwx
mask::rwx
other::---

After April 1, the same function returns a minimal ACL with no named groups:

# owner: sysadmin
# group: eng-staff
user::rwx
group::r-x
other::---

No groups to clean up — they simply produce no ACEs when their windows are closed.

Step 3: Derive per-user effective access

Per-user access is computed by PosixAcl::check_access, which runs the full POSIX.1e algorithm: scan named-group entries, join all matching groups’ masked permissions. No per-user ACEs are needed:

#![allow(unused)]
fn main() {
let acl = acl_at(&engineering, &grants, march_10);

let user_groups = vec!["alpha-lead"];
let result = acl.check_access("alice", &user_groups, PosixPerm::NONE);
// result.effective_perms == PosixPerm::RWX
// result.matched_tag == AclTag::Group("alpha-lead")

let user_groups = vec!["alpha-dev"];
let result = acl.check_access("bob", &user_groups, PosixPerm::NONE);
// result.effective_perms == PosixPerm::RX
// result.matched_tag == AclTag::Group("alpha-dev")
}

Users with no matching contractor group fall through to other::, which gives them no access (---).

Step 4: Generate setfacl commands

The POSIX ACL text output maps directly to setfacl invocations:

# Applied on each access-review cycle for /srv/data/engineering:
setfacl --remove-all --recursive /srv/data/engineering
setfacl -m u::rwx,g::r-x,o::--- /srv/data/engineering
setfacl -m g:alpha-lead:rwx -R /srv/data/engineering
setfacl -m g:alpha-dev:r-x -R /srv/data/engineering
setfacl -m g:delta-team:rwx -R /srv/data/engineering
setfacl -m m::rwx -R /srv/data/engineering

Group membership is established once at on-boarding:

groupadd alpha-lead
usermod -a -G alpha-lead alice
groupadd alpha-dev
usermod -a -G alpha-dev bob

The periodic ACL review cycle only runs setfacl — group membership is stable.

Step 5: Track ACL evolution over time

By evaluating the same grants at five points in time, you get a complete audit trail of what changed and when:

Dateengineeringlegalhrreports
2026-01-15alpha-lead:rwx, alpha-dev:r-xbeta-team:r–
2026-02-10(same)gamma-team:r–gamma-team:r–(same)
2026-03-10+delta-team:rwx(same)(same)+delta-lead:rwx, +delta-eng:r-x
2026-04-01(all removed)(removed)(removed)beta-team:r–
2026-06-30beta-team:r– (expires tonight)

Each transition is deterministic: the temporal grant’s window either overlaps the evaluation time or it doesn’t. There is no state to track, no cleanup script to forget to run.

Step 6: Point-in-time access queries

Can dave (gamma-team auditor) read /srv/data/hr?

DateWindowVerdict
2026-01-15before auditdenied
2026-02-10audit activegranted (r--)
2026-03-10audit activegranted (r--)
2026-03-15audit closeddenied

The answer comes from evaluating the temporal grant at the query time, building the ACL, and running check_access. No filesystem access required — the model is self-contained.

Python equivalent

The same scenario works from Python using acls_rs for temporal types and posix_acls for ACL construction:

from acls_rs import AtomicPermission, TemporalPermission, TemporalPermissionSet
from posix_acls import AclBuilder, PosixPerm

# Build a temporal permission set: rwx during Q1 2026
tps = TemporalPermissionSet()
tps.add(TemporalPermission(AtomicPermission("posix", "read"), Q1_FROM, Q1_UNTIL))
tps.add(TemporalPermission(AtomicPermission("posix", "write"), Q1_FROM, Q1_UNTIL))
tps.add(TemporalPermission(AtomicPermission("posix", "execute"), Q1_FROM, Q1_UNTIL))

# At a given time, extract the active bits
perm_set = tps.effective_at(time)
has_read = AtomicPermission("posix", "read") in perm_set

# Build a group-based ACL
acl = (AclBuilder("sysadmin", "eng-staff")
    .owner_perms(PosixPerm.RWX.bits)
    .owning_group_perms(PosixPerm.RX.bits)
    .other_perms(PosixPerm.NONE.bits)
    .add_group("alpha-lead", PosixPerm.RWX.bits)
    .add_group("alpha-dev", PosixPerm.RX.bits)
    .build())

# Check access for a user in the alpha-lead group
result = acl.check_access("alice", ["alpha-lead"], PosixPerm.NONE.bits)
print(result.effective_perms)  # rwx
print(result.granted)          # True

The complete Python example is in crates/posix-acls-python/examples/contractor_access.py.

Key design points

  1. Grants live at the group level. The ACL contains group:name:perm entries, never user:name:perm. Role differentiation (lead vs developer) is expressed as separate POSIX groups, each with its own grant.

  2. Temporal windows are declarative. Each grant carries a TemporalPermissionSet with explicit valid_from / valid_until. No cron jobs, no cleanup scripts — the evaluation function queries the model at a given time and produces the correct ACL.

  3. Per-user access is derived, not stored. PosixAcl::check_access runs the POSIX.1e algorithm (join all matching groups’ masked permissions) to compute what each user can actually do. The report is a view, not a separate data structure.

  4. The same model drives setfacl commands. The getfacl text output maps 1:1 to setfacl -m g:group:perm invocations. The periodic review cycle is: evaluate grants at current time → generate ACLs → apply with setfacl.

  5. The audit trail is inherent. Evaluating at consecutive times produces a diff of group ACE additions and removals, exactly what a compliance audit needs.

See also

  • acls-rs Bridge — the posix:read/write/execute namespace convention that connects POSIX permissions to acls-rs temporal types.
  • Temporal Permissions — how TemporalPermission and TemporalPermissionSet work in acls-rs.
  • The full Rust example: crates/posix-acls/examples/contractor_access.rs
  • The full Python example: crates/posix-acls-python/examples/contractor_access.py