diff --git a/backend/windmill-api/src/drafts.rs b/backend/windmill-api/src/drafts.rs index 8d6bb8612b..a1e6dc6b7d 100644 --- a/backend/windmill-api/src/drafts.rs +++ b/backend/windmill-api/src/drafts.rs @@ -1177,8 +1177,8 @@ async fn require_can_write_path( /// Resolves to `Ok(())` if `authed` can read at `path`. Three layers: /// 1. admin → always. -/// 2. Path-prefix match against own `u/{username}` or any folder in -/// `authed.folders` (the precomputed read set, with groups + direct +/// 2. Path-prefix match against own `u/{username}`, a group in `authed.groups`, or any +/// folder in `authed.folders` (the precomputed read set, with groups + direct /// grants already factored in). /// 3. RLS-aware `SELECT 1` against the backing table — covers item-level /// extra_perms grants that bypass folder/owner checks. @@ -1203,6 +1203,10 @@ async fn require_can_read_path( if parts.len() >= 2 { match parts[0] { "u" if parts[1] == authed.username => return Ok(()), + // As `require_can_write_path` and the `see_member` RLS policy read it: a + // draft-only `g/` path has no row for the probe below to fall back on, so + // without this a member cannot see a draft their group owns. + "g" if authed.groups.iter().any(|g| g == parts[1]) => return Ok(()), "f" => { let folder = parts[1]; if authed.folders.iter().any(|(name, _, _)| name == folder) { diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index b4724805e2..f14ccc7ee5 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -111,8 +111,9 @@ // event up through these runes-mode components silently drops it. onRestore?: (restoredApp: any) => void // Fired after a successful deploy, which keeps this editor open: `version` is what - // the deploy wrote, for the next draft's fork base, and `head` what is deployed now. - onDeploy?: (e: { version?: number; head?: number }) => void + // the deploy wrote, for the next draft's fork base, and `head` what is deployed + // now, with its author and time. + onDeploy?: (e: { version?: number; head?: number; headBy?: string; headAt?: string }) => void } let { @@ -416,7 +417,12 @@ if ($app) $app.parent_version = claimed // The route owns the pair the out-of-date prompt reads, and this editor stays open // across the deploy, so hand both over rather than leaving it on the old ones. - onDeploy?.({ version: claimed, head: version }) + onDeploy?.({ + version: claimed, + head: version, + headBy: appHistory[0]?.created_by, + headAt: appHistory[0]?.created_at + }) closeSaveDrawer() sendUserToast('App deployed successfully') @@ -1042,7 +1048,13 @@ itemKind="app" path={userDraftPath} draftOnly={newApp} - {onResetToDeployed} + onResetToDeployed={onResetToDeployed && + (async () => { + // Back on the deployed version, so whatever the last deploy could not claim + // no longer describes this editor. + baseUnknown = false + await onResetToDeployed() + })} {loadedFromDraft} {othersDraftsCount} {onOpenOthersDrafts} diff --git a/frontend/src/lib/components/apps/types.ts b/frontend/src/lib/components/apps/types.ts index 76e138893a..75de2d7faf 100644 --- a/frontend/src/lib/components/apps/types.ts +++ b/frontend/src/lib/components/apps/types.ts @@ -184,9 +184,10 @@ export interface AppEditorProps { // which does not propagate through these runes-mode components. onRestore?: (restoredApp: any) => void // Fired after a successful deploy, which keeps this editor open: `version` is what - // the deploy wrote, for the next draft's fork base, and `head` what is deployed now. - // The two differ when another deploy landed beside this one. - onDeploy?: (e: { version?: number; head?: number }) => void + // the deploy wrote, for the next draft's fork base, and `head` what is deployed now, + // with its author and time. `version` and `head` differ when another deploy landed + // beside this one, which is when the out-of-date prompt has something to say. + onDeploy?: (e: { version?: number; head?: number; headBy?: string; headAt?: string }) => void } export type App = { diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 9d8f288bbe..d178d97ada 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -105,7 +105,13 @@ /** Fired after a successful deploy; the session preview reloads on it and the route * re-pins the draft's fork base. `version` is what this deploy wrote and `head` * what is deployed now: the two differ when another deploy landed beside it. */ - onDeploy?: (e: { path: string; version?: number; head?: number }) => void + onDeploy?: (e: { + path: string + version?: number + head?: number + headBy?: string + headAt?: string + }) => void /** Initial collapsed state for the file/runnable sidebar. The user's * toggled preference is persisted under `sidebarStorageKey`; this prop * only seeds the very first open. */ diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 7cc0415539..5303021581 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -155,8 +155,15 @@ autosavePath?: string // Fired after a successful deploy; lets the session preview reload. `version` is // what this deploy wrote, for the next draft's fork base; `head` is what is - // deployed now, and the two differ when another deploy landed beside this one. - onDeploy?: (e: { path: string; version?: number; head?: number }) => void + // deployed now, with its author and time, and the two differ when another deploy + // landed beside this one. + onDeploy?: (e: { + path: string + version?: number + head?: number + headBy?: string + headAt?: string + }) => void /** Surfaces the user-typed path (`newEditedPath`) up to the route * when (and only when) it differs from the deployed/seeded * `savedApp.path`. The route writes it into the autosaved raw-app @@ -575,7 +582,13 @@ if (appPath !== npath) { onSavedNewAppPath?.(npath) } - onDeploy?.({ path: npath, version: claimed, head: version }) + onDeploy?.({ + path: npath, + version: claimed, + head: version, + headBy: appHistory[0]?.created_by, + headAt: appHistory[0]?.created_at + }) } async function setPublishState(message?: string) { @@ -899,7 +912,13 @@ itemKind="raw_app" path={indicatorPath} draftOnly={newApp} - {onResetToDeployed} + onResetToDeployed={onResetToDeployed && + (async () => { + // Back on the deployed version, so whatever the last deploy could not claim + // no longer describes this editor. + baseUnknown = false + await onResetToDeployed() + })} {loadedFromDraft} {othersDraftsCount} {onOpenOthersDrafts} diff --git a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte index 8e95a0842c..1006fdb780 100644 --- a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte @@ -495,13 +495,16 @@ {loadedFromDraft} othersDraftsCount={otherDraftsUsers.length} onOpenOthersDrafts={() => (othersModalOpen = true)} - onDeploy={({ version, head }) => { - // The editor stays open across a deploy, so the pair the out-of-date prompt - // reads has to move with it: the base is what the deploy could claim it - // wrote (unknown when another landed beside it), the head what is deployed. + onDeploy={({ version, head, headBy, headAt }) => { + // The editor stays open across a deploy, so what the out-of-date prompt reads + // has to move with it: the base is what the deploy could claim it wrote + // (unknown when another landed beside it), and the head is what is deployed, + // named by whoever deployed it rather than by the page load's author. draftBaseVersion = version != null ? String(version) : undefined if (head != null) { deployedHeadVersion = String(head) + deployedBy = headBy + deployedAt = headAt } }} /> diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte index c63d04479d..6667655406 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte @@ -655,7 +655,7 @@ } } : undefined} - onDeploy={({ version, head }) => { + onDeploy={({ version, head, headBy, headAt }) => { // The version this deploy wrote is the base the next autosave carries; the // head is what is deployed now. They differ when another deploy landed // beside this one, and the next draft is then behind from the start. A @@ -665,6 +665,9 @@ draftBaseVersion = version != null ? String(version) : undefined if (head != null) { deployedHeadVersion = String(head) + // Named by whoever deployed the head, not by the page load's author. + deployedBy = headBy + deployedAt = headAt } }} onResetToDeployed={reloadDeployed}