Files
windmill/frontend/src/lib/components/flows/header/FlowYamlEditor.svelte
T
e7fc1b2e2e feat: restricted job tokens per script and flow (#11484)
* feat: restricted job tokens (job_token_scopes on scripts and flows)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: admit flow-run reads, skip dedicated workers, gate on worker version

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep restricted jobs off flow runners, preserve scopes on rename and promotion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep restricted jobs off every dedicated handoff, confine progress flow id

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: exclude restricted runnables from dedicated worker startup

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: gate restrictions on the release after 1.821.0

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: store per-job scopes on job_perms instead of v2_job, pin inline runs to the checked version

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: step-level job_token_scopes for flow steps and agent tools

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: fail closed on perms read errors, refuse restricted queue imports, gate step scopes in previews

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: carry a job's scopes on its completion so a re-run keeps the caller's cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: carry a zombie job's scopes into its completion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: leave a zombie for the next sweep when its scopes cannot be read

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* docs: correct the QueuedJobV2 completion comment

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: validate step scopes in batch flows, fail closed on unvalidated step scopes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: refuse flows with step or tool restrictions at push while an older worker is live

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: apply the step-scope worker gate to flow restarts

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: list the job token toggle with the other step and flow settings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* chore: pin the EE companion merged with EE main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* perf: skip scope lookups for unrestricted jobs; list job token setting last

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* style: rustfmt scopes tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: confine restricted job tokens to their own run lineage; drop remaining extra lookups

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* chore: update ee-repo-ref to 259ad3bfeef5285ba80eedc86309b11dca001220

This commit updates the EE repository reference after PR #843 was merged in windmill-ee-private.

Previous ee-repo-ref: 2b77c0225dca441235daf7bf0a06ba968df0c927

New ee-repo-ref: 259ad3bfeef5285ba80eedc86309b11dca001220

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-03 09:33:44 +02:00

135 lines
4.4 KiB
Svelte

<script lang="ts">
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import { filteredContentForExport } from '../utils.svelte'
import YAML from 'yaml'
import { Button } from '$lib/components/common'
import { sendUserToast } from '$lib/toast'
import { Loader2 } from 'lucide-svelte'
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
import SimpleEditor from '../../SimpleEditor.svelte'
const { flowStore } = $state(getContext<FlowEditorContext>('FlowEditorContext'))
interface Props {
drawer: Drawer | undefined
}
let { drawer = $bindable() }: Props = $props()
let code = $state('')
let initialCode = $state('')
let editor = $state(undefined) as SimpleEditor | undefined
let hasChanges = $derived(code !== initialCode)
function reload() {
code = YAML.stringify(filteredContentForExport(flowStore.val))
initialCode = code
editor?.setCode(code)
}
/** `value` is assigned unconditionally below and `FlowEditor` dereferences
* `flowStore.val.value.modules`, so an OpenFlow document missing it takes the
* whole editor down mid-render — after the toast has already claimed success.
* Reject it up front instead. */
function validateShape(parsed: unknown) {
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('the document must be a mapping (key: value)')
}
const value = (parsed as Record<string, unknown>).value
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error("missing 'value' - paste a whole OpenFlow document, not just its value")
}
if (!Array.isArray((value as Record<string, unknown>).modules)) {
throw new Error("'value.modules' must be a list")
}
}
function validateGroups(groups: { start_id: string; end_id: string }[] | undefined) {
if (!groups) return
const seen = new Set<string>()
for (const g of groups) {
const key = `${g.start_id}:${g.end_id}`
if (seen.has(key)) {
throw new Error(`Duplicate group: '${g.start_id}' → '${g.end_id}'`)
}
seen.add(key)
}
}
function apply() {
try {
const parsed = YAML.parse(code)
validateShape(parsed)
validateGroups(parsed.value?.groups)
if (parsed.summary && typeof parsed.summary === 'string') {
flowStore.val.summary = parsed.summary
}
if (parsed.description && typeof parsed.description === 'string') {
flowStore.val.description = parsed.description
}
if (parsed['ws_error_handler_muted'] !== undefined) {
flowStore.val.ws_error_handler_muted = parsed['ws_error_handler_muted']
}
if (parsed['dedicated_worker'] !== undefined) {
flowStore.val.dedicated_worker = parsed['dedicated_worker']
}
if (parsed['visible_to_runner_only'] !== undefined) {
flowStore.val.visible_to_runner_only = parsed['visible_to_runner_only']
}
if (parsed['on_behalf_of_email'] !== undefined) {
flowStore.val.on_behalf_of_email = parsed['on_behalf_of_email']
}
if (parsed['on_behalf_of'] !== undefined) {
flowStore.val.on_behalf_of = parsed['on_behalf_of']
}
if (parsed['job_token_scopes'] !== undefined) {
flowStore.val.job_token_scopes = parsed['job_token_scopes']
}
flowStore.val.value = parsed.value
flowStore.val.schema = parsed.schema
flowStore.val.tag = parsed.tag
refreshStateStore(flowStore)
initialCode = code
sendUserToast('Changes applied')
} catch (e) {
sendUserToast('Error parsing yaml: ' + e, true)
}
}
let editorHeight = $state(0)
</script>
<Drawer on:open={reload} bind:this={drawer} size="800px">
<DrawerContent title="OpenFlow" on:close={() => drawer?.toggleDrawer()}>
{#snippet actions()}
<Button variant="default" unifiedSize="md" disabled={!hasChanges} on:click={reload}
>Reset code</Button
>
<Button variant="accent" unifiedSize="md" disabled={!hasChanges} on:click={apply}
>Apply changes</Button
>
{/snippet}
{#if flowStore.val}
{#await import('../../SimpleEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<div class="h-full w-full overflow-hidden" bind:clientHeight={editorHeight}>
<Module.default
bind:this={editor}
autoHeight
minHeight={editorHeight}
bind:code
lang="yaml"
leadingChangeSync
/>
</div>
{/await}
{/if}
</DrawerContent>
</Drawer>