diff --git a/backend/migrations/20261015160053_account_created_by_follows_user.down.sql b/backend/migrations/20261015160053_account_created_by_follows_user.down.sql index 218946e997..dde8dc2a9c 100644 --- a/backend/migrations/20261015160053_account_created_by_follows_user.down.sql +++ b/backend/migrations/20261015160053_account_created_by_follows_user.down.sql @@ -1,2 +1,4 @@ +DROP TRIGGER IF EXISTS account_created_by_follows_instance_user_trigger ON password; +DROP FUNCTION IF EXISTS account_created_by_follows_instance_user(); DROP TRIGGER IF EXISTS account_created_by_follows_user_trigger ON usr; DROP FUNCTION IF EXISTS account_created_by_follows_user(); diff --git a/backend/migrations/20261015160053_account_created_by_follows_user.up.sql b/backend/migrations/20261015160053_account_created_by_follows_user.up.sql index fc8987d1c0..ad7bd0a94e 100644 --- a/backend/migrations/20261015160053_account_created_by_follows_user.up.sql +++ b/backend/migrations/20261015160053_account_created_by_follows_user.up.sql @@ -19,3 +19,28 @@ CREATE TRIGGER account_created_by_follows_user_trigger AFTER UPDATE OF username OR DELETE ON usr FOR EACH ROW EXECUTE FUNCTION account_created_by_follows_user(); + +-- A superadmin can connect an account in a workspace they are not a member of, which records +-- their instance username with no `usr` row for the trigger above to follow. Follow +-- `password.username` there instead, skipping workspaces where they are a member (handled above) +-- and those where a member holds that username (the name is theirs). +CREATE OR REPLACE FUNCTION account_created_by_follows_instance_user() RETURNS trigger AS $$ +BEGIN + IF TG_OP = 'UPDATE' AND NEW.username IS NOT DISTINCT FROM OLD.username THEN + RETURN NULL; + END IF; + UPDATE account a SET created_by = CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE NEW.username END + WHERE a.created_by = OLD.username + AND NOT EXISTS ( + SELECT 1 FROM usr u + WHERE u.workspace_id = a.workspace_id + AND (u.email = OLD.email OR u.username = OLD.username) + ); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER account_created_by_follows_instance_user_trigger +AFTER UPDATE OF username OR DELETE ON password +FOR EACH ROW +EXECUTE FUNCTION account_created_by_follows_instance_user(); diff --git a/backend/windmill-api-integration-tests/tests/variables.rs b/backend/windmill-api-integration-tests/tests/variables.rs index 14e0fcda05..887672e37c 100644 --- a/backend/windmill-api-integration-tests/tests/variables.rs +++ b/backend/windmill-api-integration-tests/tests/variables.rs @@ -482,5 +482,27 @@ async fn test_oauth_account_creator_follows_user(db: Pool) -> anyhow:: .await?; assert_eq!(creator().await?, None); + // A superadmin who is not a member of the workspace is recorded by their instance username. + sqlx::query( + "INSERT INTO password (email, password_hash, login_type, super_admin, verified, username) + VALUES ('sa@windmill.dev', 'x', 'password', true, true, 'sa')", + ) + .execute(&db) + .await?; + sqlx::query("UPDATE account SET created_by = 'sa' WHERE id = $1") + .bind(account) + .execute(&db) + .await?; + + sqlx::query("UPDATE password SET username = 'sa2' WHERE email = 'sa@windmill.dev'") + .execute(&db) + .await?; + assert_eq!(creator().await?.as_deref(), Some("sa2")); + + sqlx::query("DELETE FROM password WHERE email = 'sa@windmill.dev'") + .execute(&db) + .await?; + assert_eq!(creator().await?, None); + Ok(()) }