Security fixes: P0-P2 complete (22 fixes)
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""Cross-tenant referential integrity: composite FKs on (tenant_id, contact_id).
|
||||
|
||||
Revision ID: 0036_cross_tenant_fk
|
||||
Revises: 0035_comm_search_index
|
||||
Create Date: 2026-07-25
|
||||
|
||||
Changes:
|
||||
1. Add UNIQUE (tenant_id, id) on contacts — prerequisite for composite FK.
|
||||
2. Replace contactpersons.contact_id FK with composite (tenant_id, contact_id)
|
||||
→ contacts(tenant_id, id).
|
||||
3. Replace contact_merge_history.source_contact_id FK with composite
|
||||
(tenant_id, source_contact_id) → contacts(tenant_id, id).
|
||||
4. Replace contact_merge_history.target_contact_id FK with composite
|
||||
(tenant_id, target_contact_id) → contacts(tenant_id, id).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers
|
||||
revision: str = "0036_cross_tenant_fk"
|
||||
down_revision: Union[str, None] = "0035_comm_search_index"
|
||||
branch_labels: Union[str, None] = None
|
||||
depends_on: Union[str, None] = None
|
||||
|
||||
|
||||
def _constraint_exists(name: str) -> str:
|
||||
"""Return SQL that checks if a constraint exists."""
|
||||
return (
|
||||
f"SELECT 1 FROM information_schema.table_constraints "
|
||||
f"WHERE constraint_name = '{name}'"
|
||||
)
|
||||
|
||||
|
||||
def _fk_exists(name: str) -> str:
|
||||
"""Return SQL that checks if a foreign key constraint exists."""
|
||||
return (
|
||||
f"SELECT 1 FROM information_schema.table_constraints "
|
||||
f"WHERE constraint_name = '{name}' AND constraint_type = 'FOREIGN KEY'"
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# ── 1. Add UNIQUE (tenant_id, id) on contacts ──────────────────────────
|
||||
unique_name = "uq_contacts_tenant_id"
|
||||
result = conn.execute(sa.text(_constraint_exists(unique_name))).fetchone()
|
||||
if result is None:
|
||||
op.execute(
|
||||
f"ALTER TABLE contacts ADD CONSTRAINT {unique_name} "
|
||||
f"UNIQUE (tenant_id, id)"
|
||||
)
|
||||
|
||||
# ── 2. contactpersons: replace single-column FK with composite FK ──────
|
||||
# Find and drop the existing FK on contactpersons.contact_id
|
||||
old_cp_fk_result = conn.execute(
|
||||
sa.text(
|
||||
"SELECT conname FROM pg_constraint c "
|
||||
"JOIN pg_class cls ON c.conrelid = cls.oid "
|
||||
"JOIN pg_namespace nsp ON c.connamespace = nsp.oid "
|
||||
"WHERE cls.relname = 'contactpersons' "
|
||||
"AND nsp.nspname = 'public' "
|
||||
"AND c.contype = 'f' "
|
||||
"AND EXISTS ("
|
||||
" SELECT 1 FROM pg_attribute a "
|
||||
" WHERE a.attrelid = c.conrelid AND a.attname = 'contact_id' "
|
||||
" AND a.attnum = ANY(c.conkey)"
|
||||
")"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if old_cp_fk_result is not None:
|
||||
old_cp_fk_name = old_cp_fk_result[0]
|
||||
op.execute(f"ALTER TABLE contactpersons DROP CONSTRAINT IF EXISTS {old_cp_fk_name}")
|
||||
|
||||
# Add composite FK on contactpersons (tenant_id, contact_id) → contacts(tenant_id, id)
|
||||
cp_composite_fk = "fk_contactpersons_tenant_contact"
|
||||
result = conn.execute(sa.text(_fk_exists(cp_composite_fk))).fetchone()
|
||||
if result is None:
|
||||
op.execute(
|
||||
f"ALTER TABLE contactpersons ADD CONSTRAINT {cp_composite_fk} "
|
||||
f"FOREIGN KEY (tenant_id, contact_id) "
|
||||
f"REFERENCES contacts (tenant_id, id) ON DELETE CASCADE"
|
||||
)
|
||||
|
||||
# ── 3. contact_merge_history: replace source_contact_id FK ─────────────
|
||||
old_src_fk_result = conn.execute(
|
||||
sa.text(
|
||||
"SELECT conname FROM pg_constraint c "
|
||||
"JOIN pg_class cls ON c.conrelid = cls.oid "
|
||||
"JOIN pg_namespace nsp ON c.connamespace = nsp.oid "
|
||||
"WHERE cls.relname = 'contact_merge_history' "
|
||||
"AND nsp.nspname = 'public' "
|
||||
"AND c.contype = 'f' "
|
||||
"AND EXISTS ("
|
||||
" SELECT 1 FROM pg_attribute a "
|
||||
" WHERE a.attrelid = c.conrelid AND a.attname = 'source_contact_id' "
|
||||
" AND a.attnum = ANY(c.conkey)"
|
||||
")"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if old_src_fk_result is not None:
|
||||
old_src_fk_name = old_src_fk_result[0]
|
||||
op.execute(f"ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS {old_src_fk_name}")
|
||||
|
||||
src_composite_fk = "fk_merge_history_tenant_source"
|
||||
result = conn.execute(sa.text(_fk_exists(src_composite_fk))).fetchone()
|
||||
if result is None:
|
||||
op.execute(
|
||||
f"ALTER TABLE contact_merge_history ADD CONSTRAINT {src_composite_fk} "
|
||||
f"FOREIGN KEY (tenant_id, source_contact_id) "
|
||||
f"REFERENCES contacts (tenant_id, id) ON DELETE SET NULL"
|
||||
)
|
||||
|
||||
# ── 4. contact_merge_history: replace target_contact_id FK ──────────────
|
||||
old_tgt_fk_result = conn.execute(
|
||||
sa.text(
|
||||
"SELECT conname FROM pg_constraint c "
|
||||
"JOIN pg_class cls ON c.conrelid = cls.oid "
|
||||
"JOIN pg_namespace nsp ON c.connamespace = nsp.oid "
|
||||
"WHERE cls.relname = 'contact_merge_history' "
|
||||
"AND nsp.nspname = 'public' "
|
||||
"AND c.contype = 'f' "
|
||||
"AND EXISTS ("
|
||||
" SELECT 1 FROM pg_attribute a "
|
||||
" WHERE a.attrelid = c.conrelid AND a.attname = 'target_contact_id' "
|
||||
" AND a.attnum = ANY(c.conkey)"
|
||||
")"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if old_tgt_fk_result is not None:
|
||||
old_tgt_fk_name = old_tgt_fk_result[0]
|
||||
op.execute(f"ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS {old_tgt_fk_name}")
|
||||
|
||||
tgt_composite_fk = "fk_merge_history_tenant_target"
|
||||
result = conn.execute(sa.text(_fk_exists(tgt_composite_fk))).fetchone()
|
||||
if result is None:
|
||||
op.execute(
|
||||
f"ALTER TABLE contact_merge_history ADD CONSTRAINT {tgt_composite_fk} "
|
||||
f"FOREIGN KEY (tenant_id, target_contact_id) "
|
||||
f"REFERENCES contacts (tenant_id, id) ON DELETE CASCADE"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Restore single-column FKs and remove composite FKs
|
||||
|
||||
# ── contact_merge_history: target ──
|
||||
op.execute("ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS fk_merge_history_tenant_target")
|
||||
op.execute(
|
||||
"ALTER TABLE contact_merge_history ADD CONSTRAINT "
|
||||
"contact_merge_history_target_contact_id_fkey "
|
||||
"FOREIGN KEY (target_contact_id) REFERENCES contacts (id) ON DELETE CASCADE"
|
||||
)
|
||||
|
||||
# ── contact_merge_history: source ──
|
||||
op.execute("ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS fk_merge_history_tenant_source")
|
||||
op.execute(
|
||||
"ALTER TABLE contact_merge_history ADD CONSTRAINT "
|
||||
"contact_merge_history_source_contact_id_fkey "
|
||||
"FOREIGN KEY (source_contact_id) REFERENCES contacts (id) ON DELETE SET NULL"
|
||||
)
|
||||
|
||||
# ── contactpersons ──
|
||||
op.execute("ALTER TABLE contactpersons DROP CONSTRAINT IF EXISTS fk_contactpersons_tenant_contact")
|
||||
op.execute(
|
||||
"ALTER TABLE contactpersons ADD CONSTRAINT "
|
||||
"contactpersons_contact_id_fkey "
|
||||
"FOREIGN KEY (contact_id) REFERENCES contacts (id) ON DELETE CASCADE"
|
||||
)
|
||||
|
||||
# ── Remove unique (tenant_id, id) on contacts ──
|
||||
op.execute("ALTER TABLE contacts DROP CONSTRAINT IF EXISTS uq_contacts_tenant_id")
|
||||
Reference in New Issue
Block a user