Files
signoz/frontend/docs/authz-guide.md
Ashwin Bhatkal 8ee9f97f28
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
feat(dashboards): gate the dashboards list on fine-grained authz (#12449)
#### Description

Applies the authz list-page pattern to the dashboards list and gates the
row actions per dashboard.

- **The table is blocked** without `list`, via an inline callout. **New
dashboard stays enabled** if the caller holds `create` — it is
independent of `list`.
- **Everything that only feeds the table goes with it.** The search box
and the filter chips edit a query that can only be run through the list
request, so they are disabled rather than hidden, and each carries the
standard denial on hover. Two tooltip zones cover them — the search
field, and the chips with Clear — instead of one tooltip per control.
- **The views rail is one guarded block**, not a column of dead
controls. Every view leads to a table the caller cannot see, and the
saved views behind it need the same grant, so the rail carries a single
callout. That also drops an empty state that was asserting something it
could not know ("No saved views yet" when the request was never made).
- **Row actions run their own checks** through `AuthZButton`, so the
menu opens immediately. View, Open in New Tab and Copy Link need no
permission and no longer wait behind the gated rows; all nine rows now
render through the same component rather than three of them being plain
buttons.
- **Delete gates on `delete` alone** (guide rule 3), so it survives
without `read`. **Rows are never gated** (rule 4): the list is
collection-scoped and deliberately returns rows the caller cannot
`read`, so every row stays clickable and the denial is explained on
arrival.
- **Per-row checks fire lazily**, when a menu opens rather than for all
20 rows, and the menu itself renders lazily — it previously evaluated
its whole tree and instantiated two mutations per row on every render.
- **Viewers can manage saved views again.** Save, rename and delete were
behind `edit_dashboard`; the backend only requires `list`, and the views
request now follows the same grant.
- **The row menu's lock gates on the dashboard's `source`**, not on who
created it — an integration dashboard can never be locked, and
everything else is decided by `update`.
- **Route gating**: the three dashboard routes bypass the legacy role
check, so an FGA user with no managed role isn't bounced to
`/un-authorized`.
- **Guide updated to match.** The list-page rule previously said to
leave the search, filters and views interactive. It now says to disable
what only shapes a blocked request, and to gate a region once rather
than repeat a tooltip per control.

#### Screenshots
<img width="1920" height="992" alt="Screenshot 2026-09-10 at 11 13
20 PM"
src="https://github.com/user-attachments/assets/b590ddcd-4b7b-4e82-8a51-ef5ffae68623"
/>

<img width="1571" height="275" alt="Screenshot 2026-09-10 at 11 14
09 PM"
src="https://github.com/user-attachments/assets/f0a6f32c-ff1d-4a9c-81ae-8e3ae4832ecf"
/>

<img width="570" height="363" alt="Screenshot 2026-09-10 at 11 22 32 PM"
src="https://github.com/user-attachments/assets/2f42e918-ae06-499e-8700-5deba46b987a"
/>
<img width="547" height="320" alt="Screenshot 2026-09-10 at 11 16 41 PM"
src="https://github.com/user-attachments/assets/b5e287b1-9178-4165-866f-e914348d6e39"
/>
<img width="518" height="345" alt="Screenshot 2026-09-10 at 11 17 00 PM"
src="https://github.com/user-attachments/assets/ac22cd2f-2aa1-499f-b3cf-4f9014bf6ea3"
/>
<img width="498" height="313" alt="Screenshot 2026-09-10 at 11 18 45 PM"
src="https://github.com/user-attachments/assets/72c252f5-5616-4a44-b645-3074a6d9fc12"
/>
<img width="561" height="311" alt="Screenshot 2026-09-10 at 11 18 52 PM"
src="https://github.com/user-attachments/assets/923f5c48-b272-4278-9951-8a9122b2732c"
/>
<img width="588" height="322" alt="Screenshot 2026-09-10 at 11 16 31 PM"
src="https://github.com/user-attachments/assets/5061c115-57b4-4fdd-aa9d-9f43cd539be6"
/>

 

#### Additional Information

- `DASHBOARD` and `DASHBOARD_PANEL_EDITOR` are registered for authz
here, though their pages ship in #12438. Harmless in this order — until
this PR merges those routes keep their legacy role checks, which is the
fail-safe direction.
- Two callouts show when `list` is denied, one on the rail and one where
the table would be. They are separate blocks with separate reasons to
exist, but it is more denial than the guide's "state it once".
- A 403 from the list request still renders as "Invalid query", because
`ErrorState` treats every 4xx as a client error. Pre-existing and not
authz-specific, so left alone.
- Saved views are org-shared with no ownership rule, so anyone holding
`list` can delete anyone's view. Unchanged here — flagging it as a
product question.
- Requires #12438. Rebased onto current `main`; shares
`utils/permission/index.ts` and `AppRoutes/__tests__/Private.test.tsx`
with #12448, which merges without conflict.
2026-09-11 03:54:05 +00:00

7.5 KiB

AuthZ Guide

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.

Prerequisites

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).

