mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
fix(apps): hand the sandboxed SDK token over only once per loaded document
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
This commit is contained in:
co-authored by
Claude Fable 5
parent
025069c41e
commit
571bf7ebcf
@@ -21,10 +21,7 @@
|
||||
type OnBehalfOfChoice
|
||||
} from '$lib/components/OnBehalfOfSelector.svelte'
|
||||
import { canUserBypassRuleKind, protectionRulesState } from '$lib/workspaceProtectionRules.svelte'
|
||||
import {
|
||||
FRONTEND_SDK_SCOPES,
|
||||
MIN_SANDBOXED_SDK_VERSION
|
||||
} from '$lib/components/raw_apps/sdkScopes'
|
||||
import { FRONTEND_SDK_SCOPES } from '$lib/components/raw_apps/sdkScopes'
|
||||
|
||||
const WM_DEPLOYERS_GROUP = 'wm_deployers'
|
||||
|
||||
@@ -373,10 +370,10 @@
|
||||
{/if}
|
||||
{#if policy.sandbox == true && policy.frontend_sdk_scopes?.length}
|
||||
<div class="mt-2">
|
||||
<Alert type="info" title="Requires windmill-client {MIN_SANDBOXED_SDK_VERSION}" size="xs">
|
||||
A sandboxed app calls the API cross-origin, which earlier versions of the SDK cannot do.
|
||||
Make sure your app depends on <code>windmill-client@^{MIN_SANDBOXED_SDK_VERSION}</code> and
|
||||
redeploy — apps bundled against an older version fail with a CORS error.
|
||||
<Alert type="info" title="Redeploy to use the SDK from a sandboxed app" size="xs">
|
||||
A sandboxed app calls the API cross-origin, which older <code>windmill-client</code> versions
|
||||
cannot do. An app bundled before this Windmill version fails with a CORS error until you deploy
|
||||
it again, which re-bundles it against a current client.
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
})
|
||||
|
||||
// Read by RawAppPreview: the consented viewer-scoped SDK token to expose to
|
||||
// the bundle (unsandboxed raw apps only).
|
||||
// the bundle, sandboxed or not.
|
||||
setContext('RAW_APP_SDK_TOKEN', {
|
||||
get value() {
|
||||
return sdkToken
|
||||
@@ -268,8 +268,8 @@
|
||||
appPath = resp.app_path ?? undefined
|
||||
workspaceId = resp.workspace_id ?? undefined
|
||||
// The backend only advertises scopes where a token could actually be
|
||||
// minted (raw, unsandboxed, viewer authenticated), and mints one only on
|
||||
// the follow-up request carrying the viewer's consent.
|
||||
// minted (a raw app, viewer authenticated), and mints one only on the
|
||||
// follow-up request carrying the viewer's consent.
|
||||
sdkScopes = resp.sdk_scopes?.length ? resp.sdk_scopes : undefined
|
||||
viewerEmail = resp.viewer_email ?? ''
|
||||
sdkToken = undefined
|
||||
|
||||
@@ -57,10 +57,11 @@
|
||||
// - DEFAULT (isolated): a real API URL serving a sandboxed, opaque-origin
|
||||
// document (`CSP: sandbox` response header + the iframe sandbox attribute),
|
||||
// so a malicious bundle can never reach the authenticated Windmill origin
|
||||
// (no cookie, no window.parent, no token). Root-relative so it resolves
|
||||
// against the real host even when this component itself runs inside an opaque
|
||||
// viewer (where `location.origin` is "null"). Context is handed over via
|
||||
// postMessage — never baked into the document, never a credential.
|
||||
// (no cookie, no window.parent). Root-relative so it resolves against the
|
||||
// real host even when this component itself runs inside an opaque viewer
|
||||
// (where `location.origin` is "null"). Context — and, when the viewer
|
||||
// approved SDK scopes, the viewer-scoped token — is handed over via
|
||||
// postMessage, never baked into the document.
|
||||
// - UNSANDBOXED (the default — publisher did not opt into isolation): a
|
||||
// client-built blob: wrapper (same-origin with the SPA) loaded with `allow-same-origin`,
|
||||
// so relative `fetch('/api/...')` and the session cookie work. The backend
|
||||
@@ -150,8 +151,21 @@
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// The token may only be handed over once per document we loaded ourselves. The
|
||||
// reply is addressed to a browsing context, not a document, and it must target
|
||||
// `*` because the sandboxed frame has an opaque origin — so a page the bundle
|
||||
// (or a link the viewer clicked) navigated to keeps the same `contentWindow`
|
||||
// and could ask for the credential again by posting `windmill:ready`. Reset
|
||||
// only when WE change the iframe's src, which a navigation away does not.
|
||||
let sdkHandedOff = false
|
||||
$effect(() => {
|
||||
iframeSrc
|
||||
sdkHandedOff = false
|
||||
})
|
||||
|
||||
function respondCtx() {
|
||||
const sdkToken = sdkTokenCtx?.value
|
||||
const sdkToken = sdkHandedOff ? undefined : sdkTokenCtx?.value
|
||||
if (sdkToken) sdkHandedOff = true
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
type: 'windmill:ctx',
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
// in the backend `apps.rs` — both lists must stay in sync), plus the viewer-side
|
||||
// consent persistence for the permission banner.
|
||||
|
||||
/** A sandboxed app calls the API from an opaque origin, which needs the
|
||||
* credential-free client shipped alongside this release: earlier versions force
|
||||
* `credentials: 'include'`, which CORS rejects. Tracks the monorepo version
|
||||
* because `windmill-client` is published from it in lockstep. */
|
||||
export const MIN_SANDBOXED_SDK_VERSION = __pkg__.version
|
||||
|
||||
export const FRONTEND_SDK_SCOPES: { value: string; label: string; description: string }[] = [
|
||||
{
|
||||
value: 'jobs:run',
|
||||
|
||||
Reference in New Issue
Block a user