From a230f1bfb5a065ceec6944e47b2763908deab9d2 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 5 Mar 2026 09:25:55 -0600 Subject: [PATCH] fix: prevent silent data loss from cascade deletes on agent tokens Changed foreign key constraints from CASCADE to RESTRICT for: - agent_tokens.organization_id - agent_assignments.agent_token_id **Problem:** Deleting an organization would cascade delete all agent tokens, which would then cascade delete all agent assignments, causing silent data loss without any audit trail. **Solution:** Use RESTRICT instead of CASCADE - this prevents deletion if there are dependent records, forcing explicit cleanup and preventing accidental data loss. **Impact:** - Organizations cannot be deleted if they have agent tokens - Agent tokens cannot be deleted if they have active assignments - User must explicitly clean up dependencies first This provides better data integrity and prevents accidental data loss. Fixes Medium Bug #15 from reliability audit. --- ...260305152524_fix_agent_cascade_deletes.exs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 priv/repo/migrations/20260305152524_fix_agent_cascade_deletes.exs diff --git a/priv/repo/migrations/20260305152524_fix_agent_cascade_deletes.exs b/priv/repo/migrations/20260305152524_fix_agent_cascade_deletes.exs new file mode 100644 index 00000000..d24248fd --- /dev/null +++ b/priv/repo/migrations/20260305152524_fix_agent_cascade_deletes.exs @@ -0,0 +1,64 @@ +defmodule Towerops.Repo.Migrations.FixAgentCascadeDeletes do + use Ecto.Migration + + def up do + # Drop and recreate the foreign key constraint on agent_tokens.organization_id + # Change from :delete_all to :restrict to prevent accidental data loss + execute """ + ALTER TABLE agent_tokens + DROP CONSTRAINT agent_tokens_organization_id_fkey + """ + + execute """ + ALTER TABLE agent_tokens + ADD CONSTRAINT agent_tokens_organization_id_fkey + FOREIGN KEY (organization_id) + REFERENCES organizations(id) + ON DELETE RESTRICT + """ + + # Drop and recreate the foreign key constraint on agent_assignments.agent_token_id + # Change from :delete_all to :restrict to prevent orphaned assignments + execute """ + ALTER TABLE agent_assignments + DROP CONSTRAINT agent_assignments_agent_token_id_fkey + """ + + execute """ + ALTER TABLE agent_assignments + ADD CONSTRAINT agent_assignments_agent_token_id_fkey + FOREIGN KEY (agent_token_id) + REFERENCES agent_tokens(id) + ON DELETE RESTRICT + """ + end + + def down do + # Restore original cascade delete behavior + execute """ + ALTER TABLE agent_assignments + DROP CONSTRAINT agent_assignments_agent_token_id_fkey + """ + + execute """ + ALTER TABLE agent_assignments + ADD CONSTRAINT agent_assignments_agent_token_id_fkey + FOREIGN KEY (agent_token_id) + REFERENCES agent_tokens(id) + ON DELETE CASCADE + """ + + execute """ + ALTER TABLE agent_tokens + DROP CONSTRAINT agent_tokens_organization_id_fkey + """ + + execute """ + ALTER TABLE agent_tokens + ADD CONSTRAINT agent_tokens_organization_id_fkey + FOREIGN KEY (organization_id) + REFERENCES organizations(id) + ON DELETE CASCADE + """ + end +end