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:
| Mount | Owner | Group | Mode | Purpose |
|---|---|---|---|---|
/srv/data/engineering | sysadmin | eng-staff | 0750 | Source code, builds |
/srv/data/legal | legal-admin | legal-staff | 0750 | Contracts, IP filings |
/srv/data/hr | hr-admin | hr-staff | 0750 | Personnel files |
/srv/data/reports | sysadmin | staff | 0754 | Business reports |
Four contractor teams, each with a different access window:
| Team | POSIX Groups | Window | Mounts |
|---|---|---|---|
| Alpha | alpha-lead, alpha-dev | Q1 2026 (Jan–Mar) | engineering |
| Beta | beta-team | H1 2026 (Jan–Jun) | reports |
| Gamma | gamma-team | Audit (Feb 1–Mar 14) | legal, hr |
| Delta | delta-team, delta-lead, delta-eng | Migration (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:
| Date | engineering | legal | hr | reports |
|---|---|---|---|---|
| 2026-01-15 | alpha-lead:rwx, alpha-dev:r-x | — | — | beta-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-30 | — | — | — | beta-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?
| Date | Window | Verdict |
|---|---|---|
| 2026-01-15 | before audit | denied |
| 2026-02-10 | audit active | granted (r--) |
| 2026-03-10 | audit active | granted (r--) |
| 2026-03-15 | audit closed | denied |
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
-
Grants live at the group level. The ACL contains
group:name:permentries, neveruser:name:perm. Role differentiation (lead vs developer) is expressed as separate POSIX groups, each with its own grant. -
Temporal windows are declarative. Each grant carries a
TemporalPermissionSetwith explicitvalid_from/valid_until. No cron jobs, no cleanup scripts — the evaluation function queries the model at a given time and produces the correct ACL. -
Per-user access is derived, not stored.
PosixAcl::check_accessruns 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. -
The same model drives
setfaclcommands. Thegetfacltext output maps 1:1 tosetfacl -m g:group:perminvocations. The periodic review cycle is: evaluate grants at current time → generate ACLs → apply withsetfacl. -
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/executenamespace convention that connects POSIX permissions to acls-rs temporal types. - Temporal Permissions — how
TemporalPermissionandTemporalPermissionSetwork 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