Grant and revoke roles
Give someone VIP, scoped host access or a staff role — and take it back cleanly.
Owned by Admin · 12 steps · about 20 minutes
Why this exists
Corrected 2026-08-24 (end2end). This section used to say authorisation was "grant-based, not capability-based", that there was "no permissions matrix to tick", and that six groups were the whole list. All three were true once and none of them is true now, which made this the most misleading paragraph in the manual: it is the one somebody reads during an incident.
There are two layers, and you need both. A user holds zero or more grants of a named role. Roles map to capabilities through a policy graph held in the database, and endpoints ask for the capability, not the role — events.publish, events.cancel, tiers.manage. So "who can do X" is answered by the matrix, not by reading route decorators, and the matrix is editable: see build-a-role-from-capabilities.
The role list lives in the database, not in this page. The six original roles — member, vip_member, host, door_staff, venue_manager, admin — are the ones every deployment has, and more are seeded on top: plan 109 added reviewer, a grantable role carrying exactly two capabilities and no staff surface. Ask /api/rbac/groups for the authoritative list; do not count the names in this paragraph.
Three roles imply others, so you never grant the implied one: admin implies everything, venue_manager implies door_staff, vip_member implies member. There is one name you will see in the code, employee, which is a pseudo-group meaning door_staff or venue_manager or admin. It is a gate used by endpoints; it is not grantable, and trying to grant it fails with an unknown group rather than silently doing nothing.
host is the odd one. A host grant carries an event id. The promoter is not "a host" in general — they are a host of exactly that event, and the same person can hold several host grants for several events. Scope is in the grant, not in a separate table, which is why revoking one event's access never touches another's.
Two design decisions bite people, and both are on purpose. First, every grant and revoke requires a reason and writes one row to an append-only audit table — you cannot quietly hand someone the keys. Second, groups carry door zones, so granting or revoking almost any group re-issues the person's membership pass: otherwise a terminated employee would walk around with a card that still opens Zone C.
Before you start
- An admin session. Every route in this workflow is admin-only; a venue manager gets 403 on all of them.
- A target user to practise on — use a demo account, never a real colleague.
- For a scoped host grant, an existing event id (the seed ships demo-event-0001).
Practise with
| Persona | Password | Note | |
|---|---|---|---|
| admin | admin@club.test | admin123 | the only persona that can grant or revoke anything |
| member | freya@demo.club | freya-pass-123 | a plain registered member — safe to promote and demote |
| host_promoter | host@club.test | host123 | already holds a scoped host grant on demo-event-0001 |
| door_staff | door@club.test | door123 | scanner-only; use to see what a staff grant actually opens |
Steps 1–3 — Admin
their manual →-
1Open the user directory and find the person by email. You can filter by group to answer questions like 'who are our admins'.Expected result A paged list of users with their active groups.Watch out for Deactivated users still appear here. Being listed is not the same as being able to log in.
-
2Open the user. Read the three panels before you touch anything: current grants, live sessions, and the audit timeline.Expected result Grants with who granted them and why, a session count, and the last 50 permission events for this person.Watch out for The audit timeline is the answer to 'who gave them that'. Read it before you assume it was a mistake.
-
3Grant vip_member from the grant form on that page, and type a real reason — 'subscription settled, ticket 4417', not 'vip'.Expected result 201 with the new grant row, a flash on the page, and one audit row. The user's VIP tab and Zone B become available.Watch out for An empty reason is refused with 400 reason_required. Granting a group they already hold is 409 duplicate_grant, not a silent no-op. And do not grant member alongside vip_member — vip_member already implies it.
Steps 4 — Member
their manual →It is their account: the grant is what changes what they can buy and where they can walk.
-
4Log in as the target and look at your own account page: your groups, and the sessions you have open.Expected result vip_member appears in your groups. Nothing about the grant is hidden from the person who holds it.Watch out for Group changes are re-read per request, so the new power appears on your very next page load. You do not need to log out and back in.
Steps 5 — Admin
their manual →-
5Now grant a scoped host role: group host, plus the event id it applies to, plus a reason.Expected result A grant row carrying that event_id.Watch out for host without an event id is 400 event_id_required. An event id on any other group is 400 event_id_not_allowed. An event id that does not exist is 400 unknown_event. The scope is checked, not trusted.
Steps 6 — Host / Promoter
their manual →A host grant is scoped to one event, and they must see only that one.
-
6Log in as the promoter and open the event dashboard for the event you were scoped to.Expected result Their own event, with live sales.Watch out for Put a different event id in the URL and they get 403. That is the scope doing its job — and it is also the fastest way to prove a grant was made against the wrong event.
Steps 7 — Admin
their manual →-
7Grant door_staff to a new starter.Expected result A grant row, an audit row, and a re-issued membership pass carrying the staff zone set.Watch out for Do not reach for employee here. It is a pseudo-group the endpoints use as a gate; it is not in the grantable list and asking for it fails with unknown_group.
Steps 8 — Door Staff
their manual →A door_staff grant is the thing that opens the scanner at all.
-
8Log in as the new starter and open the scanner.Expected result The scanner loads. Without a door_staff (or venue_manager or admin) grant it is a 403.Watch out for Door staff get the scanner and the guest list, and nothing else. They cannot see orders, money or users, by design.
Steps 9–12 — Admin
their manual →-
9Pull the group list and read it as the authoritative answer to 'what can I actually grant'.Expected result Every grantable role with its metadata, read from the roles table — six on a bare deployment, more wherever a plan has seeded one (reviewer, from plan 109).Watch out for Corrected 2026-08-24 (end2end): this step used to promise 'the six grantable groups'. The list is data, not a constant, so a count written into the manual goes stale the first time somebody adds a role. If a name is not in this response, no amount of typing will make it grantable — that part has always held.
-
10Revoke a grant using the grant id (not the group name) and a reason. Use the Revoke button on the user page — it already knows the id.Expected result The grant is marked revoked, effective on the person's next request. Their pass is re-issued without that group's zones.Watch out for Revoking the last admin is refused with 409 last_admin, and that check runs inside the write transaction, so two admins revoking each other at the same moment cannot both succeed. Revoking a grant that is already revoked is 409 already_revoked.
-
11Open the RBAC audit log and find the two rows you just created.Expected result Actor, target, group, reason and timestamp for every grant, revoke, trigger and deactivation.Watch out for This table is append-only at the database level. It cannot be edited or deleted — not through the data suite, not through raw SQL. If a reason was wrong, add a new event; you cannot rewrite history.
-
12When someone insists their access is broken, dump their identity: every grant including revoked ones, every session with its status, and their computed zones globally and per event.Expected result One page that usually ends the argument.Watch out for Admin-only debug, and it 404s entirely when debug endpoints are disabled. The password hash is redacted — do not go looking for it.