Outcome
By the end of this lesson, you will be able to recognize how ZopNight’s frontend gates UI based on RBAC, distinguish the frontend gate (UX) from the gateway check (security), and reason about what happens when the two disagree.
| Prerequisites | M4.1.L1-L5 (policy table, roles, scope) |
| Time | 9 minutes |
1. Concept
The ZopNight frontend uses a single hook, usePermission(), to decide whether to render an action. The hook reads the user’s policy set, which the app fetches from the API, checks it against one required policy string such as recommendation:update, and returns a boolean. UI components condition their render on the boolean.
const canApply = usePermission('recommendation:update')
return <ApplyRemediateButton disabled={!canApply} />The hook is the convention. Every gated UI element uses it. The result is uniform behavior across the product; every page, every button, every settings section gates with the same primitive.
How it works
1. User signs in (login or SSO).2. The app calls GET /permissions once and keeps the user's policy list in an in-memory store.3. usePermission('entity:verb') looks the policy up in that store. A scoped policy such as resource:view:ec2 also satisfies a check for resource:view.4. Components render conditionally.5. The store is refetched when the browser tab regains focus, so a role change made elsewhere shows up without a reload.
No extra API call per check. While the list is still loading, thehook fails open (buttons appear) and the gateway enforces on write.This design matters at scale. A typical Inventory page renders 50-100 row-level action gates (Stop, Schedule, Override, View Details, etc.). With an API-per-check design, the page would issue hundreds of requests. With one fetched policy list, the page renders in one paint cycle.
Where it’s used
SURFACE EXAMPLE GATE──────────────────────────────────────────────────────────Page-level Redirect if user lacks the page's policy (e.g., /audit-logs requires audit-log:view)
Top-level CTAs "Apply All", "Connect Cloud Account", "Invite User"
Row-level actions Per-row Stop / Schedule / Override disabled if action unavailable
Settings sections Every tab is visible (Viewers hold role:view and cloud-account:view); the actions inside, such as creating a role or connecting an account, are gated
Inline forms Form fields disabled if user cannot edit specific values (e.g., budget threshold requires budget:update)The principle: every interactive element is a gate. If the user cannot perform the action, the element is usually shown disabled; some are hidden instead. Disabled is the common choice because it tells the user the action exists, so they know to ask for it.
The “Access restricted” experience
When a user does manage to hit a gated route (deep link, bookmarked URL, paste from a colleague), they see an explicit panel:
Access restricted─────────────────────────────────────────────────────You do not have permission to view <this page>.Contact your organisation admin to request access.The panel is deliberately short. It does not name the missing policy, the user’s role or their scope, so the admin works that out: check the user’s role in Settings → Roles against the policy the page needs (L1’s table). If many users report the same panel on the same page, the policy is probably in the wrong role.
Frontend gates vs gateway enforcement
The most-asked question in code reviews of this layer: “if the gateway enforces anyway, why gate the frontend at all?” The answer is two-fold.
1. UX: showing actions the user cannot take leads to confused clicks and support tickets. Hiding them is cleaner. The user does not have to learn the permission model through trial and error.
2. Performance: as noted above, gating in the frontend avoids issuing API calls that the gateway will reject. For row-level actions on a 100-row Inventory page, this is the difference between one render and dozens of round-trips.
But, and this is the critical caveat, frontend gating is not the security boundary. A scripted client that ignores the frontend can still issue any API call. The gateway rejects it. The frontend gates exist for users; the gateway exists for everyone.
LAYER ROLE ROLE TYPE──────────────────────────────────────────────────────────Frontend gate UX, performance Not securityGateway check Authorization decision Security boundaryBackend service Applies the user's scope Data filteringIf a frontend gate were to disagree with the gateway (e.g., frontend cached an old policy set), the worst case is a 403 from the gateway and a corresponding “Access restricted” panel. For any route mapped in the policy table, that means never an unauthorized action.
How drift is prevented
Frontend gates and the gateway must reference the same policy primitives. ZopNight prevents drift through:
1. Single source: the policy table (Go code in the config service, streamed to the gateway) is the only declaration of policies.
2. Mirrored names: the frontend keeps the policy names as hand-written constants (permissions.js) and gates with them.
3. Prefix-aware matching on both sides: a scoped policy such as resource:view:ec2 satisfies a check for resource:view in the gateway and in the hook alike.There is no build step that fails on an unknown policy name, so drift is possible. Typical drift bugs: a policy renamed but a frontend reference missed; a new endpoint added without a frontend gate; a custom role granting a policy the frontend did not check. In every case the gateway still decides, so drift costs usability, not security.
How ZopNight uses it
The hook itself is a thin wrapper around a memoized policy lookup:
function usePermission(policy) { const { policies, loaded, error } = usePermissionsStore() if (!loaded) return true // fail open while loading return policies.some((p) => p === policy || p.startsWith(policy + ':'))}The hook takes one policy string and nothing else; it does not check individual resources. Row-level scope is handled by the backend: a team-scoped Editor never receives the product team’s resources in the first place, so there is no product row to gate.
2. Demo
Same product, three users, three rendered surfaces:
USER A: Viewer role Inventory page ✓ Resource list visible ✗ "Stop" button disabled ✗ "Attach Schedule" disabled ✗ Bulk actions disabled Recommendations page ✓ List visible ✗ "Apply" disabled ✗ "Dismiss" disabled Settings ✓ Every tab visible, including Roles and Cloud Accounts ✗ Create, edit and connect actions disabled
USER B: Editor role, platform team Inventory page ✓ Resource list visible (platform team only) ✓ "Stop" enabled on platform resources ✗ Product resources not listed at all (scope filters them out) ✓ "Attach Schedule" enabled Recommendations page ✓ "Apply" enabled on platform recs ✗ Product recs not listed (out of scope)
USER C: Admin role, allResources All pages ✓ Every action visible ✓ "Roles" + "Cloud Accounts" + "Audit Log Export" all visibleThree users, three role/scope combinations, three coherent UIs. The same component code; the gates do the differentiation.
3. Hands-on (6 min)
Open the product in a private window. Log in with two different users (or ask a teammate). Notice differences:
DIFFERENCES OBSERVED:
USER 1 ROLE: __________________ USER 2 ROLE: __________________
Pages where buttons differ: 1. Page: ______________ USER 1 sees: ______________ USER 2 sees: ______________
2. Page: ______________ USER 1 sees: ______________ USER 2 sees: ______________
Settings actions that are disabled for one but not the other: __________________________________________________________
An action one can take and the other cannot: Action: ______________ Why (which policy?): ______________This exercise builds intuition for which policies gate which UI surfaces.
4. Knowledge check
Q1
A button is hidden because the user lacks permission. The user constructs the API call manually using a script. What happens?
A. The script succeeds because the frontend was the only check
B. The script gets cached approval
C. The gateway rejects with 403 Forbidden
D. Random behavior
Show answer
Correct: C. Frontend gating is UX; gateway enforcement is security. The script will see the same authorization failure as a clicked button would. This is the canonical test of the security model. Frontend gates are user-facing; the gateway is the authoritative authorization point. Scripts cannot bypass it because they cannot avoid the gateway.
Q2
The “Access restricted” panel includes:
A. The specific policy required, your current role, and your scope state
B. A short notice naming the page and telling you to contact your admin
C. The raw 403 response body, exactly as the gateway returned it to the client
D. A stack trace from the failing request, useful for debugging it later on
Show answer
Correct: B. The panel says you do not have permission to view the page and to contact your organisation admin. It does not name the policy, role or scope; the admin works those out from the role’s policy list.
Q3
The usePermission() hook reads from:
A. An API call out to the gateway on every single permission check made
B. A local configuration file bundled in with the frontend build
C. The user’s JWT claims, decoded in the browser at page load time
D. A policy list fetched once from the API and kept in an in-memory store
Show answer
Correct: D. The app fetches the policy list from GET /permissions and keeps it in memory, so there is no API call per check. It refetches when the browser tab regains focus, so role changes show up without a reload.
5. Apply
The hook is invoked by every gated component already; no per-feature configuration needed. As an admin, validate the gates work by inspecting a Viewer’s perspective in a private browser window.
If a feature lacks a gate (an Editor sees an Admin-only button enabled), file an issue. The gateway will still refuse the action, but the gate is missing.
Related lessons
Module quiz
Complete M4.1 → 10-question module quiz.