RBAC vocabulary - roles, groups and terms
One vocabulary, used verbatim by every moving part: dfe-engine, dfe-ui, dfe-infra, dfe-deploy, dfe-schemas, and the test environments. If a role or group name appears in two repos it means the same thing in both, or it is a bug.
dfe-engine owns this vocabulary. Roles are defined in
src/dfe_engine/auth/resources/roles.yamland default groups insrc/dfe_engine/auth/bootstrap.py. This document is the human-readable face of those two files. Adding or renaming anything starts here and in them, never in a consumer.
Related: oidc-rbac-architecture.md for how external identities reach these roles, and rbac.md for enforcement and the ClickHouse tenant model.
The two planes
Roles split across two orthogonal planes, and conflating them is the most common modelling mistake:
- Control plane - what you can DO. Static roles granting API actions: configure sources, run hunts, deploy services, manage groups.
- Data plane - what you can SEE. Dynamic, driven by
org_ids, and restricted to data browsing (Observe, Kibana-style). A user can be a powerful analyst on the control plane and see exactly one org's rows.
A role is not "more senior" than another across planes. org_viewer is not
a junior admin. It is a different axis.
Canonical roles
Defined in roles.yaml. scoped: true means the role never reads across orgs: its holders are held to their own orgs wherever it is bound, system-wide included.
| Role | Plane | For |
|---|---|---|
admin | control | Full access to all resources and all orgs |
infra_admin | control | Service configs, deployments, Helm, ArgoCD, orgs, groups, Governed Ops |
infra_viewer | control | Infrastructure read-only |
data_analyst | control | Hunt, query, source, fieldmap, alert, rule, transform CRUD |
data_analyst_viewer | control | Analyst scope, read-only |
data_viewer | data | Search dashboards and query execution |
dfe_operator | control | Curated operational dials - invoke shipped actions, read governed state |
org_viewer | data | Org-restricted data viewer (scoped: true) |
Canonical default groups
The engine SEEDS the first four on bootstrap when the groups directory is empty. They are not suggestions - a fresh install has them, so every consumer can rely on them existing.
| Group | Roles | Seeded | For |
|---|---|---|---|
dfe-admins | admin | yes | Full access |
dfe-analysts | data_analyst | yes | Analyst control plane |
dfe-viewers | data_viewer | yes | Dashboards and query |
dfe-infra | infra_admin | yes | Service and deployment management |
dfe-infra-viewers | infra_viewer | no | Infrastructure read-only |
dfe-analyst-viewers | data_analyst_viewer | no | Analyst read-only |
dfe-operators | dfe_operator | no | Curated ops dials |
dfe-<org>-viewers | org_viewer | no | Per-org data plane, scope: org:<org> |
Note dfe-infra, not dfe-infra-admins. The engine seeded that name first and
it stands, because a parallel name would mean two groups granting one role.
org_viewer -- the org-tied exception role
org_viewer (renamed from customer_viewer) is the one role hard-tied to the org_id CRUD: a user or group carrying it is associated with one or more org_ids, and that association IS the grant. When such a user browses data through the Observe iframe inside dfe-ui, dfe-hyperdx connects as that org's pinned ClickHouse user, whose READONLY tenant setting and the shared RESTRICTIVE row policies filter every query to the org's _org_id values, always. There is no unfiltered path: the scoping lives in the ClickHouse user, not in anything the UI or the user sends. See rbac.md sections 5-6 for the mechanism.
Naming conventions
- Roles:
snake_case, singular, describing the person -data_analyst, notDataAnalystRoleorANALYST_USER. - Groups:
kebab-case, plural,dfe-prefixed -dfe-analysts. The prefix matters because these names land in customers' own IdPs alongside their groups. - Per-org groups:
dfe-<org>-viewers, paired withscope: org:<org>. - Permissions:
resource:action, colon-separated,*wildcards -hunt:read,service:*:config:*. - Orgs / tenants: the term is org everywhere in the product surface (
org_id,org_ids,scope: org:<name>). "Tenant" appears only inside the ClickHouse isolation model (the tenant role, the tenant setting and its row policies). One concept, and the boundary between the two words is exactly that model.
Who consumes what
| Repo | Consumes | How |
|---|---|---|
| dfe-engine | roles + groups | Defines them, enforces via _resolve_group_grants and RoleConfig |
| dfe-ui | roles | Shows/hides UI by resolved role from GET /auth/me, and must not hardcode its own names |
| dfe-infra | groups | ArgoCD RBAC group mappings, ESO secret wiring for providers |
| dfe-deploy | groups | Deployment-time group seeding |
| dfe-schemas | orgs | org_id column semantics for tenant row policies |
| Test environments | both | The OIDC test fixture renders these names into dex, Entra and Okta |
Rules
- Add roles here first. A role that exists in a consumer but not in
roles.yamlgrants nothing - unknown groups resolve to zero roles, which presents as a broken IdP rather than a config error. - Never rename in a consumer. Renaming propagates from this document and the two engine files outward, in one change.
- Group names are external contract. They are created in customers' Entra, Okta and Workspace directories. Renaming one is a customer-facing migration, not a refactor.
- Map roles on stable ids where the provider has them, and display names. Entra emits group GUIDs, not names. See the OIDC architecture doc.