feat: devOps role can edit worker groups (#5984)

* feat: allow devOps role to edit worker groups config

- Updated backend permission checks in configs.rs to use require_devops_role() instead of require_super_admin()
- Updated frontend UI in workers page to show worker group management for devOps users
- Updated WorkerGroup component to allow devOps role access to all configuration features
- Updated AssignableTagsInner component to allow devOps users to manage tags

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* Update configs.rs

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
claude[bot]
2025-06-18 23:06:53 +02:00
committed by GitHub
parent d3c1c34baf
commit d53b620e46
4 changed files with 38 additions and 37 deletions
+5 -5
View File
@@ -21,7 +21,7 @@ use windmill_common::{
DB,
};
use crate::{db::ApiAuthed, utils::require_super_admin};
use crate::{db::ApiAuthed, utils::{require_devops_role}};
pub fn global_service() -> Router {
Router::new()
@@ -103,7 +103,7 @@ async fn get_config(
Path(name): Path<String>,
Extension(db): Extension<DB>,
) -> error::JsonResult<Option<serde_json::Value>> {
require_super_admin(&db, &authed.email).await?;
require_devops_role(&db, &authed.email).await?;
let config = sqlx::query_as!(Config, "SELECT * FROM config WHERE name = $1", name)
.fetch_optional(&db)
@@ -119,7 +119,7 @@ async fn update_config(
authed: ApiAuthed,
Json(config): Json<serde_json::Value>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
require_devops_role(&db, &authed.email).await?;
#[cfg(not(feature = "enterprise"))]
if name.starts_with("worker__") {
@@ -157,7 +157,7 @@ async fn delete_config(
Extension(db): Extension<DB>,
authed: ApiAuthed,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
require_devops_role(&db, &authed.email).await?;
let mut tx = db.begin().await?;
@@ -232,7 +232,7 @@ async fn list_configs(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> error::JsonResult<Vec<Config>> {
require_super_admin(&db, &authed.email).await?;
require_devops_role(&db, &authed.email).await?;
let configs = sqlx::query_as!(Config, "SELECT name, config FROM config")
.fetch_all(&db)
.await?;
@@ -5,7 +5,7 @@
import { ExternalLink, Loader2, X } from 'lucide-svelte'
import { SettingService, WorkerService } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import { superadmin } from '$lib/stores'
import { superadmin, devopsRole } from '$lib/stores'
import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
import { CUSTOM_TAGS_SETTING } from '$lib/consts'
import { base } from '$lib/base'
@@ -80,10 +80,10 @@
loadCustomTags()
sendUserToast('Tag added')
}}
disabled={newTag.trim() == '' || !$superadmin}
disabled={newTag.trim() == '' || !($superadmin || $devopsRole)}
>
Add {#if !$superadmin}
<span class="text-2xs text-tertiary">superadmin only</span>
Add {#if !($superadmin || $devopsRole)}
<span class="text-2xs text-tertiary">superadmin or devops only</span>
{/if}
</Button>
<span class="text-sm text-primary"
+19 -19
View File
@@ -9,7 +9,7 @@
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
import { emptyString, pluralize } from '$lib/utils'
import { enterpriseLicense, superadmin } from '$lib/stores'
import { enterpriseLicense, superadmin, devopsRole } from '$lib/stores'
import Tooltip from './Tooltip.svelte'
import Editor from './Editor.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
@@ -155,7 +155,7 @@
let vcpus_memory = $derived(computeVCpuAndMemory(workers))
let selected = $derived(nconfig?.dedicated_worker != undefined ? 'dedicated' : 'normal')
$effect(() => {
$superadmin && listWorkspaces()
($superadmin || $devopsRole) && listWorkspaces()
})
</script>
@@ -209,7 +209,7 @@
<Drawer bind:this={drawer} size="800px">
<DrawerContent
on:close={() => drawer?.closeDrawer()}
title={$superadmin ? `Edit worker config '${name}'` : `Worker config '${name}'`}
title={($superadmin || $devopsRole) ? `Edit worker config '${name}'` : `Worker config '${name}'`}
>
{#if !$enterpriseLicense}
<Alert type="warning" title="Worker management UI is EE only">
@@ -433,7 +433,7 @@
{:else if selected == 'dedicated'}
{#if nconfig?.dedicated_worker != undefined}
<input
disabled={!$superadmin}
disabled={!($superadmin || $devopsRole)}
placeholder="<workspace>:<script path>"
type="text"
onchange={() => {
@@ -442,7 +442,7 @@
}}
bind:value={nconfig.dedicated_worker}
/>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<div class="py-2"
><Alert
type="info"
@@ -471,11 +471,11 @@
<div class="flex gap-1 items-center">
<input
type="text"
disabled={!$superadmin}
disabled={!($superadmin || $devopsRole)}
placeholder="/path/to/python3.X/site-packages"
bind:value={nconfig.additional_python_paths![i]}
/>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
@@ -497,7 +497,7 @@
</div>
{/each}
{/if}
{#if $superadmin}
{#if $superadmin || $devopsRole}
<div class="flex">
<Button
variant="contained"
@@ -523,12 +523,12 @@
{#each nconfig.pip_local_dependencies as _, i}
<div class="flex gap-1 items-center">
<input
disabled={!$superadmin}
disabled={!($superadmin || $devopsRole)}
type="text"
placeholder="httpx"
bind:value={nconfig.pip_local_dependencies[i]}
/>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
@@ -550,7 +550,7 @@
</div>
{/each}
{/if}
{#if $superadmin}
{#if $superadmin || $devopsRole}
<div class="flex">
<Button
variant="contained"
@@ -584,7 +584,7 @@
{#each customEnvVars as envvar, i}
<div class="flex gap-1 items-center">
<input
disabled={!$superadmin}
disabled={!($superadmin || $devopsRole)}
type="text"
placeholder="ENV_VAR_NAME"
bind:value={envvar.key}
@@ -593,7 +593,7 @@
}}
/>
<ToggleButtonGroup
disabled={!$superadmin}
disabled={!($superadmin || $devopsRole)}
class="w-128"
bind:selected={envvar.type}
on:selected={(e) => {
@@ -610,13 +610,13 @@
</ToggleButtonGroup>
<input
type="text"
disabled={!$superadmin || envvar.type === 'dynamic'}
disabled={!($superadmin || $devopsRole) || envvar.type === 'dynamic'}
placeholder={envvar.type === 'dynamic'
? 'value read from worker env var'
: 'static value'}
bind:value={envvar.value}
/>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
@@ -639,7 +639,7 @@
{/if}
</div>
{/each}
{#if $superadmin}
{#if $superadmin || $devopsRole}
<div class="flex">
<Button
variant="contained"
@@ -657,7 +657,7 @@
</div>
{/if}
</div>
{#if !superadmin}
{#if !($superadmin || $devopsRole)}
<div class="flex flex-wrap items-center gap-1 pt-2">
<Button
variant="contained"
@@ -745,7 +745,7 @@
>
{/if}
<Editor
disabled={!$superadmin}
disabled={!($superadmin || $devopsRole)}
class="flex flex-1 grow h-full w-full"
automaticLayout
scriptLang={'bash'}
@@ -824,7 +824,7 @@
}}
disabled={(!dirty && nconfig?.dedicated_worker == undefined) ||
!$enterpriseLicense ||
!$superadmin}
!($superadmin || $devopsRole)}
>
Apply changes
</Button>
@@ -19,6 +19,7 @@
import {
enterpriseLicense,
superadmin,
devopsRole,
userStore,
workspaceStore,
userWorkspaces
@@ -131,7 +132,7 @@
loadWorkers()
loadWorkerGroups()
loadCustomTags()
$: $superadmin && loadDefaultTagsPerWorkspace()
$: ($superadmin || $devopsRole) && loadDefaultTagsPerWorkspace()
onDestroy(() => {
if (intervalId) {
@@ -271,7 +272,7 @@
}
</script>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<QueueMetricsDrawer bind:this={queueMetricsDrawer} />
{/if}
@@ -331,7 +332,7 @@
tooltip="The workers are the dutiful servants that execute the jobs."
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<div class="flex flex-row-reverse w-full pb-2 items-center gap-4">
<div>
<AssignableTags
@@ -392,7 +393,7 @@
>
<div></div>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<div class="flex flex-row gap-4 items-center">
<Button
size="sm"
@@ -577,7 +578,7 @@
<Cell head>Last ping</Cell>
<Cell head>Worker start</Cell>
<Cell head>Jobs ran</Cell>
{#if (!config || config?.dedicated_worker == undefined) && $superadmin}
{#if (!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)}
<Cell head>Last job</Cell>
<Cell head>Occupancy rate<br />(15s/5m/30m/ever)</Cell>
{/if}
@@ -585,7 +586,7 @@
<Cell head>Limits</Cell>
<Cell head>Version</Cell>
<Cell head>Liveness</Cell>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<Cell head>
Live Shell
<Tooltip>
@@ -604,7 +605,7 @@
<tr class="border-t">
<Cell
first
colspan={(!config || config?.dedicated_worker == undefined) && $superadmin
colspan={(!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)
? 12
: 9}
scope="colgroup"
@@ -655,7 +656,7 @@
>
<Cell>{displayDate(started_at)}</Cell>
<Cell>{jobs_executed}</Cell>
{#if (!config || config?.dedicated_worker == undefined) && $superadmin}
{#if (!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)}
<Cell>
{#if last_job_id}
<a href={`/run/${last_job_id}?workspace=${last_job_workspace_id}`}>
@@ -715,7 +716,7 @@
: 'Unknown'}
</Badge>
</Cell>
{#if $superadmin}
{#if $superadmin || $devopsRole}
<Cell>
<Button
size="xs"