diff --git a/backend/windmill-api/src/drafts.rs b/backend/windmill-api/src/drafts.rs index 5b47a06ec9..9d4976489b 100644 --- a/backend/windmill-api/src/drafts.rs +++ b/backend/windmill-api/src/drafts.rs @@ -276,7 +276,17 @@ async fn update_draft( ) -> Result> { let email = &authed.email; let path = path.to_path(); - require_can_write_path(&authed, &db, &user_db, &w_id, kind, path).await?; + // Saving a draft requires write permission on the underlying path. Deleting + // (discarding) one's OWN draft does not: the email-scoped row belongs to the + // authed user, so they can always discard it even after losing write access + // to the underlying item (e.g. a draft-only item whose folder perms changed). + // The DELETE below is scoped to `email = authed.email`, so it can only ever + // touch the caller's own row. Legacy (NULL-email) rows aren't owned by anyone + // — they keep the write gate. + let is_own_discard = req.value.is_none() && !req.legacy; + if !is_own_discard { + require_can_write_path(&authed, &db, &user_db, &w_id, kind, path).await?; + } let applied_at = if let Some(value) = &req.value { // Secret variable values must never sit in `draft.value` in plaintext diff --git a/frontend/src/lib/components/CompareDrafts.svelte b/frontend/src/lib/components/CompareDrafts.svelte index cc450a51fb..aca63e4b32 100644 --- a/frontend/src/lib/components/CompareDrafts.svelte +++ b/frontend/src/lib/components/CompareDrafts.svelte @@ -167,14 +167,23 @@ ) } - // Why a row can't be deployed/discarded (drives the disabled-checkbox tooltip - // and the Discard button's title). `undefined` ⇒ actionable. + // Why a row can't be deployed (drives the disabled-checkbox tooltip). + // `undefined` ⇒ actionable. function blockedReason(item: Row): string | undefined { if (!item.mine) return 'This draft belongs to another user' if (!item.can_write) return "You don't have write permission on this path" return undefined } + // Why a row can't be discarded (drives the Discard button's title). + // Discarding only removes the caller's own draft row, which they always own, + // so — unlike deploy — it never requires write permission on the path. The + // only block is someone else's draft (view-only in the "all drafts" view). + function discardBlockedReason(item: Row): string | undefined { + if (!item.mine) return 'This draft belongs to another user' + return undefined + } + // The Draft Items list only carries the *deployed* summary, so the draft's // (new) display name isn't known yet. Fetch each item's draft blob once and // cache both names — mirrors CompareWorkspaces' fetchSummaries (eager on load, @@ -628,7 +637,7 @@ {/if} {:else} - {@const discardBlock = blockedReason(draftItem)} + {@const discardBlock = discardBlockedReason(draftItem)} diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index 69f1e9bb33..f016fafd5c 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -211,7 +211,10 @@ } }, type: 'delete', - disabled: !canEdit, + // A draft-only row is always the authed user's own draft (the + // list endpoint only surfaces own/legacy draft-only rows), so + // discarding it never requires write permission on the path. + disabled: !showEditButton, hide: $userStore?.operator }, { diff --git a/frontend/src/lib/components/common/table/FlowRow.svelte b/frontend/src/lib/components/common/table/FlowRow.svelte index 2f31556d3a..89a08b412b 100644 --- a/frontend/src/lib/components/common/table/FlowRow.svelte +++ b/frontend/src/lib/components/common/table/FlowRow.svelte @@ -230,7 +230,10 @@ } }, type: 'delete', - disabled: !owner, + // A draft-only row is always the authed user's own draft (the + // list endpoint only surfaces own/legacy draft-only rows), so + // discarding it never requires write permission on the path. + disabled: !showEditButton, hide: $userStore?.operator } ] diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index fb31a6add7..1d87d4c8d5 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -296,7 +296,10 @@ } }, type: dlt, - disabled: !canEdit + // A draft-only row is always the authed user's own draft (the + // list endpoint only surfaces own/legacy draft-only rows), so + // discarding it never requires write permission on the path. + disabled: !showEditButton } ] }