mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 00:03:07 +00:00
* fix(backend): authorize single-job read endpoints by job/flow visibility Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(jobs): share read links + cached access checks for run visibility - Cache the job read-access RLS probe (size-bounded LRU keyed by the caller's authz-relevant identity + job id; no TTL since job-side inputs are immutable). - Inherit visibility along the full parent_job chain so any flow you can see lets you read its (deeply nested) steps. - Share read links: GET /jobs/job_view_token/{id} mints a stateless HMAC(workspace_key, job_id) token (only if the caller can read the job); the token grants an authenticated member read of that job and its flow subtree via a ?view_token query param or X-View-Token header. Run page gains a Share button and honors a ?view_token link. - Denied-but-existing reads now return 403 with guidance to request a share link (vs 404 for non-existent), and the run page renders that case with instructions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(jobs): address PR review — scope-tag check on mint, constant-time view-token verify - P1 (Codex): get_job_view_token now enforces the caller's if_jobs:filter_tags scope before minting, so a tag-scoped token can't mint a transferable link for a job outside its tags. Adds a scoped-token regression test (allowed + denied). - Constant-time view-token verification (HmacSha256::verify_slice) instead of comparing hex strings (Claude/Pi nit). - get_completed_job_result: an authed reader passing an invalid suspended-secret triple now falls through to the normal visibility gate instead of erroring out (Claude nit); unauthenticated callers still rejected. - Length-prefix the read-access cache key fields so no input values can collide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(api): add job_view_token to openapi spec; use generated client in run page Addresses Codex review nit: the new GET /jobs/job_view_token/{id} endpoint was missing from openapi.yaml (the source the frontend client is generated from). Adds the path + operationId getJobViewToken, and switches the run page's Share button from a raw fetch to JobService.getJobViewToken. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): carry view_token on share-link downloads Addresses Codex review: download actions bypass the request interceptor that adds X-View-Token (downloadViaClient uses raw fetch; cookie-mode downloads use plain hrefs), so a share-link viewer got 403 downloading logs/results/args. Append the view_token query param to the job download paths (result/logs/args/flow-all-logs) via a new appendViewToken() helper, covering both client-fetch and href modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(jobs): enforce tag scope in require_job_read_access (view-token use side) Addresses Codex P1: the view_token use-side bypassed if_jobs:filter_tags on handlers that don't tag-filter their data query (result_by_id, get_flow_job_debug_info, get_otel_traces) — a tag-scoped token could use someone else's valid share token to read out-of-scope job data. Move the tag-scope check into require_job_read_access (runs before any created_by/view_token/RLS grant), so it applies uniformly to every gated handler; removes the now-redundant explicit check in get_job_view_token. Adds a use-side regression test (scoped token + valid out-of-scope view_token denied on otel/result_by_id; in-scope still allowed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): include workspace in share read link Addresses Codex P1: the copied share URL omitted the workspace. The token is signed with the run's workspace key and the logged layout only switches $workspaceStore when the URL carries workspace=, so a recipient whose persisted active workspace differs would open the link against the wrong workspace and the token would fail validation. Pin workspace= alongside view_token in the link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(jobs): authorize get_result_maybe get_started branch for queued jobs Addresses Codex P1: get_completed_job_result_maybe only gated when a completed row existed; with ?get_started=true a non-reader reached the fallback branch and got started:true for a running private job. Now fetches created_by and authorizes (created_by/view_token/RLS, or anonymous for unauth) before disclosing running-state; a non-existent job still returns started:false (leaks nothing). Adds a regression test with a queued (no completed row) private job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
2.6 KiB
Svelte
95 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { Download, InfoIcon, ClipboardCopy, Expand } from 'lucide-svelte'
|
|
import Popover from './Popover.svelte'
|
|
import { copyToClipboard } from '$lib/utils'
|
|
import { downloadViaClient, shouldDownloadViaClient } from '$lib/utils/downloadFile'
|
|
import { appendViewToken } from '$lib/viewToken'
|
|
import type { DisplayResultUi } from './custom_ui'
|
|
import { createEventDispatcher } from 'svelte'
|
|
|
|
interface Props {
|
|
customUi?: DisplayResultUi | undefined
|
|
filename?: string | undefined
|
|
workspaceId?: string | undefined
|
|
jobId?: string | undefined
|
|
nodeId?: string | undefined
|
|
base: string
|
|
result: any
|
|
disableTooltips?: boolean
|
|
}
|
|
|
|
let {
|
|
customUi = undefined,
|
|
filename = undefined,
|
|
workspaceId = undefined,
|
|
jobId = undefined,
|
|
nodeId = undefined,
|
|
base,
|
|
result,
|
|
disableTooltips = false
|
|
}: Props = $props()
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
function toJsonStr(result: any) {
|
|
try {
|
|
return JSON.stringify(result ?? null, null, 4) ?? 'null'
|
|
} catch (e) {
|
|
return 'error stringifying object: ' + e.toString()
|
|
}
|
|
}
|
|
|
|
let resultApiPath = $derived(
|
|
workspaceId && jobId
|
|
? appendViewToken(
|
|
nodeId
|
|
? `/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
|
: `/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
|
)
|
|
: undefined
|
|
)
|
|
let downloadName = $derived(`${filename ?? 'result'}.json`)
|
|
</script>
|
|
|
|
<div class={twMerge('flex flex-row gap-2.5 z-10 text-primary -mt-1 items-center')}>
|
|
{#if customUi?.disableDownload !== true}
|
|
{#if resultApiPath && shouldDownloadViaClient()}
|
|
<button
|
|
class="text-current"
|
|
onclick={() => downloadViaClient(resultApiPath!, downloadName)}
|
|
aria-label="Download result"
|
|
>
|
|
<Download size={14} />
|
|
</button>
|
|
{:else}
|
|
<a
|
|
download={downloadName}
|
|
class="text-current"
|
|
href={resultApiPath
|
|
? `${base}/api${resultApiPath}`
|
|
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
|
>
|
|
<Download size={14} />
|
|
</a>
|
|
{/if}
|
|
{/if}
|
|
{#if disableTooltips !== true}
|
|
<Popover documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering">
|
|
{#snippet text()}
|
|
The result renderer in Windmill supports rich display rendering, allowing you to customize
|
|
the display format of your results.
|
|
{/snippet}
|
|
<div>
|
|
<InfoIcon size={14} />
|
|
</div>
|
|
</Popover>
|
|
{/if}
|
|
<button onclick={() => copyToClipboard(toJsonStr(result))}>
|
|
<ClipboardCopy size={14} />
|
|
</button>
|
|
<button onclick={() => dispatch('open-drawer')}>
|
|
<Expand size={14} />
|
|
</button>
|
|
</div>
|