From 4cba79b7f00c689ac6d5dd52bf7f22b0248232aa Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Thu, 17 Sep 2026 18:41:11 +0200 Subject: [PATCH] fix(datatables): authenticate instance database setup before writing its status, and keep a fork reservation across it Co-Authored-By: Claude Opus 5 (1M context) --- backend/windmill-api-settings/src/lib.rs | 25 ++++++++++++++++++++++-- backend/windmill-api/openapi.yaml | 3 +++ backend/windmill-common/src/lib.rs | 4 ++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index 5d01baafbd..096bf4e9b0 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -1647,6 +1647,8 @@ struct CustomInstanceDb { tag: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] used_by_workspaces: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace_id: Option, } #[derive(Deserialize, Debug, Serialize, Default)] @@ -1754,12 +1756,31 @@ async fn setup_custom_instance_pg_database( Path(dbname): Path, Json(body): Json, ) -> JsonResult { + // Before anything is recorded: the status written below replaces the registry entry, and with it + // the workspace a fork copy is reserved for. + require_super_admin(&db, &authed).await?; + // A re-run keeps the fork reservation: without it, the workspace the copy was made for could no + // longer import into it or finish its fork. + let workspace_id = sqlx::query_scalar::<_, Option>( + "SELECT value->'databases'->$1->>'workspace_id' FROM global_settings + WHERE name = 'custom_instance_pg_databases'", + ) + .bind(&dbname) + .fetch_optional(&db) + .await? + .flatten(); let mut logs = CustomInstanceDbLogs::default(); let result = setup_custom_instance_pg_database_inner(authed, &db, &dbname, &mut logs).await; let success = result.is_ok(); let error = result.err().map(|e| e.to_string()); - let status = - CustomInstanceDb { logs, success, error, tag: body.tag, used_by_workspaces: vec![] }; + let status = CustomInstanceDb { + logs, + success, + error, + tag: body.tag, + used_by_workspaces: vec![], + workspace_id, + }; let status_json = serde_json::to_value(&status).map_err(to_anyhow)?; // Save that the database was setup successfully sqlx::query!( diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index a3292ecfbc..dde6d53d97 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -33954,6 +33954,9 @@ components: items: type: string description: Workspaces that reference this database via a ducklake catalog or datatable database with resource_type 'instance'. Computed at request time, not persisted. + workspace_id: + type: string + description: The workspace a member created this database for as a fork copy. Only that workspace can import into it or point a fork at it. NewSqsTrigger: type: object diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index ab3a1dc46b..db52194730 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -1653,6 +1653,10 @@ pub async fn create_custom_instance_database( /// `dbname`, unless `w_id` created it for that ([`create_custom_instance_database`]) and nothing uses /// it yet. The `wm_fork_` prefix is no authorization: every instance database answers to the same /// `custom_instance_user`, so a name is all it takes to reach another workspace's copy. +/// +/// Authorization: reads the global registry and every workspace's settings, and names other +/// workspaces in its refusal. Callers MUST have authorized `w_id` for the caller first — a member +/// of it forking or importing there — and MUST NOT call it on a workspace the caller is not in. pub async fn ensure_fork_database_available_to( db: &DB, dbname: &str,