Core rules

These hold for every page. The per-pattern sections below only add to them.

  1. A page is always reachable, regardless of permission. The intended action must be known before permission can be checked, so the route renders first and individual pieces are gated.
  2. update requires both read and update. A user who can write but cannot read the current value must not get an edit affordance.
  3. delete is independent of read. Delete controls stay visible for a user who holds delete but not read.
  4. Never gate per row. If a user can list, render every row. Check read only when the row is opened (drawer or detail route).
  5. Gate the narrowest thing that works, a button over a section, a section over a page.
    • Gate a page on read alone. It is the only verb the page's own request needs. update and delete gate individual controls, so waiting for them holds up the whole page for nothing — pass them as preloadChecks and they resolve in the same request, leaving the controls to read from cache.
  6. A resource may be gated while a sub-resource is not. A user without read on Service Accounts can still hold create on API Keys, so blocking the outer container would hide work they are allowed to do.
  7. Verbs not covered here (attach, detach, assignee) behave like delete: gate the control that triggers them, not the surrounding content.

Page patterns

List page

Visual rules for structuring a list page

Without list, but with any of read / create / update, the table and everything that only feeds it are blocked:

  • Title, description, search, filters and action buttons stay visible. Nothing is hidden for lack of permission.
  • Disable what only shapes the blocked request — the search box, the filter chips, a Clear button — and give it the same denial. It edits a query that has nowhere to run, so leaving it live invites the user to compose a filter and watch nothing happen.
  • Gate a region as one section when several of its controls are dead. A saved views rail is a block with a single callout, not a column of identical tooltips; a row of filters is one tooltip zone, not one per control.
  • The create button stays enabled if the user holds create. It is independent of list.

Edit page

Visual rules for structuring an edit page

Without read:

  • Block the content with withAuthZContent, not the whole route.
  • Keep delete visible (rule 3).
  • Keep update blocked (rule 2).

Drawer

Visual rules for structuring a drawer

Without read:

  • Block the drawer body, not the drawer itself.
  • Prefer routing that carries the resource ID in the URL, so delete stays reachable without read.
  • Keep update blocked (rule 2).
  • Watch for sub-resources the user may still be allowed to act on (rule 6).

Create

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

Delete

Gate the control with AuthZButton, or AuthZTooltip for a non-button trigger such as an icon or menu item.

Quick filters

Visual rules for structuring a page with quick filters

What "blocked" means

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.

Denial copy comes from the components. Never write a custom message for a permission check. disabledTooltip is for a block that is not a permission — a lock, an immutable resource, a mount that is deliberately read-only. It takes precedence over the checks, which are then skipped, so set it only when that block is the real obstacle: a missing permission must still surface its own wording.

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.

Migration checklist

Use the existing components. Create a new one only if none fit.

  • Can I apply a withAuthZ* variant directly, or must I extract the content into a component first?
    • If extraction is needed, declare the new content component in the same file to keep the diff small, then move it to its own file in a follow-up commit or PR.
  • Is the layout structured to respect the core rules?
    • If not, raise it in the frontend Slack channel before reshaping the page.
  • Am I gating a shared component rather than a page or section?
    • If so, it must stay functional with no permission. Example: the Query Builder works without field suggestions when the user cannot read them.

Testing

Devtool

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.

Unit tests

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.