docs: state the RLS and restamp constraints once each

The RLS envelope was argued at three sites in drafts.rs; it now sits only on
`resolve_moved_to_in`, whose signature is what a caller would break. The
restamp scoping was copy-pasted at all three deploy call sites while already
documented in full on `move_drafts_for_path`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-09-09 12:51:59 +02:00
co-authored by Claude Opus 5
parent 3528054801
commit f79812cc44
4 changed files with 8 additions and 23 deletions
+1 -4
View File
@@ -1385,10 +1385,7 @@ async fn update_flow(
if is_new_path {
// Everything left at the old path is a draft this deploy didn't consume
// — teammates' rows, and the deployer's own when the caller asked us to
// keep it. Carry them rather than strand them. Only the deployer's own
// row is restamped: this runs on any path-changing deploy, content edits
// included, and a teammate whose draft claimed the new head would lose
// their stale-draft warning.
// keep it. Carry them rather than strand them.
let outcome = windmill_common::user_drafts::move_drafts_for_path(
&mut tx,
&w_id,
+1 -4
View File
@@ -2137,10 +2137,7 @@ async fn create_script_internal<'c>(
if p_path != &ns.path {
// Everything left at the old path is a draft this deploy didn't
// consume — teammates' rows, and the deployer's own when the caller
// asked us to keep it. Carry them rather than strand them. Only the
// deployer's own row is restamped: this runs on any path-changing
// deploy, content edits included, and a teammate whose draft claimed
// the new head would lose their stale-draft warning.
// asked us to keep it. Carry them rather than strand them.
let outcome = windmill_common::user_drafts::move_drafts_for_path(
&mut tx,
&w_id,
+1 -4
View File
@@ -3450,10 +3450,7 @@ async fn update_app_internal<'a>(
if npath != path {
// Everything left at the old path is a draft this deploy didn't consume
// — teammates' rows, and the deployer's own when the caller asked us to
// keep it. Carry them rather than strand them. Only the deployer's own
// row is restamped: this runs on any path-changing deploy, content edits
// included, and a teammate whose draft claimed the new head would lose
// their stale-draft warning.
// keep it. Carry them rather than strand them.
let outcome = windmill_common::user_drafts::move_drafts_for_path(
&mut tx,
&w_id,
+5 -11
View File
@@ -415,9 +415,7 @@ fn draft_lineage(kind: UserDraftItemKind, value: &str) -> Option<DraftBaseVersio
/// caller can't see where it went.
///
/// Reads under RLS so the answer can never reveal an item the caller has no
/// access to. `resolve_moved_to_in` makes no such guarantee on its own — it
/// inherits whatever scoping the transaction it is handed carries, which is why
/// every caller passes an RLS-scoped one.
/// access to.
async fn resolve_moved_to(
authed: &ApiAuthed,
user_db: &UserDB,
@@ -429,14 +427,10 @@ async fn resolve_moved_to(
if draft_lineage(kind, value).is_none() {
return Ok(None);
}
// RLS is not optional on THIS entry point: it runs before the write gate, so
// the envelope is what stops the answer naming an item the caller cannot see.
// Cost, stated honestly: `UserDB::begin` is not a bare BEGIN — it issues
// BEGIN, `set_session_context`, and `SET LOCAL search_path` when `PG_SCHEMA`
// is set, and the commit is another round-trip. So 3-4 round-trips wrap one
// indexed existence check, and the lineage query runs only once that check
// says the item is gone. `draft_lineage` above is what keeps all of it off
// the drafts that have nothing to follow.
// `UserDB::begin` is not a bare BEGIN — it also issues `set_session_context`
// and, under `PG_SCHEMA`, `SET LOCAL search_path`, so this wraps one indexed
// existence check in 3-4 round-trips. The `draft_lineage` check above is what
// keeps that off every draft with nothing to follow.
let mut tx = user_db.clone().begin(authed).await?;
let moved = resolve_moved_to_in(&mut tx, &authed.username, w_id, kind, path, value).await;
tx.commit().await?;