feat: configurable workspace roles with per-workspace overrides (#11821)

* 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>
This commit is contained in:
Ruben Fiszel
2026-10-21 15:51:54 +02:00
committed by GitHub
co-authored by Claude Opus 5.5 windmill-internal-app[bot]
parent c740a8f369
commit 21d2a49971
106 changed files with 3966 additions and 822 deletions
+12 -18
View File
@@ -185,6 +185,10 @@ async fn list_scripts(
Query(pagination): Query<Pagination>,
Query(lq): Query<ListScriptQuery>,
) -> JsonResult<Vec<ListableScript>> {
// A role that cannot write code lists only what it can run: no libraries, no drafts.
let cannot_code = !windmill_common::roles::caller_capabilities(&db, &w_id, &authed)
.await?
.code;
let (per_page, offset) = paginate_without_limits(pagination);
let mut sqlb = SqlBuilder::select_from("script as o")
.fields(&[
@@ -255,7 +259,7 @@ async fn list_scripts(
.as_ref()
.map(|x| !x.contains(&"preprocessor".to_string()))
.unwrap_or(true))
|| authed.is_operator
|| cannot_code
{
// only include scripts that have a runnable entrypoint. Use a
// deny-list: anything that isn't a 'lib' (library script without
@@ -312,7 +316,7 @@ async fn list_scripts(
);
}
}
if authed.is_operator {
if cannot_code {
sqlb.and_where_eq("kind", quote("script"));
} else if let Some(lowercased_kinds) = lowercased_kinds {
let safe_kinds = lowercased_kinds
@@ -360,7 +364,7 @@ async fn list_scripts(
// `include_draft_only` so picker callers stay deployed-only (home page opts in);
// skipped past page 0 or under any narrowing filter to keep pagination clean.
if lq.include_draft_only.unwrap_or(false)
&& !authed.is_operator
&& !cannot_code
&& offset == 0
&& lq.path_start.is_none()
&& lq.path_exact.is_none()
@@ -1352,11 +1356,7 @@ async fn create_script_internal<'c>(
Option<PostCommitDeploy>,
Vec<MovedNativeTrigger>,
)> {
if authed.is_operator {
return Err(Error::NotAuthorized(
"Operators cannot create scripts for security reasons".to_string(),
));
}
windmill_common::workspaces::check_role_can_code(&db, &w_id, &authed, "create scripts").await?;
check_scopes(&authed, || format!("scripts:write:{}", ns.path))?;
// Normalize positive-only settings so a `<= 0` value (e.g. a CLI-pushed `0`) persists as
@@ -4086,11 +4086,8 @@ async fn archive_script_by_path(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<()> {
if authed.is_operator {
return Err(Error::NotAuthorized(
"Operators cannot archive scripts for security reasons".to_string(),
));
}
windmill_common::workspaces::check_role_can_code(&db, &w_id, &authed, "archive scripts")
.await?;
let path = path.to_path();
check_scopes(&authed, || format!("scripts:write:{}", path))?;
if let RuleCheckResult::Blocked(msg) = check_deploy_rules(
@@ -4182,11 +4179,8 @@ async fn archive_script_by_hash(
Extension(webhook): Extension<WebhookShared>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script<ScriptRunnableSettingsInline>> {
if authed.is_operator {
return Err(Error::NotAuthorized(
"Operators cannot archive scripts for security reasons".to_string(),
));
}
windmill_common::workspaces::check_role_can_code(&db, &w_id, &authed, "archive scripts")
.await?;
if let RuleCheckResult::Blocked(msg) = check_deploy_rules(
&w_id,
AuditAuthorable::username(&authed),