RBAC: Roles & Verbs

Horizon enforces access at the BFF on every HTTP request. The UI hides controls based on the verbs the session reports, but the enforcement is server-side — a forged UI cannot escalate. The UI also gates whole pages by verb: navigating to a restricted page you lack the verb for (by URL or a stray link) redirects you home, so a viewer can’t land on a maintainer page even if its data comes from a shared endpoint. This page is the full reference for the verb vocabulary, the four built-in roles, and how grants are matched against requests.

Model

  • Subject: an authenticated session (username + roles).
  • Object: a protected request.
  • Action (verb): a dot-namespaced string each protected request requires.
  • Decision: granted if any of the user’s roles holds a grant that matches the required verb.

Sessions capture the role list at login time, and the verbs they grant are resolved from the current rbac.roles definitions on each request. Hot-reloading role definitions takes effect immediately; hot-reloading group mappings or local user roles requires the user to re-login (since sessions hold their original role list).

Verb vocabulary

Known verbs are grouped into areas:

Data reads (the public catalog)

Verb Gates
metrics:read Layer dashboards, overview widgets that fetch MQE values.
alarms:read Alarms page, alarm widgets on overviews.
events:read Events popout on a service banner: that service’s lifecycle events.
traces:read Traces tab on any layer, trace detail page.
logs:read Logs tab on any layer, log detail page.
browser-errors:read Browser Logs tab (BROWSER layer): list JS error logs, list source maps, resolve a stack.
inspect:read The read-only inspect tools: Metrics Inspect (/operate/inspect), Trace Inspect (/operate/trace-inspect), Log Inspect (/operate/log-inspect).
topology:read Topology tab, topology widgets on overviews.
profile:read Profiling tab (results read-only) and the continuous-profiling policy list.
overview:read Public overview dashboards. The template-administration pages read their sync status through it too, so a role built to edit only layer dashboards needs it alongside dashboard:read.
infra-3d:read 3D Infrastructure Map — the map’s config + live traffic metrics.
ai:read AI assistant: send a chat message. Grants no data access by itself — each of the assistant’s data tools re-checks its own read verb, so the assistant never reads more than the session could.

Operate — dashboards, rules, diagnostics

Verb Gates
overview:write Overview templates (/admin/overview-templates), the 3D-map config (/admin/3d-map), the Translations editor (/admin/translations), and publishing edits from the Alert page (/admin/alert-page-setup) and Global defaults (/admin/global-defaults) pages.
dashboard:read / dashboard:write Layer dashboard templates admin page: list / edit.
alarm-setup:read Shows the Alert page setup entry (/admin/alert-page-setup). Publishing an edit there requires overview:write, like every other template write.
alarm-rule:read Alarm Rule catalog: list (read-only — alarm-rule edits go through the OAP alarm-rule YAML, not this page).
alarm-rule:write Reserved — OAP’s alarm-rule catalog is read-only, so there is no write for Horizon to gate.
setup:read Shows the Global defaults entry (/admin/global-defaults) — default theme and time defaults. Publishing an edit there requires overview:write, like every other template write.
rule:read DSL Management — list rules, read a rule body, and download a runtime-rule dump.
rule:write DSL Management — save a rule whose change is not structural, and inactivate a rule.
rule:write:structural DSL Management — save an edit that moves a metric’s storage identity (scope, downsampling, single ↔ labeled ↔ histogram), force a re-apply to recover a degraded rule, and revert a rule to its bundled version.
rule:delete DSL Management — delete a rule.
live-debug:read / live-debug:write Live Debugger — watch captures (the page, the active-session list, per-node status, capture history) / start and stop them. These two are the whole of the Live Debugger’s access control: live-debug:read is enough on its own to watch, live-debug:write is enough on its own to start and stop, and no rule:* verb takes part.
source-map:write Browser Logs — upload / remove source maps (held in BFF memory).
profile:enable Create a profiling task on a layer, and arm a continuous-profiling policy.

Platform monitoring

Verb Gates
cluster:read Cluster Status page (/operate/cluster).
ttl:read Data Retention page (/operate/ttl).
config:read OAP Configuration page (/operate/config).

Admin surface

Verb Gates
user:read Users admin page (/admin/users) — the list is read-only; local users are defined in horizon.yaml.
user:write Reserved — the user list has no write.
role:read Shows the Roles & Permissions entry and page (/admin/roles). The board is drawn from the same status read as the Auth Status page, so grant auth:read alongside it or the page opens and reports a load failure.
role:write Reserved — role definitions are edited in horizon.yaml, not from the UI.
auth:read Auth Status admin page (/admin/auth-status) + LDAP probe. Also the data behind the Roles & Permissions board.
audit:read Reserved — the audit trail is a file for you to ship to an SIEM; Horizon does not serve it.

Special

Verb Meaning
admin Synonym for *. Matches anything. Never required by a request — it is only ever a grant.
* Wildcard. Matches anything.

Reserved verbs

