Skip to main content

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.yaml and default groups in src/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.

RolePlaneFor
admincontrolFull access to all resources and all orgs
infra_admincontrolService configs, deployments, Helm, ArgoCD, orgs, groups, Governed Ops
infra_viewercontrolInfrastructure read-only
data_analystcontrolHunt, query, source, fieldmap, alert, rule, transform CRUD
data_analyst_viewercontrolAnalyst scope, read-only
data_viewerdataSearch dashboards and query execution
dfe_operatorcontrolCurated operational dials - invoke shipped actions, read governed state
org_viewerdataOrg-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.

GroupRolesSeededFor
dfe-adminsadminyesFull access
dfe-analystsdata_analystyesAnalyst control plane
dfe-viewersdata_vieweryesDashboards and query
dfe-infrainfra_adminyesService and deployment management
dfe-infra-viewersinfra_viewernoInfrastructure read-only
dfe-analyst-viewersdata_analyst_viewernoAnalyst read-only
dfe-operatorsdfe_operatornoCurated ops dials
dfe-<org>-viewersorg_viewernoPer-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, not DataAnalystRole or ANALYST_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 with scope: 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​

RepoConsumesHow
dfe-engineroles + groupsDefines them, enforces via _resolve_group_grants and RoleConfig
dfe-uirolesShows/hides UI by resolved role from GET /auth/me, and must not hardcode its own names
dfe-infragroupsArgoCD RBAC group mappings, ESO secret wiring for providers
dfe-deploygroupsDeployment-time group seeding
dfe-schemasorgsorg_id column semantics for tenant row policies
Test environmentsbothThe OIDC test fixture renders these names into dex, Entra and Okta

Rules​

  1. Add roles here first. A role that exists in a consumer but not in roles.yaml grants nothing - unknown groups resolve to zero roles, which presents as a broken IdP rather than a config error.
  2. Never rename in a consumer. Renaming propagates from this document and the two engine files outward, in one change.
  3. 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.
  4. Map roles on stable ids where the provider has them, and display names. Entra emits group GUIDs, not names. See the OIDC architecture doc.