Interactive demo
RBAC playground
Menu and guard both read the permission map.
the app
Billing
guard: billing.manage
the permission map
granted = [dashboard.read, reports.read, audit.read, billing.manage, settings.manage]
- dashboarddashboard.read
- reportsreports.read
- auditaudit.read
- billingbilling.manage
- settingssettings.manage
Menu items the guard refuses
0
— zero by construction: the menu is the map, filtered.
What just happened
Navigation visibility and route authorisation are the same question asked twice: may this person open this module? In derived mode it is asked once. One permission map answers it, the menu is that map filtered, and the guard is that map checked. The count of menu items the guard would refuse is zero, and it is zero by construction rather than by vigilance.
In broken mode the menu comes from a hand-written list instead. Nothing about that list is stupid — it is what a menu looks like when somebody writes it at build time, and it is correct on the day it ships. It goes wrong quietly, later, when a permission changes and only the guard is updated. Switch to viewer and Billing is still in the menu; open it and you are on a 403 with no way forward.
Why the 403 is the point
Nothing crashed. No error was logged. Two lists simply fell out of step, and the person who clicked now believes the application is broken — or worse, that they were supposed to have access and something took it away. That is the cost of the second list, and it is paid in trust rather than in exceptions.
The trade-off
Deriving the menu from permissions means the menu is only as expressive as the map. Ordering, grouping, a module that should be visible but disabled rather than hidden — each of those has to become a property of the map instead of a decision made in the menu. That is more upfront design than a hand-written list, and it is the whole cost.
Worth naming the other half: hiding a menu item is not authorisation. The guard is what enforces the rule, and the server is what enforces the guard. A derived menu removes a confusing dead end from the interface — it is not a security boundary, and nothing here treats it as one.
What is simulated
The roles, permissions and modules are generic and invented for this page. There is no server: the map is a plain object, the guard is a set lookup, and both run in the browser.