Access Control Configuration
Role-Based Access Control. Defines the role → verb grants and the post-login landing route per role. Full behavior reference (verb vocabulary, grant matching, where each verb gates) is in Access Control → RBAC: Roles & Verbs; this page is the horizon.yaml shape.
Shape
rbac:
enabled: true
builtinRoles: replace # or `keep` to merge onto the built-ins
roles:
viewer: [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, mcp:read]
maintainer: [metrics:read, alarms:read, events:read, traces:read, logs:read, browser-errors:read, topology:read, profile:read, overview:read, cluster:read, inspect:read, ttl:read, config:read, infra-3d:read, ai:read, mcp:read]
operator: [metrics:read, ..., rule:write:structural, live-debug:write, profile:enable, ai:read, mcp:read]
admin: ["*"]
landingByRole:
viewer: /
maintainer: /operate/cluster
operator: /
admin: /operate/cluster
Fields
| Field | Type | Default | Required | Notes |
|---|---|---|---|---|
enabled |
boolean | true |
no | When false, every authenticated session is granted * (full access). Useful for dev. Set true in production. |
builtinRoles |
replace | keep |
replace |
no | What a configured roles block does to the built-ins. replace: the block is the whole role set. keep: the built-ins are the base — a listed name overrides that one role, a new name is appended, the rest stay. Also merges landingByRole. |
roles |
object | the four built-ins | no | Custom role definitions. Keys are role names; values are arrays of permission strings. Omitting this block uses the four built-ins (viewer, maintainer, operator, admin) — the full grants are in the table below. Under the default builtinRoles: replace, defining the block overrides them entirely. |
landingByRole |
object | see below | no | Post-login redirect route per role. First role on the user wins. |
Built-in roles (used when roles is not set)
| Role | Purpose | Grants |
|---|---|---|
viewer |
Read-only data catalog, inspect tools, public overviews, the AI assistant, and MCP. | 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, mcp:read. Deliberately not *:read so the viewer cannot see rule definitions, live-debug sessions, setup screens, or cluster / TTL / config internals. |
maintainer |
Viewer + platform monitoring. | viewer baseline + cluster:read, ttl:read, config:read. |
operator |
Configures observability. | maintainer baseline + source-map:write, the six Dashboard-setup read/write pairs (overview-template, layer-template, translation, alarm-setup, infra-3d-setup, setup), alarm-rule:read, rule:read, rule:write, rule:write:structural, rule:delete, live-debug:read/write, profile:enable. Alarm rules are read-only for every role. |
admin |
Unrestricted. | *. |
Adding your own role
rbac.roles is an open map — the four built-ins have no special status, and you can define as many roles as you like beside them.
By default, naming roles replaces the block entirely: the roles that exist are exactly the ones you listed, and a user whose every role has disappeared holds no verbs and is refused everywhere. Set builtinRoles: keep to treat the built-ins as a base instead — then a name you list overrides that one role wholesale, a new name is appended, and everything you did not mention stays as it shipped.
A common addition is a read-only template viewer — someone who inspects how the dashboards are configured, and reviews translations, without being able to publish anything. Grant the read half of each Dashboard-setup pair and none of the write half:
rbac:
enabled: true
builtinRoles: keep # viewer / maintainer / operator / admin stay as they are
roles:
# The six Dashboard-setup pages, read-only. No `:write` anywhere.
# Nothing else belongs here — grant it beside `viewer`, whose data reads
# (metrics, alarms, topology, overview) render the dashboards being
# reviewed. A user's verbs are the union of their roles'.
template-viewer:
- overview-template:read
- layer-template:read
- translation:read
- alarm-setup:read
- infra-3d-setup:read
- setup:read
landingByRole:
template-viewer: /
Assign it alongside the role the person already holds — roles: [viewer, template-viewer] — rather than as their only role.
What the pair gets that user: all six rows under Dashboard setup in the sidebar, each page opening with the whole stored configuration visible and every editing control disabled, and a banner naming the permission publishing would need. Browsing still works — switching template, locale or component, refreshing from OAP, exporting, and opening a diff to compare the shipped default against what is live. Nothing it does can change OAP: Horizon refuses every publish server-side, not merely in the interface.
Grant only the pairs you mean. translation:read on its own is enough for a translator: it opens the Translations page and reads the source templates being translated, and nothing else — the Layer dashboards and Overview templates editors stay closed to it.
Under keep, a built-in cannot be removed by leaving it out — leaving it out is what keeps it. Grant it nothing instead, which leaves the role in place conferring no verb:
rbac:
builtinRoles: keep
roles:
admin: []
builtinRoles defaults to replace, and deliberately: a deployment that trimmed its roles block in order to remove a role would otherwise get it back on upgrade, and one of the roles it would get back is admin: ["*"]. Opt in when you want merging; never rely on it appearing by itself.
Assigning the role. Roles come from whichever sign-in backend is configured — a local user’s roles list, an LDAP group mapping, or the SSO role tables. See Authentication. A user carries the union of every verb their roles grant, which is why a role like the one above only has to name what it adds: granting it beside viewer widens that user by exactly the six setup reads and nothing else. The effective role names are logged at startup, so you can confirm what a merge produced.
Verb grammar
Grants are dot-namespaced strings. Four matching modes:
| Pattern | Meaning |
|---|---|
* or admin |
Matches any verb (admin). |
area:verb |
Exact match: e.g., rule:write grants exactly rule:write. |
area:* |
Matches any verb in that area: rule:* grants rule:read, rule:write, rule:write:structural, rule:delete. |
*:read |
Matches the read action across any area. |
Nothing else is a grant. A string outside these forms — a typo like rule:*:typo, or a fourth segment — matches no verb and confers nothing; Horizon names it in a startup warning so it does not sit in your config looking effective.
A user’s effective verbs are the union of all grants from all their roles.
landingByRole
Default:
landingByRole:
viewer: /
maintainer: /operate/cluster
operator: /
admin: /operate/cluster
The login flow returns this route as landingRoute in the login response. The UI router uses it as the post-login destination unless a ?redirect= query param overrides (e.g., the user was bounced to login from a protected route — they return there after auth).
Common shapes
Loosen for dev
rbac:
enabled: false # all authenticated users get full access
Add a custom role
rbac:
enabled: true
roles:
viewer: [...]
maintainer: [...]
operator: [...]
admin: ["*"]
on-call: # custom
- metrics:read
- alarms:read
- traces:read
- logs:read
- topology:read
- overview:read
- inspect:read # so they can poke at the catalog
- live-debug:read # but not write
landingByRole:
on-call: /alarms # land directly on the alarm board
Custom roles are usable from both backends: assign to local users via auth.local.users[].roles, or map LDAP groups via auth.ldap.groupMappings.
Hot reload behavior
Changes to rbac.roles and rbac.landingByRole apply on the next route evaluation — existing sessions pick up new grants without re-login. The session’s role list is the authoritative source; the verb set is computed per request from roles → rbac.roles at policy check time.
rbac.enabled: false → true and vice versa also applies on next request, with no session invalidation.
Enforcement
Verb checks happen on the BFF, not the UI. The UI hides controls based on the verbs the session reports, but a malicious client cannot escalate by calling the API directly — the BFF re-checks every request. See Access Control → Admin Pages for the Roles & Permissions read-only board that visualizes the live policy.