diff --git a/backend/.sqlx/query-add01e9e31d64e88b84c9505fe3de553031e581b1bb173413a9a3e3eb0817b43.json b/backend/.sqlx/query-add01e9e31d64e88b84c9505fe3de553031e581b1bb173413a9a3e3eb0817b43.json deleted file mode 100644 index 2c00759a63..0000000000 --- a/backend/.sqlx/query-add01e9e31d64e88b84c9505fe3de553031e581b1bb173413a9a3e3eb0817b43.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "add01e9e31d64e88b84c9505fe3de553031e581b1bb173413a9a3e3eb0817b43" -} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index d94d87dff2..436566218b 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -88568d11162ffa11723e7955e613224bab4f0568 +60c20e686cead73ff075512b15c6e2d6232beca6 diff --git a/backend/windmill-api-integration-tests/tests/workspaces.rs b/backend/windmill-api-integration-tests/tests/workspaces.rs index e9eeb88996..65fe4241cc 100644 --- a/backend/windmill-api-integration-tests/tests/workspaces.rs +++ b/backend/windmill-api-integration-tests/tests/workspaces.rs @@ -1007,3 +1007,108 @@ async fn test_dbt_warehouses(db: Pool) -> anyhow::Result<()> { Ok(()) } + +/// A service account created over an orphaned `usr_to_group` row must start with +/// exactly the memberships that were asked for. +/// +/// `usr_to_group` has no FK to `usr`, so `leave_workspace` leaves group rows behind +/// for a deleted username. Recreating that username used to fail outright on the +/// duplicate `all` row; tolerating the duplicate alone would instead have handed the +/// new account every stale membership, including privileged ones. +/// +/// Gated on `private` because `create_service_account` is EE-only — the OSS shim +/// (`windmill-api-workspaces/src/workspaces_oss.rs`) rejects the request outright. +#[cfg(feature = "private")] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_create_service_account_drops_orphaned_group_memberships( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + + sqlx::query( + "INSERT INTO group_ (workspace_id, name, summary) VALUES + ('test-workspace', 'wm_deployers', 'deployers'), + ('test-workspace', 'secrets', 'privileged')", + ) + .execute(&db) + .await?; + + // Stale rows from a prior user of this username: no matching `usr` row exists. + sqlx::query( + "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES + ('test-workspace', 'svc_acct', 'all'), + ('test-workspace', 'svc_acct', 'wm_deployers'), + ('test-workspace', 'svc_acct', 'secrets')", + ) + .execute(&db) + .await?; + + // Same username, different workspace, and very much alive — must not be touched. + sqlx::query("INSERT INTO workspace (id, name, owner) VALUES ('other-workspace', 'other', 'svc_acct')") + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO group_ (workspace_id, name, summary) VALUES + ('other-workspace', 'all', 'All users'), + ('other-workspace', 'secrets', 'privileged')", + ) + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO usr (workspace_id, email, username, is_admin, role) + VALUES ('other-workspace', 'other@windmill.dev', 'svc_acct', false, 'User')", + ) + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES + ('other-workspace', 'svc_acct', 'all'), + ('other-workspace', 'svc_acct', 'secrets')", + ) + .execute(&db) + .await?; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/test-workspace/workspaces"); + + let resp = authed(client().post(format!("{base}/create_service_account"))) + .json(&json!({"username": "svc_acct", "is_admin": false, "operator": true, "add_to_deployers": false})) + .send() + .await + .unwrap(); + assert_eq!( + resp.status(), + 201, + "creation over an orphaned row failed: {}", + resp.text().await? + ); + + let groups: Vec = sqlx::query_scalar( + "SELECT group_ FROM usr_to_group WHERE workspace_id = $1 AND usr = $2 ORDER BY group_", + ) + .bind("test-workspace") + .bind("svc_acct") + .fetch_all(&db) + .await?; + assert_eq!( + groups, + vec!["all".to_string()], + "service account inherited stale memberships" + ); + + let other: Vec = sqlx::query_scalar( + "SELECT group_ FROM usr_to_group WHERE workspace_id = $1 AND usr = $2 ORDER BY group_", + ) + .bind("other-workspace") + .bind("svc_acct") + .fetch_all(&db) + .await?; + assert_eq!( + other, + vec!["all".to_string(), "secrets".to_string()], + "cleanup escaped the workspace it was scoped to" + ); + + Ok(()) +}