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. 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: an HTTP route.
- Action (verb): a dot-namespaced string declared by the route policy table.
- Decision: granted iff any of the user’s roles has a grant that matches the route’s required verb.
Sessions capture the role list at login time. Verbs are computed per request from session.roles → rbac.roles → grants. Hot-reloading role definitions takes effect on the next route check; hot-reloading group mappings or local user roles requires the user to re-login (since sessions hold their original role list).
Verb vocabulary
Source: apps/bff/src/rbac/verbs.ts. Twenty-eight verbs 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. |
traces:read |
Traces tab on any layer, trace detail page. |
logs:read |
Logs tab on any layer, log detail page. |
topology:read |
Topology tab, topology widgets on overviews. |
profile:read |
Profiling tab (results read-only). |
Operate — dashboards, rules, diagnostics
| Verb | Gates |
|---|---|
overview:read / overview:write |
Overview templates admin page (/admin/overview-templates): list / edit. |
dashboard:read / dashboard:write |
Layer dashboard templates admin page: list / edit. |
alarm-setup:read / alarm-setup:write |
Alarm Setup page: list / edit. |
alarm-rule:read / alarm-rule:write |
Alarm Rule catalog: list / edit. |
setup:read / setup:write |
Service / instance / endpoint setup pages. |
rule:read |
DSL Management — list rules. |
rule:write |
DSL Management — content edits (non-structural). |
rule:write:structural |
DSL Management — add / remove rules, change rule kind. |
rule:delete |
DSL Management — delete a rule. |
rule:debug |
DSL Management — debug a rule against sample input. |
live-debug:read / live-debug:write |
Live Debugger — observe / start sessions. |
profile:enable |
Create a profiling task on a layer. |
Platform monitoring
| Verb | Gates |
|---|---|
cluster:read |
Cluster Status page (/operate/cluster). |
inspect:read |
Inspect page (/admin/inspect). |
Admin surface
| Verb | Gates |
|---|---|
user:read |
Users admin page (/admin/users). |
user:write |
Reserved (no current write endpoint). |
role:read |
Roles & Permissions admin page (/admin/roles). |
role:write |
Reserved. |
auth:read |
Auth Status admin page (/admin/auth-status) + LDAP probe. |
auditRead |
Reserved (audit log not yet exposed via API). |
Special
| Verb | Meaning |
|---|---|
admin |
Synonym for *. Matches anything. |
* |
Wildcard. Matches anything. |
Grant matching
A user’s grant string is matched against a required verb using these rules (verbs.ts):
| 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, rule:debug. |
*: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. Deliberately limited — does not include *:read so a viewer cannot peek at rule definitions, live-debug sessions, setup screens, or platform internals.
metrics:read, alarms:read, traces:read, logs:read, topology:read, profile:read
maintainer
Viewer + platform monitoring.
viewer baseline + cluster:read, inspect:read
operator
Configures observability. Inherits maintainer’s reads + write access to dashboards, alarms, rules, live-debug, profiling.
maintainer baseline +
overview:read, overview:write,
setup:read, setup:write,
dashboard:read, dashboard:write,
alarm-setup:read, alarm-setup:write,
alarm-rule:read, alarm-rule:write,
rule:read, rule:write, rule:write:structural, rule:delete, rule:debug,
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 BFF returns a landingRoute from rbac.landingByRole. The UI router uses it as the post-login destination unless ?redirect= overrides (set when the user was bounced to login from a protected route — 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
Source: apps/bff/src/rbac/route-policy.ts, apps/bff/src/rbac/policy.ts, apps/bff/src/user/middleware.ts.
The BFF builds a route → required-verb table at startup. Every Fastify route is gated by:
requireAuth()— looks up the session cookie, returns 401 on missing / expired.checkVerb(verb)— looks up the session’s effective verbs, returns 403 on mismatch.
Routes without an explicit policy entry default to 'auth' (session required, no specific verb) with a warning at startup. This is fail-safe: a forgotten route does not become accidentally public.
Policy values
| Policy | Meaning |
|---|---|
'public' |
No auth required. Login, logout, health-check endpoints. |
'auth' |
Session required; no verb check. Identity-only routes. |
'<verb>' |
Session required + verb check. Most application routes. |
Example policy entries
'POST /api/auth/login': 'public',
'POST /api/auth/logout': 'public',
'GET /api/auth/me': 'auth',
'GET /api/oap/info': 'auth',
'POST /api/layer/:key/dashboard': 'metrics:read',
'GET /api/rule': 'rule:read',
'POST /api/rule/addOrUpdate': 'rule:write',
'POST /api/admin/auth-status/probe': 'auth:read',
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, verb role:read) renders a read-only board of roles × verbs with check marks. 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.
Common patterns
Read-only role for a new team
roles:
on-call:
- metrics:read
- alarms:read
- traces:read
- logs:read
- topology: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-tuning role
roles:
alarm-tuner:
- metrics:read, alarms:read, topology:read, traces:read, logs:read
- alarm-setup:read, alarm-setup:write
- alarm-rule:read, alarm-rule:write
- rule:read, rule:debug
Can view operational data and edit alarm rules but cannot touch DSL rule structure or live-debug.