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 Permissions

Your platform hires a contractor for a three-month engagement. They need read access to the codebase, but that access must disappear automatically when the contract ends. An on-call engineer needs emergency admin access for 24 hours after an incident, not forever. A database migration requires production write access during a four-hour maintenance window.

Temporal permissions model all of these: access that activates at a specific time and expires at another.

Creating temporal permissions

A TemporalPermission wraps an AtomicPermission with optional start and end timestamps. Timestamps are milliseconds since the Unix epoch.

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

let now = current_timestamp_millis();
let three_months = 90 * 24 * 60 * 60 * 1000; // in milliseconds

let contractor_access = TemporalPermission::new(
    AtomicPermission::new("code", "read"),
    Some(now),                     // active immediately
    Some(now + three_months),      // expires in 3 months
);

// Valid right now
assert!(contractor_access.is_currently_valid());

// Valid at a specific time
assert!(contractor_access.is_valid_at(now + 1000));

// Expired after the window
assert!(!contractor_access.is_valid_at(now + three_months + 1));
}

Validity windows

The valid_from and valid_until fields are both optional, giving you four patterns:

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

let perm = AtomicPermission::new("system", "access");
let now = current_timestamp_millis();

// Active now, expires later
let expiring = TemporalPermission::new(perm.clone(), None, Some(now + 3600_000));

// Activates in the future, never expires
let deferred = TemporalPermission::new(perm.clone(), Some(now + 86400_000), None);

// Active during a specific window
let window = TemporalPermission::new(
    perm.clone(),
    Some(now + 3600_000),
    Some(now + 7200_000),
);

// Always valid (equivalent to a regular permission)
let permanent = TemporalPermission::new(perm.clone(), None, None);
}

Attaching temporal permissions to subjects

Subjects carry a TemporalPermissionSet alongside their regular permissions. Add temporal permissions with .add() and query what is effective at a given time with .effective_at():

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

let now = current_timestamp_millis();
let one_day = 24 * 60 * 60 * 1000;

let mut engineer = Subject::new("alice");

// Regular permission: always has code:read
engineer.grant(AtomicPermission::new("code", "read"));

// Temporal permission: admin access for 24 hours
engineer.temporal_permissions.add(TemporalPermission::new(
    AtomicPermission::new("system", "admin"),
    Some(now),
    Some(now + one_day),
));

// Right now: both permissions are effective
let effective_now = engineer.effective_permissions_at(now + 1000);
assert!(effective_now.contains(&AtomicPermission::new("code", "read")));
assert!(effective_now.contains(&AtomicPermission::new("system", "admin")));

// After 24 hours: only the regular permission remains
let effective_later = engineer.effective_permissions_at(now + one_day + 1000);
assert!(effective_later.contains(&AtomicPermission::new("code", "read")));
assert!(!effective_later.contains(&AtomicPermission::new("system", "admin")));
}

Temporal permission sets

When you need to manage multiple temporal permissions as a collection, TemporalPermissionSet provides batch operations:

#![allow(unused)]
fn main() {
use acls_rs::prelude::*;
use acls_rs::permission::{TemporalPermissionSet, current_timestamp_millis};

let now = current_timestamp_millis();
let mut temp_perms = TemporalPermissionSet::new();

temp_perms.add(TemporalPermission::new(
    AtomicPermission::new("database", "write"),
    Some(now),
    Some(now + 3600_000),
));

temp_perms.add(TemporalPermission::new(
    AtomicPermission::new("deploy", "staging"),
    Some(now),
    Some(now + 86400_000),
));

// What permissions are effective right now?
let active = temp_perms.currently_effective();

// What permissions will be effective at a specific time?
let at_time = temp_perms.effective_at(now + 7200_000);
}

Use cases

Contractor access

A contractor needs three months of read-only access:

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

let now = current_timestamp_millis();
let three_months = 90 * 24 * 60 * 60 * 1000;

let mut contractor = Subject::new("contractor-jane");
contractor.temporal_permissions.add(TemporalPermission::new(
    AtomicPermission::new("project", "read"),
    Some(now),
    Some(now + three_months),
));
}

Emergency access

An on-call engineer gets 24-hour admin access after an incident:

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

let now = current_timestamp_millis();
let one_day = 24 * 60 * 60 * 1000;

let mut oncall = Subject::new("alice");
oncall.temporal_permissions.add(TemporalPermission::new(
    AtomicPermission::new("system", "admin"),
    Some(now),
    Some(now + one_day),
));
}

Maintenance window

A DBA gets production write access during a scheduled window:

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

let window_start = 1737338400_000; // 2025-01-20 02:00 UTC
let window_end   = 1737352800_000; // 2025-01-20 06:00 UTC

let mut dba = Subject::new("dba-bob");
dba.temporal_permissions.add(TemporalPermission::new(
    AtomicPermission::new("production", "write"),
    Some(window_start),
    Some(window_end),
));
}

Best practices

  • Always set an expiration. Open-ended temporary access defeats the purpose.
  • Use UTC timestamps internally. Convert to local time only for display.
  • Build in a small grace period to account for clock skew between systems.
  • Log temporal grants and expirations for audit purposes.
  • Periodically check for expired permissions and clean up stale entries.

Next steps

So far, acls-rs has been controlling what actions users can perform within your application. But as your organization adopts FreeIPA for centralized identity management, a different question arises: which users can access which services on which hosts? This is host-based access control, and it is the domain of hbac-rs.

See hbac-rs: HBAC Evaluation.