Files
coder/coderd/database/migrations/000490_trigger_delete_user_secrets.up.sql
T
Zach b221632615 fix: wipe user secrets when user is soft-deleted (#24985)
Extend the delete_deleted_user_resources() trigger so that secrets
belonging to a soft-deleted user are removed in the same transaction as
the existing api_keys and user_links cleanup.

user_secrets.user_id has ON DELETE CASCADE, but Coder soft-deletes users
by flipping users.deleted rather than removing the row, so the foreign key
cascade never fires and secrets would otherwise survive deletion.

Assisted by Coder Agents.
2026-05-11 09:07:30 -06:00

65 lines
1.8 KiB
PL/PgSQL

-- Extend the soft-delete cleanup trigger to also wipe user_secrets.
-- user_secrets.user_id has ON DELETE CASCADE, but Coder soft-deletes
-- users by flipping users.deleted instead of removing the row, so the
-- FK cascade never fires and secrets would otherwise survive deletion.
--
-- Backfill any rows that belonged to already-soft-deleted users before
-- replacing the function.
DELETE FROM
user_secrets
WHERE
user_id
IN (
SELECT id FROM users WHERE deleted
);
CREATE OR REPLACE FUNCTION delete_deleted_user_resources() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
BEGIN
IF (NEW.deleted) THEN
-- Remove their api_keys
DELETE FROM api_keys
WHERE user_id = OLD.id;
-- Remove their user_links
-- Their login_type is preserved in the users table.
-- Matching this user back to the link can still be done by their
-- email if the account is undeleted. Although that is not a guarantee.
DELETE FROM user_links
WHERE user_id = OLD.id;
-- Remove their user_secrets.
-- user_secrets.user_id has ON DELETE CASCADE, but soft-delete
-- does not remove the users row so the FK cascade never fires.
DELETE FROM user_secrets
WHERE user_id = OLD.id;
END IF;
RETURN NEW;
END;
$$;
-- Prevent adding new user_secrets for soft-deleted users.
-- Closes the window between an in-flight CreateUserSecret request
-- and the soft-delete UPDATE committing.
CREATE FUNCTION insert_user_secret_fail_if_user_deleted() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
BEGIN
IF (NEW.user_id IS NOT NULL) THEN
IF (SELECT deleted FROM users WHERE id = NEW.user_id LIMIT 1) THEN
RAISE EXCEPTION 'Cannot create user_secret for deleted user';
END IF;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trigger_upsert_user_secrets
BEFORE INSERT OR UPDATE ON user_secrets
FOR EACH ROW
EXECUTE PROCEDURE insert_user_secret_fail_if_user_deleted();