mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
89a7a37776
* 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>
45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
import { OpenAPI } from '$lib/gen'
|
|
|
|
/**
|
|
* Share-read-link support. When viewing a run via a share link
|
|
* (`/run/{id}?view_token=...`), the token grants the current authenticated member
|
|
* read access to that job and its flow subtree on the backend.
|
|
*
|
|
* The token is attached to every generated-client request via the `X-View-Token`
|
|
* header (registered once below) so we don't have to thread it through every
|
|
* `JobService` call. `EventSource`/SSE can't set headers, so those URLs read
|
|
* `getViewToken()` and append it as a `view_token` query param instead.
|
|
*/
|
|
let currentViewToken: string | undefined = undefined
|
|
|
|
export function setViewToken(token: string | undefined): void {
|
|
currentViewToken = token || undefined
|
|
}
|
|
|
|
export function getViewToken(): string | undefined {
|
|
return currentViewToken
|
|
}
|
|
|
|
/**
|
|
* Append the current view token as a `view_token` query param to a URL/path.
|
|
* Used for download links (plain `<a href>` and `downloadViaClient`), which don't
|
|
* go through the request interceptor that adds the `X-View-Token` header.
|
|
* Returns the url unchanged when no share link is active.
|
|
*/
|
|
export function appendViewToken(url: string): string {
|
|
if (!currentViewToken) return url
|
|
const sep = url.includes('?') ? '&' : '?'
|
|
return `${url}${sep}view_token=${encodeURIComponent(currentViewToken)}`
|
|
}
|
|
|
|
// Register the request interceptor exactly once. It is a no-op unless a view token
|
|
// is currently set, so it is safe to keep installed for the whole session.
|
|
OpenAPI.interceptors.request.use((options) => {
|
|
if (currentViewToken) {
|
|
const headers = new Headers(options.headers)
|
|
headers.set('X-View-Token', currentViewToken)
|
|
options.headers = headers
|
|
}
|
|
return options
|
|
})
|