mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-23 08:03:33 +00:00
* 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. * feat: add workspace roles settings UI and role pickers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show the server's reason when a settings save fails Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: default role pickers to a defined role and confirm resets of unknown capabilities Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat null role capabilities as unset and key role drafts by map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: gate the frontend on workspace role capabilities Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: gate deploys, bulk actions and chat composition per role kind Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat a superadmin as full in deploy permission checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: leave page visibility out of isRestricted, as the server does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: let a member discard their own draft without the authoring right Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show custom roles as inactive without a valid license Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the user store import on the agent page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show custom roles as frozen without a valid license Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: reorder instance roles with a drag handle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 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> * fix: move the instance role label into the expanded panel 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>
246 lines
8.4 KiB
Svelte
246 lines
8.4 KiB
Svelte
<script lang="ts">
|
|
import Path from './Path.svelte'
|
|
import LabelsInput from './LabelsInput.svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
import Alert from './common/alert/Alert.svelte'
|
|
import { Button } from './common'
|
|
import Tooltip from './Tooltip.svelte'
|
|
import Label from './Label.svelte'
|
|
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
|
import { Loader2, RotateCcw } from 'lucide-svelte'
|
|
import autosize from '$lib/autosize'
|
|
import { type UserExt, userWorkspaces } from '$lib/stores'
|
|
import { capabilitiesIn } from '$lib/roleCapabilities'
|
|
import { isOwner } from '$lib/utils'
|
|
import { isEncryptedDraftValue } from '$lib/encryptedDraft'
|
|
import EncryptedDraftField from './EncryptedDraftField.svelte'
|
|
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
|
|
|
const operatingWorkspace = useOperatingWorkspace()
|
|
|
|
interface Variable {
|
|
value: string
|
|
is_secret: boolean
|
|
description: string
|
|
}
|
|
|
|
interface Props {
|
|
path: string
|
|
initialPath: string
|
|
pathError: string
|
|
variable: Variable
|
|
labels: string[] | undefined
|
|
wsSpecific: boolean
|
|
deployTo: string | undefined
|
|
can_write: boolean
|
|
edit: boolean
|
|
onLoadSecret?: () => void
|
|
/** Fires after the user flips the Secret toggle, with its new state. */
|
|
onSecretChange?: (isSecret: boolean) => void
|
|
/** Whether the deployed variable being edited is secret. */
|
|
deployedSecret?: boolean
|
|
/** Workspace the path is validated against; defaults to the nav workspace. */
|
|
workspace?: string | undefined
|
|
/** The user acting in `workspace`, resolved by the editor above. `undefined` while
|
|
* `null` while that lookup is pending or after it failed: every check below then
|
|
* refuses, rather than answering with the navigation user's rights in another
|
|
* workspace. */
|
|
actingUser: UserExt | null
|
|
}
|
|
|
|
let {
|
|
path = $bindable(),
|
|
initialPath,
|
|
pathError = $bindable(),
|
|
variable = $bindable(),
|
|
labels = $bindable(),
|
|
wsSpecific = $bindable(),
|
|
deployTo,
|
|
can_write,
|
|
edit,
|
|
onLoadSecret,
|
|
onSecretChange,
|
|
deployedSecret = false,
|
|
workspace = undefined,
|
|
actingUser
|
|
}: Props = $props()
|
|
|
|
let ws = $derived(workspace ?? $operatingWorkspace)
|
|
let caps = $derived(
|
|
capabilitiesIn(
|
|
actingUser ?? undefined,
|
|
$userWorkspaces.find((w) => w.id === ws)
|
|
)
|
|
)
|
|
|
|
// Loading the deployed secret overwrites the draft row this form shares with the AI
|
|
// chat, so every path that would trigger it has to be blocked while that row stages a
|
|
// value — otherwise the staged one is replaced and the next deploy carries the old one.
|
|
// '' is the sentinel for "stages nothing", matching the deploy bodies.
|
|
let hasStagedValue = $derived(variable.value !== '')
|
|
|
|
// Un-securing has to send the plaintext, and fetching it silently on the toggle would
|
|
// reveal the secret without the explicit, audited "Load secret value" action.
|
|
let unsecureNeedsLoad = $derived(edit && deployedSecret && variable.is_secret && !hasStagedValue)
|
|
|
|
const MAX_VARIABLE_LENGTH = 10000
|
|
|
|
let editorKind: 'plain' | 'json' | 'yaml' = $state('plain')
|
|
let editor: any = $state(undefined)
|
|
|
|
export function setCode(value: string) {
|
|
editor?.setCode(value)
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-1">
|
|
<label for="path" class="text-xs font-semibold text-emphasis">Path</label>
|
|
<Path
|
|
disabled={initialPath != '' && !isOwner(initialPath, actingUser ?? undefined, ws)}
|
|
bind:error={pathError}
|
|
bind:path
|
|
{initialPath}
|
|
namePlaceholder="variable"
|
|
kind="variable"
|
|
workspaceOverride={workspace}
|
|
{actingUser}
|
|
/>
|
|
<LabelsInput bind:labels />
|
|
</div>
|
|
<!-- No <label> for the heading: clicking it would flip the toggle, and a wrapping one would
|
|
stretch the hit area over the whole row and the alert. The toggle carries its name instead. -->
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Secret</span>
|
|
<div class="flex flex-row items-center gap-2">
|
|
<!-- An `$encrypted:` value is only redeemable while the variable stays secret — the
|
|
deploy endpoints decrypt the marker inside their `is_secret` branch and store it
|
|
verbatim otherwise — so un-securing one has to be unreachable until it is Reset. -->
|
|
<Toggle
|
|
ariaLabel="Secret"
|
|
class="w-fit"
|
|
on:change={(e) => onSecretChange?.(e.detail)}
|
|
bind:checked={variable.is_secret}
|
|
disabled={unsecureNeedsLoad ||
|
|
(edit && (!actingUser || !caps.code || isEncryptedDraftValue(variable.value)))}
|
|
/>
|
|
{#if unsecureNeedsLoad && can_write && caps.code}
|
|
<span class="text-2xs text-secondary">
|
|
Load the secret value or enter a new one to make it non-secret
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{#if variable.is_secret}
|
|
<Alert type="info" title="Audit log for each access">
|
|
Every secret is encrypted at rest and in transit with a key specific to this workspace. In
|
|
addition, any read of a secret variable generates an audit log whose operation name is:
|
|
variables.decrypt_secret
|
|
</Alert>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if deployTo}
|
|
<Label
|
|
label="Workspace specific"
|
|
tooltip="Prevents this variable from being deployed to prod/staging. May have been enabled automatically because a workspace-specific resource references this variable via $var:. Disabling this toggle does not retroactively un-mark the resource that referenced it."
|
|
>
|
|
<Toggle bind:checked={wsSpecific} />
|
|
</Label>
|
|
{/if}
|
|
|
|
<div class="flex flex-col gap-1">
|
|
<label for="variable-value" class="flex flex-row justify-left items-center">
|
|
<span class="text-xs font-semibold text-emphasis">Variable value </span>
|
|
{#if !isEncryptedDraftValue(variable.value)}
|
|
<span class="text-xs text-secondary font-normal">
|
|
({variable.value.length}/{MAX_VARIABLE_LENGTH} characters)
|
|
</span>
|
|
{/if}
|
|
{#if edit && variable.is_secret}
|
|
<div class="ml-3"></div>
|
|
{#if hasStagedValue}
|
|
<!-- Clearing the staged value is the only way back to loading the deployed one;
|
|
see `hasStagedValue`. An `$encrypted:` value additionally cannot be displayed. -->
|
|
<Button
|
|
size="xs"
|
|
variant="default"
|
|
startIcon={{ icon: RotateCcw }}
|
|
disabled={!can_write}
|
|
on:click={() => (variable.value = '')}
|
|
>
|
|
Reset
|
|
</Button>
|
|
{:else if !caps.code}
|
|
<div class="p-2 border">Your role cannot load secret values</div>
|
|
{:else}
|
|
<Button size="xs" variant="default" on:click={() => onLoadSecret?.()}>
|
|
Load secret value<Tooltip>Will generate an audit log</Tooltip>
|
|
</Button>
|
|
{/if}
|
|
{/if}
|
|
</label>
|
|
<div>
|
|
{#if isEncryptedDraftValue(variable.value)}
|
|
<!-- The draft's secret value was encrypted server-side and can't be
|
|
loaded back. Saving deploys the last saved value as-is; Reset clears
|
|
it so a new secret can be typed. -->
|
|
<EncryptedDraftField disabled={!can_write} onReset={() => (variable.value = '')} />
|
|
{:else}
|
|
<div class="flex flex-col gap-2">
|
|
<ToggleButtonGroup bind:selected={editorKind}>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="plain" label="Plain" {item} />
|
|
<ToggleButton value="json" label="Json" {item} />
|
|
<ToggleButton value="yaml" label="YAML" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
{#if editorKind == 'plain'}
|
|
<textarea
|
|
disabled={!can_write}
|
|
rows="4"
|
|
use:autosize
|
|
bind:value={variable.value}
|
|
placeholder="Update variable value"
|
|
id="variable-value"
|
|
></textarea>
|
|
{:else if editorKind == 'json'}
|
|
<div class="border rounded mb-4 w-full">
|
|
{#await import('$lib/components/SimpleEditor.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
bind:this={editor}
|
|
autoHeight
|
|
lang="json"
|
|
bind:code={variable.value}
|
|
fixedOverflowWidgets={false}
|
|
class="bg-surface-tertiary"
|
|
/>
|
|
{/await}
|
|
</div>
|
|
{:else if editorKind == 'yaml'}
|
|
<div class="border rounded mb-4 w-full">
|
|
{#await import('$lib/components/SimpleEditor.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
bind:this={editor}
|
|
autoHeight
|
|
lang="yaml"
|
|
bind:code={variable.value}
|
|
fixedOverflowWidgets={false}
|
|
class="bg-surface-tertiary"
|
|
/>
|
|
{/await}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Description</span>
|
|
<textarea rows="4" use:autosize bind:value={variable.description} placeholder="Used for X"
|
|
></textarea>
|
|
</label>
|