CRUMB a card from devarno-cloud

AccessControl & Permission Model

aegis beginner 4 min read

ELI5

A guest-list clipboard with sticky notes: each note says who can do what to which document and optionally when it expires. The door staff check the list, tear off expired notes, and can erase a whole row when a guest is removed — but they can’t erase just one permission from a multi-permission row.

Technical Deep Dive

AccessControl in src/access_control.rs is a flat Vec<AccessEntry> with a hard cap of 10,000 entries (max_entries). There is no secondary index — every lookup is a linear scan.

AccessEntry Fields

FieldTypeNotes
user_idStringCaller-supplied; no format validation
doc_idStringCaller-supplied; no format validation
permissionPermission`Read
granted_at_msu64Wall-clock ms at construction
expires_at_msOption<u64>None = never expires

is_valid() checks current wall clock against expires_at_ms. Entries with None are always valid.

Permission Enum

Read → "read"
Write → "write"
Delete → "delete"
Admin → "admin"

check_permission(user_id, doc_id, action) maps action: &str to the enum. An unrecognised string returns Err("Unknown action: ...").

Entry Lifecycle

stateDiagram-v2
[*] --> Valid : grant_permission()
Valid --> Expired : expires_at_ms elapsed
Valid --> Revoked : revoke_permission()
Expired --> Revoked : revoke_permission() (still removed)
Revoked --> [*]

revoke_permission(user_id, doc_id) removes ALL entries matching the (user_id, doc_id) pair regardless of permission type or expiry state. It returns true if any were removed, false if the pair was not found.

Capacity Behaviour

When entries.len() >= 10_000, grant_permission returns Err("Access control table full"). There is no LRU eviction — callers must call clear() or revoke_permission() explicitly to free space.

Key Terms

  • AccessEntry → Single (user, doc, permission) grant with optional expiry; defined in src/access_control.rs
  • Permission → Four-variant enum: Read, Write, Delete, Admin
  • expires_at_ms → Unix millisecond timestamp after which is_valid() returns false
  • max_entries → Hard cap of 10,000 entries; exceeding it blocks new grants
  • revoke_permission → Removes all entries for a (user_id, doc_id) pair in one call

Q&A

Q: grant_permission appends a new entry even if an identical one already exists — does this cause double-grant? A: Yes. Two identical entries means check_permission finds the first matching entry and returns true, which is harmless, but get_permissions returns duplicates. Callers are responsible for idempotency.

Q: What does valid_count() return versus entry_count()? A: entry_count() is the raw Vec length; valid_count() is the subset where is_valid() is true — i.e., non-expired entries. The difference represents stale entries still consuming table capacity.

Q: with_expiry(expires_at_ms) — can it be called after an entry is already inserted? A: No. with_expiry is a builder method on the newly created AccessEntry; once pushed into entries, there is no mutation API. Expiry must be set before calling grant_permission via the internal AccessEntry::new().with_expiry() builder (not directly exposed on AccessControl).

Examples

Grant a time-limited write permission and verify expiry:

let mut ac = AccessControl::new();
// grant with expiry 1 ms in the past → immediately invalid
let expired_ms = 1u64;
let entry = AccessEntry::new("u1", "doc1", Permission::Write).with_expiry(expired_ms);
ac.entries.push(entry); // direct push bypasses grant_permission for testing
let valid = ac.valid_count(); // 0 — entry is expired
assert_eq!(valid, 0);

neighbors on the map