frontend/docs/authz-guide.md
How to structure a page so it works with the permission system.
We are migrating from the ADMIN | EDITOR | VIEWER roles to per-action checks. Instead of granting VIEWER and
exposing everything, a user can now be granted access to a single resource, eg: Logs only.
Check whether the resource your page represents is supported: see permissions.type.ts for the current resources and their allowed verbs.
If the resource is not listed there, skip authz for now. The backend does not enforce it yet, so any frontend check would be decorative. Revisit once the resource is generated into that file (it is auto-generated from the backend).
These hold for every page. The per-pattern sections below only add to them.
update requires both read and update. A user who can write but cannot read the current value must not get
an edit affordance.delete is independent of read. Delete controls stay visible for a user who holds delete but not read.list, render every row. Check read only when the row is opened (drawer or
detail route).read on Service Accounts can still hold
create on API Keys, so blocking the outer container would hide work they are allowed to do.attach, detach, assignee) behave like delete: gate the control that triggers
them, not the surrounding content.Without list, but with any of read / create / update, only the table is blocked:
create.Without read:
withAuthZContent, not the whole route.Without read:
read.Without create:
| Entry point | Gate with |
|---|---|
| Button | AuthZButton |
| Dedicated create page | withAuthZPage |
| Create drawer opened directly (deep link) | withAuthZContent on the body + AuthZButton on footer actions |
Gate the control with AuthZButton, or AuthZTooltip for a non-button trigger such as an icon or menu item.
Blocking is always a visible denial, never a silent removal. Use the components in
lib/authz/components rather than hand-rolling a check, they carry the
denial message and the loading state.
| Scope | Component | Denied state |
|---|---|---|
| Button | AuthZButton | Disabled + tooltip |
| Any element | AuthZTooltip | Disabled child + tooltip |
| Section | withAuthZContent / AuthZGuardContent | Inline PermissionDeniedCallout |
| Page / route | withAuthZPage / AuthZGuardPage | PermissionDeniedFullPage |
| Custom fallback | withAuthZ / AuthZGuard | Whatever you pass as fallback |
Prefer the HOC (withAuthZ*); reach for the JSX guard (AuthZGuard*) when the gate depends on conditional rendering
and an HOC cannot express it. The components README has the full decision tree and how to build the checks array.
Use the existing components. Create a new one only if none fit.
withAuthZ* variant directly, or must I extract the content into a component first?
Press Cmd + K (Ctrl + K on Windows/Linux) to open the shortcuts, search for AuthZ, and pick the first
result. The devtool simulates granted and denied permissions across the UI.
Available only in local development, it is stripped from production builds.
lib/authz/utils/README.md covers the *.authz.test.tsx naming convention, the
MSW handlers (setupAuthzAdmin, setupAuthzDenyAll, setupAuthzDeny, setupAuthzAllow,
setupAuthzGrantByPrefix), and how to test the loading state.