ADR 0005: Role definitions are deployment configuration; the permission vocabulary is the product contract¶
Status: Accepted Date: 2026-06-12 Issue: #560
Context¶
The identity architecture is deliberately external-IdP-only: no built-in
identity provider, no internal store of human users, every interactive
login via the customer's OIDC provider or LDAP (pkg/auth/). That
posture is reaffirmed here — it is correct and does not change.
Authorization, however, is compiled in. The five human role groups
(dcs-admin, dcs-supervisor, dcs-engineer, dcs-lead-operator,
dcs-operator) are constants in pkg/auth/auth.go, and the
role→permission mapping is a static RolePermissions table in
pkg/auth/authz.go over a six-permission vocabulary (read, operate,
operate-lead, engineer, supervise, admin). The only configurable
layer is external-group→role-name mapping (--ldap-group-mappings).
Every customer staffs differently; today their only lever is which IdP
groups map onto our five fixed names — they cannot define a role or
tune what a role may do.
Two implementation details make the hardcoding worse than the table alone suggests:
- About twenty gateway call sites bypass the permission table and
compare role names directly (
auth.RequireAnyGroup(auth.AdminGroup, auth.SupervisorGroup)for recipe approval and batch-record review,HasGroup(auth.AdminGroup)for the cross-site bypass ininternal/gateway/tenant.go, ad-hoc group lists ininternal/gateway/mes.go, …). Even if the table became configurable, those paths would still hardwire the shipped names. - The frontend duplicates the role names and its own access rules
(
ENDPOINT_ACCESSininternal/gateway/static/js/auth.js); the session payload carries raw groups, not resolved permissions.
The product-vs-deployment split principle (ADR 0004 applied it to node
join) says the product ships interfaces and recommended defaults while
the deployment instance owns policy. Who may approve a recipe is
plainly policy. docs/compliance/iec62443.md SR 1.3 (Account
management) is already marked Partial; under the external-IdP posture,
account lifecycle is the IdP's job, but role/permission definition
is the product's gap to close.
Decision¶
Identity and group membership stay in the customer's IdP. Role definitions — named roles and the permission set each grants — become declarative deployment configuration consumed by the gateway, with the current five roles shipped as the built-in default. The permission vocabulary itself remains a fixed, product-owned contract.
Concretely:
- Roles are config. A roles document (YAML: role name → list of
permissions) is supplied via a gateway flag/file, rendered from a
Helm value (
gateway.auth.roles) into a ConfigMap. Absent configuration, the gateway uses the built-in default equal to today'sRolePermissionstable, so existing deployments are unaffected. Customers may add roles, remove shipped ones, and tune permission sets per role; role names are free-form. - Permissions are the contract. The vocabulary (
read,operate,operate-lead,engineer,supervise,admin) is defined, tested, and documented by the product and changes only via product releases. The gateway fails fast at startup on unknown permission names and warns when no configured role grantsadmin. - Product code checks permissions, never role names. Every direct
group comparison in handlers migrates to
auth.RequirePermission; the cross-site admin bypass keys on theadminpermission instead of thedcs-adminname. After the migration, role names appear in product code only where the default table is defined. - The UI learns permissions from the gateway. The auth session
payload carries the user's resolved permission set; the frontend
gates pages and controls on permissions and drops its hardcoded
role-name constants and
ENDPOINT_ACCESStable. - Two conventions stay product-defined: the
dcs-site-*group prefix (site assignment) anddcs-mes-integration(the integration identity shape enforced by the MES fence). Integration identities are structural, not staffing policy. - No in-product role administration surface. Role definitions change through the deployment's own GitOps/change-management flow (values file → ConfigMap → rollout), not through a UI, API, or CRD.
Alternatives Considered¶
- Roles as CRDs (cluster-scoped Role resources watched by the
gateway) — declarative and visible to
kubectl/dcs. Not chosen: the gateway is the sole policy enforcement point, and making its authorization policy mutable through the very API surface it protects creates a self-referential escalation path (a compromised admin session could rewrite policy live); it adds watch/bootstrap machinery no second consumer needs; and for the pharma target, security-policy changes belong in the customer's change-managed deployment flow, which a values change rides naturally and a live CR edit sidesteps. - In-product role-editing UI/API backed by a ConfigMap — most discoverable. Not chosen for the same self-referential concern plus solo-maintainer surface growth (a policy editor must itself be authorized, audited, and validated). Revisit on concrete customer demand as a new ADR; the config schema chosen here would back it.
- Full policy engine (per-route or attribute-based rules, OPA-style) — maximum flexibility. Not chosen: the test and compliance-evidence matrix explodes when customers can compose arbitrary policies; the fixed permission vocabulary is exactly the tested, documentable middle ground. Customers compose roles from audited capabilities, not rules from scratch.
- Do nothing (five fixed roles, group-mapping as the only lever) — cheapest. Not chosen: every customer defines operator/supervisor/ engineer boundaries differently; a compiled-in table cannot absorb that variance and forces forks or feature requests for what is configuration.
Consequences¶
- Code that moves:
pkg/auth/authz.gogains a loader (defaults + validation) andRolePermissionsstops being a package-level mutable map;humanRoleGroups/IsMESOnlyderive from the loaded config; ~20 gateway call sites migrate fromRequireGroup/RequireAnyGroup/HasGrouptoRequirePermission;cmd/dcs-gatewaygrows the flag and file loading; the auth session payload adds resolved permissions (docs/api-reference.mdupdates per ADR 0003);auth.jsre-gates on permissions. - Behavior normalization is part of the migration. Some direct group lists are not expressible as a cumulative tier and look like drift (e.g. an MES endpoint admitting operator and engineer but not lead-operator or supervisor). The migration issue must enumerate each delta and confirm it as intended normalization, not silent change.
- Chart-shape change → release-worthy: new
gateway.auth.rolesvalue, ConfigMap template, checksum annotation for rollout on change. - Compliance: IEC 62443 SR 1.3 evidence improves (role/permission
definition becomes customer-controlled, change-managed
configuration);
docs/compliance/iec62443.mdanddocs/security-operations.mdupdate when the implementation lands, including guidance that role-config changes are GxP change-controlled items in the deployment's quality system. - Default-path safety: with no roles config supplied, behavior is identical to today; the five shipped roles remain the documented recommended default.
- Reversibility: high for the config mechanism (defaults preserve behavior; deleting the flag restores the constant table). Low for the contract claim — once customers write roles files against the permission vocabulary, renaming or removing a permission is a breaking config change and must follow deprecation discipline.