Four verbs — alarm-rule:write, user:write, role:write, audit:read — are part of the vocabulary but nothing checks them. Granting one opens nothing and closes nothing. They keep their names so a horizon.yaml that already lists one still validates, and so the name stays stable if a capability is ever bound to it.

No built-in role names a reserved verb — admin’s * matches them like everything else, which still does nothing — and the Roles & Permissions page marks each one on screen rather than presenting it as a capability. If a custom role of yours grants one, you can drop it: it is doing nothing today, and leaving it in means the grant takes effect silently on the day something enforces it.

Grant matching

A user’s grant string is matched against a required verb using these rules:

Grant pattern Matches
* or admin Any verb.
area:verb (exact) The exact required verb (case-sensitive).
area:* Any verb in that area, including sub-actions: rule:* matches rule:read, rule:write, rule:write:structural, rule:delete.
*:read The read action in any area: matches metrics:read, alarms:read, cluster:read, etc. Does not match rule:write:structural (the action is not read).

Effective verbs for a session are the union of all grants from all roles.

Built-in roles

Default definitions (used when rbac.roles is not overridden):

viewer

Read-only data catalog, the read-only inspect tools, and the AI assistant. Deliberately limited — does not include *:read so a viewer cannot peek at rule definitions, live-debug sessions, setup screens, or cluster / TTL / config internals.

metrics:read, alarms:read, events:read, traces:read, logs:read, browser-errors:read, inspect:read, topology:read, profile:read, overview:read, infra-3d:read, ai:read

maintainer

Viewer + platform monitoring.

viewer baseline + cluster:read, ttl:read, config:read

operator

Configures observability. Inherits maintainer’s reads + write access to dashboards, rules, live-debug, profiling and source maps. Alarm rules stay read-only for every role — see alarm-rule:write.

maintainer baseline +
overview:write,
source-map:write,
setup:read,
dashboard:read, dashboard:write,
alarm-setup:read,
alarm-rule:read,
rule:read, rule:write, rule:write:structural, rule:delete,
live-debug:read, live-debug:write,
profile:enable

admin

Unrestricted. "*".

Role assignment

Backend Assignment
Local auth.local.users[].roles: [role1, role2, ...] in horizon.yaml.
LDAP auth.ldap.groupMappings: each group DN → one role. A user matching multiple groups gets the union of all matching roles.

A user with no role gets no verbs. The session is created (login succeeds) but everything is denied. The login response carries an empty verb list; the UI shows “no access” for every protected feature.

Landing route per role

After login, the user lands on the route configured for their role in rbac.landingByRole — unless they were bounced to login from a protected route, in which case they return to where they came from.

Default mapping:

landingByRole:
  viewer:     /
  maintainer: /operate/cluster
  operator:   /
  admin:      /operate/cluster

When a user has multiple roles, the first role on the user wins. Order matters in auth.local.users[].roles and in LDAP group-mapping resolution.

Enforcement

Access is enforced server-side, not in the browser. Every protected request is checked for a valid session (an unauthenticated request is rejected with 401) and then for the verb that request requires (a session lacking the verb is rejected with 403). The UI hides controls a session cannot use, but a forged UI cannot bypass these checks.

Enforcement is fail-safe: a request with no explicit verb still requires a valid session, so a misconfiguration cannot accidentally expose a protected endpoint to anonymous callers.

Disabling RBAC for dev

rbac:
  enabled: false

Every authenticated session is granted *. Useful for local development. Never set false in production. When disabled, the Admin → Roles page shows a red banner.

Visualizing the policy

The Admin → Roles page (/admin/roles, verbs role:read + auth:read) renders a read-only board of roles × verbs with check marks, grouped by feature area and preceded by a menu-visibility matrix showing which navigation entries each role sees. It pulls live data — what you see is exactly what the BFF will use to evaluate the next request. Use it to verify role changes after editing horizon.yaml.

Rows for reserved verbs are marked as such. A check mark on a reserved row still reflects the grant a role holds — it just tells you the grant buys nothing.

Common patterns

Read-only role for a new team

roles:
  on-call:
    - metrics:read
    - alarms:read
    - traces:read
    - logs:read
    - topology:read
    - overview:read
    - inspect:read       # so they can browse the catalog
landingByRole:
  on-call: /alarms       # land on the alarm board

Lockdown for an external auditor

roles:
  auditor:
    - "*:read"           # all reads only
landingByRole:
  auditor: /operate/cluster

*:read grants every read — useful for audit access without write capability.

Separate alarm-triage role

roles:
  alarm-triage:
    - metrics:read
    - alarms:read
    - topology:read
    - traces:read
    - logs:read
    - alarm-rule:read      # the rule behind a firing alarm
    - alarm-setup:read     # which layers the alarm overview covers
landingByRole:
  alarm-triage: /alarms

Reads operational data plus the alarm rule behind each firing alarm, and can open the Alert page setup to see how the alarm overview is composed. It cannot change any of it: publishing an Alert page edit needs overview:write, and alarm rules are read-only for every role.