fix: keep an oauth account's creator in step with the user's username

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-10-19 12:56:26 +02:00
co-authored by Claude Opus 5.5
parent 6e93305e8c
commit 9303ddd50f
3 changed files with 53 additions and 0 deletions
@@ -0,0 +1,2 @@
DROP TRIGGER IF EXISTS account_created_by_follows_user_trigger ON usr;
DROP FUNCTION IF EXISTS account_created_by_follows_user();
@@ -0,0 +1,21 @@
-- `account.created_by` grants the right to link the account to a variable, so it must name the
-- same person for as long as it is set: follow the user through a rename, and clear it when the
-- user leaves the workspace, so a later member given the same username does not inherit it.
-- A trigger rather than application code, because users are removed from several code paths.
CREATE OR REPLACE FUNCTION account_created_by_follows_user() RETURNS trigger AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
UPDATE account SET created_by = NULL
WHERE workspace_id = OLD.workspace_id AND created_by = OLD.username;
ELSIF NEW.username IS DISTINCT FROM OLD.username THEN
UPDATE account SET created_by = NEW.username
WHERE workspace_id = OLD.workspace_id AND created_by = OLD.username;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
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();
@@ -454,3 +454,33 @@ async fn test_oauth_account_link_requires_ownership(db: Pool<Postgres>) -> anyho
Ok(())
}
/// The creator recorded on an OAuth account follows the user through a rename and is cleared
/// when they leave, so a later member reusing the username cannot link the account.
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
async fn test_oauth_account_creator_follows_user(db: Pool<Postgres>) -> anyhow::Result<()> {
let account: i32 = sqlx::query_scalar(
"INSERT INTO account (workspace_id, client, expires_at, refresh_token, created_by)
VALUES ('test-workspace', 'github', now(), 'r', 'alice') RETURNING id",
)
.fetch_one(&db)
.await?;
let creator = || async {
sqlx::query_scalar::<_, Option<String>>("SELECT created_by FROM account WHERE id = $1")
.bind(account)
.fetch_one(&db)
.await
};
sqlx::query("UPDATE usr SET username = 'alice2' WHERE username = 'alice'")
.execute(&db)
.await?;
assert_eq!(creator().await?.as_deref(), Some("alice2"));
sqlx::query("DELETE FROM usr WHERE username = 'alice2'")
.execute(&db)
.await?;
assert_eq!(creator().await?, None);
Ok(())
}