Roles and Permissions (RBAC)
The two-tier model#
Authorization has two layers that compose:
- Global roles —
admin,editor,viewer— govern the instance: connectors, settings, and who may create projects. - Project roles —
Viewer,Developer,Maintainer,Owner— govern each ETL project, per member, with awareness of protected environments.
Every decision flows through a single backend chokepoint, and an enforcement meta-test fails the build if any mutating ETL route ships without a guard.
Global roles#
| Global role | Grants |
|---|---|
admin |
Instance administrator. Implicit Owner of every project, plus all settings surfaces (users, tokens, audit, providers, license). |
editor |
May create projects (becoming their Owner) and manage instance-scoped connectors. |
viewer |
Baseline read access only. |
Any global role — including viewer — grants baseline read access across the
instance, including all projects.
Note. Read endpoints are enforced by authentication plus this global-role baseline. The model defines view capabilities (
project.view,task.view,connection.view,schedule.view) and resolves them for every project role, but in this release read endpoints check that the caller is authenticated rather than consulting per-project membership. Treat project read access as instance-wide for any signed-in user; project roles gate writes.
Project role × capability matrix#
The full capability vocabulary is 20 ids. Fifteen are resolved against the member's project role:
| Capability | Viewer | Developer | Maintainer | Owner |
|---|---|---|---|---|
project.view |
yes | yes | yes | yes |
task.view |
yes | yes | yes | yes |
connection.view |
yes | yes | yes | yes |
schedule.view |
yes | yes | yes | yes |
task.create |
– | unprotected envs only | yes | yes |
task.update |
– | unprotected envs only | yes | yes |
task.delete |
– | unprotected envs only | yes | yes |
task.run |
– | unprotected envs only | yes | yes |
connection.bind |
– | unprotected envs only | yes | yes |
schedule.manage |
– | unprotected envs only | yes | yes |
project.environment.manage |
– | – | yes | yes |
task.promote |
– | – | yes (any target) | yes (any target) |
project.update |
– | – | – | yes |
project.delete |
– | – | – | yes |
project.member.manage |
– | – | – | yes |
The remaining five are instance-flag capabilities, resolved from the
global role rather than any project role: project.create (global editor+),
connection.create, connection.update, connection.delete, and
instance.admin. In this release, connector management endpoints gate on the
global editor role directly; instance.admin maps to the global admin
role.
Environment gating#
Each project has named environments (only dev is auto-created; environments
named prod or production default to protected). An environment can be
marked protected, which changes what a Developer may do:
- Exactly six write capabilities are environment-gated:
task.create,task.update,task.delete,task.run,connection.bind, andschedule.manage. - In an unprotected environment, a Developer holds all six. In a
protected environment, all six are removed from Developers — the denial
carries reason
protected_environment. Maintainers and Owners keep them everywhere. - Promotion is not environment-gated.
task.promoterequires Maintainer or Owner, and a Maintainer may promote into a protected environment — the role check is the control, not the target's protection flag. Protection gates direct edits, while promotion is the sanctioned, diff-and-confirm path into protected environments.
Warning.
project.environment.manage(Maintainer+) includes toggling the protected flag itself — a Maintainer can un-protect an environment and then edit it directly. If that separation matters in your organization, grant Maintainer sparingly and treat Owner as the trust boundary.
Ownership rules#
- Creator becomes Owner. Whoever creates a project is its first Owner.
- Owner-only trio.
project.update,project.delete, andproject.member.managerequire the Owner role (or globaladmin). - Last-Owner guard. Removing or demoting the last remaining Owner is rejected with HTTP 409 — a project can never be left ownerless.
Denial semantics — the 403 envelope#
Every permission denial returns a structured envelope:
{
"code": "permission_denied",
"reason": "protected_environment",
"required_capability": "task.update",
"project_id": "00000000-0000-0000-0000-000000000101",
"environment_id": "00000000-0000-0000-0000-000000000201"
}
reason comes from a closed vocabulary:
| Reason | Meaning |
|---|---|
unknown_capability |
The capability id is not in the vocabulary (denied, never allowed-by-accident). |
instance_capability_required |
The action needs an instance flag (e.g. global editor), not a project role. |
missing_project_context |
The request could not be tied to a project. |
not_a_member |
The caller has no membership in this project. |
protected_environment |
An environment-gated write hit a protected environment (Developer). |
insufficient_role |
The member's project role is below what the capability requires. |
Denied attempts are recorded in the audit log as etl_authz_denied
events — including the capability, reason, and context.
Worked examples#
A Developer in dev vs prod. Dana is a Developer on the project. In the
unprotected dev environment she creates, edits, runs, and schedules tasks
freely. The same task.update request against the protected prod
environment returns 403 with reason: "protected_environment". To change
prod, she edits in dev and asks a Maintainer to promote.
A Maintainer and a protected target. Sam is a Maintainer. He promotes a
task from dev into the protected prod environment — allowed, because
task.promote checks role, not target protection. He can also flip the
protected flag on prod via project.environment.manage; see the warning
above about what that implies.
An Owner change that would strand the project. The only Owner tries to demote themselves to Maintainer. The request fails with 409 until a second Owner exists.
Related pages#
- Environment and promotion workflows: environments and promotion
- Managing users and global roles: user management
- Token-based access and scopes: Authentication