clear orphaned usr_to_group rows on service account creation (#10660)

* fix: clear orphaned usr_to_group rows on service account creation

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: pin service account creation over orphaned usr_to_group rows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 60c20e686cead73ff075512b15c6e2d6232beca6

This commit updates the EE repository reference after PR #723 was merged in windmill-ee-private.

Previous ee-repo-ref: 5c2c553f960abcd7988fdac8830dd36c066160ad

New ee-repo-ref: 60c20e686cead73ff075512b15c6e2d6232beca6

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
Ruben Fiszel
2026-08-12 14:53:47 +00:00
committed by GitHub
co-authored by Claude Opus 5 windmill-internal-app[bot]
parent 83bdff89d5
commit 9cd2bcea25
3 changed files with 106 additions and 17 deletions
@@ -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"
}
+1 -1
View File
@@ -1 +1 @@
88568d11162ffa11723e7955e613224bab4f0568
60c20e686cead73ff075512b15c6e2d6232beca6
@@ -1007,3 +1007,108 @@ async fn test_dbt_warehouses(db: Pool<Postgres>) -> 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<Postgres>,
) -> 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<String> = 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<String> = 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(())
}