Skip to main content
Your progress
0 of 7 complete0%
T4 / M4.1 / L6 OF 6 / Architect LEVEL / 9 min

Frontend gating with usePermission()

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.


PrerequisitesM4.1.L1-L5 (policy table, roles, scope)
Time9 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.

Terminal window
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

Terminal window
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, the
hook 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

Terminal window
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:

Terminal window
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.

Terminal window
LAYER ROLE ROLE TYPE
──────────────────────────────────────────────────────────
Frontend gate UX, performance Not security
Gateway check Authorization decision Security boundary
Backend service Applies the user's scope Data filtering

If 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:

Terminal window
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:

Terminal window
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:

Terminal window
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 visible

Three 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:

Terminal window
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.



Module quiz

Complete M4.1 → 10-question module quiz.


Start with the bill.

Foundations takes about five hours. The first lesson is nine minutes.

Free curriculum. No paywall. 290 lessons across 7 courses, plus an optional 24-lesson beginner course. Browse every course without an account; a free account opens the lessons, saves your progress and opens the exams for three publicly verifiable credentials. Read it on the train, take the exam on a Saturday, list the credential on your résumé Monday.

5h median time to finish Foundations
free account, every course, no paywall
3 certifications, each with a public verifier
Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console·