can() never consults a constraint; inheritance still only widens; precedence is still one rule.
Declaring constraints
["erp.vendors.*"]), so a constraint survives the vendor tab growing new permissions.
Declarations are validated at boot, with the failure modes that would make a constraint lie rejected loudly:
- fewer than two sets, or an empty set, which cannot express an exclusion;
- a pattern matching no declared permission, which is a typo and not a control;
- a key falling in two sets of the same constraint, since every holder would be a violator, which is a declaration bug;
- a pattern reaching into an open import region, because a constraint over keys the catalog cannot enumerate cannot be checked, and a control that silently checks nothing is worse than a refusal.
The detective report
can() sees: access inherited through groups and roles, expiry filtered, and revokes respected fail-closed, so a violation names only access the user genuinely holds right now, and a personal revoke on one side clears it.
Scope is deliberately ignored: Vendor Admin on project A plus Payment Approver on project B still co-locates both capabilities in one person, which is what the control exists to prevent. A deployment that wants scoped tolerance splits the constraint.
Candidates are every user the store can name: user records plus user-subject grant rows. Access arriving purely through everyone or an org subject reaches users the store never heard of; pass { userIds } to check a population you enumerate yourself. Run the report on a schedule and alert on non-empty; it is a read, with no writes and no locks.
Preventive enforcement, opt-in
- Only a user-subject grant that would create a new violation is rejected. Pre-existing violations never block unrelated writes, because the report owns those.
- Group- and role-shaped writes pass through. Rejecting them would mean evaluating every member on a write path; the detective report catches the members a group write violated. The test suite pins this boundary.
enforce: "off"): observe first, tighten once the report is clean.
Why detective-first is the design
A preventive-only SoD in a union-only system would need a deny somewhere in evaluation, and denies in evaluation are exactly the bug class Alfiz’s precedence model exists to exclude. Keeping constraints out ofcan() preserves every invariant the system is built on (cycle condensation, merge safety, single-rule precedence) while making the control expressible, reportable, and (at the grant edge, where it is cheap and precise) enforceable.
For the export a reviewer signs alongside this report, see Access reviews.