* feat: configurable workspace roles with per-workspace overrides Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: close role review gaps in pickers, declarative sync and removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: keep resolve_role_name doc on its function Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: validate overrides on instance role changes and bill admins fully Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: allow pipeline drafts and keep role checks on one connection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: list data pipelines in operator_can_draft Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: expect a 403 role refusal when an operator resolves jobs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: require a valid license for custom workspace roles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: freeze workspace roles when the license lapses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: pin the ee companion for workspace roles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: broadcast role override changes from the write path, not a trigger Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update ee-repo-ref to b5294d38db082bdd104d5358a47695390694c05d This commit updates the EE repository reference after PR #891 was merged in windmill-ee-private. Previous ee-repo-ref: 688e00c5982865b14098f530d54280e2311994c0 New ee-repo-ref: b5294d38db082bdd104d5358a47695390694c05d Automated by sync-ee-ref workflow. * fix: let an admin row that kept the operator flag hold full rights everywhere Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat superadmin members as admins and return role names from get user Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
4.1 KiB
Workspace roles
What a non-admin member may author, manage and see is its role. developer and operator are
the default roles; a licensed EE instance defines its own list in the workspace_roles global
setting (at least one role), and any workspace may override a role's capabilities in
workspace_settings.role_overrides, or, when licensed, add roles of its own there. A lapsed
license freezes them rather than dropping them (custom_roles_enabled): the stored list and the
workspace-only roles keep applying, writes that change them are refused, and resetting the list to
the defaults or removing a workspace-only role stays open. Falling back to the defaults instead
would hand a restricted developer everything. Workspace admin is not a
role: it is the is_admin flag on top of whichever role a member holds, and holds everything. The
code is windmill_common::roles.
Capabilities
| Capability | Values | Enforced |
|---|---|---|
code |
bool | yes: scripts, previews, dependency jobs, dynamic selects, resolving jobs, data table migrations, eval datasets |
flows |
none / compose / full |
yes |
apps |
none / compose / full |
yes; low-code apps and worker-built apps need full |
manage_schedules, manage_triggers |
bool | yes, see docs/operator-write-rights.md |
visibility.* |
bool per page | no, UI only |
compose is the former operator builder right: the composition-only checks of
docs/operator-builder-rights.md run for it and are skipped only at full. full flows or apps
without code is refused at write, since both carry inline code.
Stored values are patches: an unset key falls through to the layer below, so a key added later
reads as "never configured". The layers, top first: the workspace override, the instance
definition, then RoleCapabilities::base_for (FULL for developer, the legacy operator for any
other name). A role name no longer defined resolves to NONE, so deleting a role fails closed;
deleting one that a member, invite, auto-invite or instance group mapping still names is refused,
and so is deleting an instance role that a workspace still overrides (the override would fall onto
base_for instead of the definition it was written against).
Who is restricted
ApiAuthed::is_operator means "this member's role is restricted", computed at authentication from
the role, never from usr.operator alone. Every gate fast-paths on it (caller_capabilities
returns FULL for an unrestricted or admin caller without reading the role), so it must stay
derived from the role: a stale flag lets a restricted member through. The authentication cache
holds it for up to 120s.
usr.operator is kept as "this role cannot write code", which is what clients that predate roles
read it as. A member's role is usr.workspace_role, or, while that is NULL, operator /
developer from usr.operator: rows written by paths that know nothing of roles stay meaningful.
The old usr.role column is unrelated and holds legacy display strings.
Seats
A role that writes or composes anything costs a full seat; anything else half. Billing SQL cannot
merge capability patches, so role_seat materializes the author flag per role
(workspace_id = '' for the instance, a row per workspace override), rewritten by
sync_role_seats after every write to either setting. Every count reads it through
AUTHOR_SEAT_SQL, and usage telemetry reports builders (authors anywhere) against non-builders the
same way. A change that adds seats under an offline license is priced inside the transaction that
writes it, by check_seat_cap_for_role_change.
Legacy operator_settings
/operator_settings still works: it is applied as an override of the operator role, dropping any
key equal to what the instance gives that role, and the payload is mirrored into the
operator_settings column for git sync and older servers. The migration moved every key a
workspace had changed from its default into role_overrides.operator the same way. The workspace
list still returns an operator_settings object for a restricted member, synthesized from its
role, alongside role and capabilities.