Per-resource authorization and membership lifecycle
#14 in tonybierman/arium — merged 2026-05-26
Adds arium's per-resource authorization layer: a ResourceRole lattice
(Viewer < Editor < Admin < Owner), a ResourceAuthority enforcement gate
(require_resource), and a MembershipStore lifecycle layer whose composites
own the invariants apps otherwise reinvent — grant, revoke, transfer, and the
reverse "what can this user see?" enumeration. The app supplies storage-shaped
primitives; arium sequences them inside one transaction so count-then-write
checks can't be raced. Ships both a batteries-included SqlMembershipStore
and the trait for apps that own their own table.
Orphan-resource guards
The lifecycle composites refuse to leave a resource with zero owners:
revoke_membership— refuses to remove the sole Owner (LastOwner).transfer_ownership— promotes the new owner and demotes the old one atomically (never a window with two owners or none), and is a no-op whenfrom == to(which would otherwise collapse the upsert pair onto one row and net-demote the only owner).grant_membership— besides bounding the granted role by the actor's tier, now reads the target's current role in-tx: an actor can't modify a member who outranks them (an Admin can't demote the Owner), and demoting the sole Owner is refused (LastOwner), mirroring the revoke guard. Without this, a grant/upsert was a back door to the same ownerless state revoke and transfer already prevented.
Tests
crates/arium/tests/membership.rs covers the full matrix: last-owner refusal,
non-last-owner removal, atomic transfer, actor-authority limits, the new
self-demote / admin-can't-demote-owner / non-last-owner-demotable /
self-transfer-no-op cases, and the SqlMembershipStore round-trip.
🤖 Generated with Claude Code
Last updated 2026-05-27
Links to this note
Merged pull requests, newest first.