PMaker home
What the user sees should equal what they can doThree kindsPage, action, and data permissions work differently; data is the easiest to missAdd a role layerRBAC: users get permissions through roles; adding people or rights becomes configHide by default in the UIDon't render what they can't use; show-but-disable only when they can applyHiding on the frontend is experience, not securityWrite it into project rulesPut the rules in CLAUDE.md; every API checks permissions on its ownThe backend has to gate again

The same account, two approaches. On the left, "can't do it" is only revealed after clicking; on the right, it never appears.

Permission as View

Features the user has no permission for should not appear and then error out.

Features the user has no permission for should not appear and then error out. What the user sees should equal what they can do. Show things they can't access and then block them, and you're making them hit a wall every time.

What you'll run into:

  • It only says "no permission" after you click, and you don't know who to ask to enable it
  • The frontend hides the button, but the API doesn't gate, so a tweaked request gets through
  • Adding a new role means editing a dozen pages, because permissions are hard-coded all over the place

Three kinds of permission

First sort out which kind it is—the implementation is completely different.

Kind What it controls What it looks like in the UI
Page Which pages you can see Whether the nav item exists. No permission, no entry
Action What you can do on a page Whether the button renders: view, add, edit, delete, approve
Data Which records you can see on the same page The list shows different rows. Sales only sees their own customers

The third kind is the easiest to miss. Page and buttons are gated, but the API returns all the data and the frontend just doesn't show it—that's where most privilege-escalation bugs come from.

Granularity can split one more layer: admin, edit, preview. Admin can assign permissions and delete data; edit can change content; preview can only view. Document products often add independent toggles for download, upload, and public sharing.

How to build RBAC

Don't bind permissions directly to users. Slip a role layer in between, and both adding people and adding permissions turn into configuration. A role is a bundle of permissions; users get roles, and departments or user groups can feed roles too. This is role-based access control (RBAC), which stays manageable when you have hundreds of users instead of binding rights one by one.rbac-wiki

Design it in three steps: list the roles first (by org structure or by system structure), then list the permissions (page, action, data), and finally draw a role-by-permission matching table. Once that table is drawn, most of the permission design is done.

The other model is ACL: keep a separate access list for each object. It's more precise and flexible, but every object needs its own list, so maintenance costs are much higher. It fits files, documents, and other cases that need per-item authorization.

How to handle it in the UI

  • Don't show entries the user has no permission for. They don't appear in the nav; buttons aren't rendered. This is the default.
  • Show-but-disable only in one case: the user knows the feature exists and can apply for it themselves. Then the disabled state needs a line like "contact your admin to enable"; without it, the state says nothing.
  • The backend has to gate again. Hiding on the frontend is an experience decision, not a security one. Without API checks, a tweaked request crosses the line (see the security basics in understand tech).
  • Filter data permission at the query layer. Not "fetch everything and filter on the frontend."
  • Permission changes need feedback. The admin just granted you access; you have to refresh to see it—that's a good moment for a hint.

A note for AI

Put the permission rules into a project file so every API and every render has a basis.

## Permissions

Model: RBAC. User -> Role -> Permission. Never bind permissions directly to users.

Three kinds of permissions, all must be implemented:
- Page permissions: don't render the nav item without permission; also gate direct URL access
- Action permissions: don't render the button without permission; never render-then-disable
- Data permissions: filter in the query conditions; don't fetch everything and filter on the frontend

Granularity: admin / edit / preview.

Hard requirements:
- Hiding on the frontend is only an experience improvement. Every API must
  check permissions independently; never rely on the frontend not sending requests.
- When adding any API, state which permission it needs before writing the implementation.
- For queries that involve data permissions, put the filter into the SQL or ORM query,
  and explain the filtering basis in a code comment.

The exception (show-but-disable) applies only when the user knows the feature
exists and can apply for it themselves; the disabled state must include a line
telling them who to contact to enable it.

References

  1. Role-based access control — Wikipedia