feat(db): block deleting an org that still has service users#1796
Conversation
Adds a BEFORE DELETE trigger on organizations that fails if any serviceusers row still points at the org. This is a DB-level safety net: the app deleter already removes service users before the org, so normal deletes are unaffected. If that cleanup ever regresses, or an org is deleted out of band, the delete now fails loudly instead of leaving service users that keep authenticating. A plain foreign key can't be used. The bootstrap superuser lives under the virtual platform org (uuid.Nil), which has no row in organizations by design, so an FK would reject that row. A trigger only fires on real org deletes, so the platform org and its bootstrap account stay out of scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a PostgreSQL trigger preventing deletion of organizations referenced by service users, with a down migration that removes the trigger and function. ChangesOrganization delete protection
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage Report for CI Build 29992856414Coverage remained the same at 46.629%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a015a5f-b611-4bca-841e-72f29dd3932c
📒 Files selected for processing (2)
internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.down.sqlinternal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.up.sql
|
part of #1585 |
What
Adds a
BEFORE DELETEtrigger onorganizations. If anyserviceusersrow still points at the org, the delete fails.Why
Deleting an org used to leave its service users behind, with an
org_idpointing at a row that no longer exists. Those leftover accounts kept authenticating. The app deleter now removes service users before the org, but the database had no fallback. This trigger is that fallback: if the app-level cleanup ever breaks, or a row is removed out of band, the org delete fails loudly instead of silently orphaning service users.Normal deletes are unaffected — the deleter clears the service users first, so by the time the org row is deleted, the trigger has nothing to block.
Why a trigger and not a foreign key
The obvious fix is a foreign key on
serviceusers.org_idwithON DELETE RESTRICT. That does not work here.The bootstrap superuser service account lives under the virtual platform org (
00000000-0000-0000-0000-000000000000). That org id is never inserted into theorganizationstable by design. A foreign key checks every row, so it would reject the bootstrap account and also fail on every server boot, which re-creates that account.A trigger only runs when a real org is deleted. The platform org is never deleted, so the bootstrap account is never in scope. Same "fail loud" behaviour, no conflict with the special org id.
Testing
Verified locally against a running server:
DELETE FROM organizationswhile a service user still exists → blocked by the trigger.DeleteOrganization→ succeeds, and the service user rows are gone.🤖 Generated with Claude Code