Files
windmill/docs/workspace-roles.md
T

4.3 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
fork bool yes: both fork endpoints, read with caller_role_capabilities; the forker is a developer in the fork, see docs/operator-write-rights.md. Like visibility, not part of is_restricted
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.