diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 786f21011c..3d0e542669 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -e689ab9cf9cd2ed9b3ff0e4b0104d683df2bac1c \ No newline at end of file +a97b5a5982d67e977dde6222380903f019da39c2 \ No newline at end of file diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index de170b48f4..386923a8ee 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -1678,6 +1678,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)] @@ -1914,12 +1916,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 371785c404..13cf8e5e52 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -34090,6 +34090,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/external_instance_pg.rs b/backend/windmill-common/src/external_instance_pg.rs index a4e945a60d..3d8534224f 100644 --- a/backend/windmill-common/src/external_instance_pg.rs +++ b/backend/windmill-common/src/external_instance_pg.rs @@ -41,6 +41,21 @@ pub struct ExternalInstancePgState { pub databases: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub last_setup: Option, + /// The cluster ([`external_instance_pg_address`]) the last successful setup converged. Databases + /// are only created on a cluster setup succeeded on: the passwords above exist as soon as setup + /// first runs, whether or not the cluster accepted them. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub set_up_for: Option, +} + +/// What identifies the cluster a configuration points at. Other fields (admin login, sslmode) can +/// change without it becoming another cluster. +pub fn external_instance_pg_address(config: &ExternalInstancePg) -> String { + format!( + "{}:{}", + config.host.trim().to_lowercase(), + config.port.unwrap_or(5432) + ) } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -125,6 +140,10 @@ pub async fn external_instance_pg_status(db: &DB) -> Result Result> { Ok(read_external_instance_pg_state(db).await?.databases) } @@ -372,8 +391,7 @@ async fn ensure_external_instance_pg_not_repointed( let Ok(desired) = serde_json::from_value::(value.clone()) else { return Ok(()); }; - let address = |c: &ExternalInstancePg| (c.host.trim().to_lowercase(), c.port.unwrap_or(5432)); - if address(¤t) == address(&desired) { + if external_instance_pg_address(¤t) == external_instance_pg_address(&desired) { return Ok(()); } let state = read_external_instance_pg_state(db).await?; diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 4afddfcbc1..fbe56b6a5b 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -1682,6 +1682,10 @@ pub fn system_ca_bundle() -> Option { /// its external instance counterpart) and nothing uses it yet. The `wm_fork_` prefix is no /// authorization: every database of a cluster answers to the same `custom_instance_user`, so a name /// is all it takes to reach another workspace's copy. +/// +/// Authorization: reads the global registries 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, kind: workspaces::DataTableCatalogResourceType,