From 10fee4bdc536b6f9b2de0389ebd387e2323954c2 Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Thu, 23 Jul 2026 14:22:03 +0530 Subject: [PATCH] feat(db): block deleting an org that still has service users 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) --- ...rict_org_delete_with_serviceusers.down.sql | 2 ++ ...strict_org_delete_with_serviceusers.up.sql | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.down.sql create mode 100644 internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.up.sql diff --git a/internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.down.sql b/internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.down.sql new file mode 100644 index 000000000..07f0469fb --- /dev/null +++ b/internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.down.sql @@ -0,0 +1,2 @@ +DROP TRIGGER IF EXISTS trg_serviceusers_block_org_delete ON organizations; +DROP FUNCTION IF EXISTS serviceusers_block_org_delete(); diff --git a/internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.up.sql b/internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.up.sql new file mode 100644 index 000000000..763be75bc --- /dev/null +++ b/internal/store/postgres/migrations/20260723120000_restrict_org_delete_with_serviceusers.up.sql @@ -0,0 +1,26 @@ +-- Block deleting an organization that still has service users. + +-- A plain foreign key to organizations(id) cannot be used here. The bootstrap +-- superuser lives under the virtual platform org (00000000-0000-0000-0000-000000000000), +-- which has no row in organizations by design. A BEFORE DELETE trigger only +-- fires when a real org is deleted, so the virtual platform org and its +-- bootstrap service account are never in scope. +-- +-- Existing orphaned rows (if any) are left untouched; they are cleaned up out +-- of band, not by this migration. +CREATE OR REPLACE FUNCTION serviceusers_block_org_delete() + RETURNS trigger AS $$ +BEGIN + IF EXISTS (SELECT 1 FROM serviceusers WHERE org_id = OLD.id) THEN + RAISE EXCEPTION 'cannot delete organization %: service users still reference it', OLD.id + USING ERRCODE = 'foreign_key_violation'; + END IF; + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_serviceusers_block_org_delete ON organizations; +CREATE TRIGGER trg_serviceusers_block_org_delete + BEFORE DELETE ON organizations + FOR EACH ROW + EXECUTE FUNCTION serviceusers_block_org_delete();