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,104 @@
|
||||
"""FORCE Row Level Security + WITH CHECK on all tenant-scoped tables.
|
||||
|
||||
Revision ID: 0028_rls_force
|
||||
Revises: 0027_unify_company_to_contact
|
||||
Create Date: 2026-07-25
|
||||
|
||||
This migration:
|
||||
1. Discovers all tables in the public schema that have a tenant_id column.
|
||||
2. ALTER TABLE ... FORCE ROW LEVEL SECURITY on each (ensures RLS applies to table owners too).
|
||||
3. Drops existing tenant_isolation policies and recreates them with both
|
||||
USING and WITH CHECK clauses so writes are also filtered by tenant.
|
||||
4. Covers core tables AND plugin tables (anything with tenant_id).
|
||||
|
||||
Idempotent: safe to run multiple times.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "0028_rls_force"
|
||||
down_revision: Union[str, None] = "0027_unify_company_to_contact"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
logger = logging.getLogger("alembic.migration.0028_rls_force")
|
||||
|
||||
|
||||
def _discover_tenant_tables(conn) -> list[str]:
|
||||
"""Return all table names in the public schema that have a tenant_id column."""
|
||||
result = conn.execute(
|
||||
sa.text(
|
||||
"SELECT table_name FROM information_schema.columns "
|
||||
"WHERE table_schema = 'public' AND column_name = 'tenant_id' "
|
||||
"ORDER BY table_name"
|
||||
)
|
||||
)
|
||||
return [row[0] for row in result.fetchall()]
|
||||
|
||||
|
||||
def _discover_existing_policies(conn, table_name: str) -> list[str]:
|
||||
"""Return all policy names on *table_name* that contain 'tenant' or 'isolation'."""
|
||||
result = conn.execute(
|
||||
sa.text(
|
||||
"SELECT policyname FROM pg_policies "
|
||||
"WHERE schemaname = 'public' AND tablename = :t"
|
||||
),
|
||||
{"t": table_name},
|
||||
)
|
||||
return [row[0] for row in result.fetchall()]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
tenant_tables = _discover_tenant_tables(conn)
|
||||
logger.info("Discovered %d tenant-scoped tables: %s", len(tenant_tables), tenant_tables)
|
||||
|
||||
for table_name in tenant_tables:
|
||||
# 1. Enable RLS (idempotent — ENABLE is safe to repeat)
|
||||
op.execute(f'ALTER TABLE "{table_name}" ENABLE ROW LEVEL SECURITY')
|
||||
|
||||
# 2. FORCE RLS — ensures policies apply even to table owners/superusers
|
||||
# who would otherwise bypass RLS
|
||||
op.execute(f'ALTER TABLE "{table_name}" FORCE ROW LEVEL SECURITY')
|
||||
|
||||
# 3. Drop ALL existing policies on this table that relate to tenant isolation
|
||||
existing_policies = _discover_existing_policies(conn, table_name)
|
||||
for policy_name in existing_policies:
|
||||
op.execute(f'DROP POLICY IF EXISTS "{policy_name}" ON "{table_name}"')
|
||||
logger.info("Dropped policy %s on %s", policy_name, table_name)
|
||||
|
||||
# 4. Create new policy with both USING and WITH CHECK
|
||||
# USING: filters rows visible in SELECT/UPDATE/DELETE
|
||||
# WITH CHECK: enforces tenant_id on INSERT/UPDATE
|
||||
op.execute(
|
||||
f'CREATE POLICY tenant_isolation ON "{table_name}" '
|
||||
f"USING (tenant_id = current_setting('app.current_tenant_id')::uuid) "
|
||||
f"WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid)"
|
||||
)
|
||||
logger.info("Created policy tenant_isolation on %s (USING + WITH CHECK)", table_name)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Revert FORCE RLS and restore USING-only policies (matching 0015 behavior)."""
|
||||
conn = op.get_bind()
|
||||
tenant_tables = _discover_tenant_tables(conn)
|
||||
|
||||
for table_name in tenant_tables:
|
||||
# Drop the USING+WITH CHECK policy
|
||||
op.execute(f'DROP POLICY IF EXISTS tenant_isolation ON "{table_name}"')
|
||||
|
||||
# Remove FORCE but keep ENABLE (matching pre-0028 state)
|
||||
op.execute(f'ALTER TABLE "{table_name}" NO FORCE ROW LEVEL SECURITY')
|
||||
|
||||
# Recreate USING-only policy (matching original 0015 behavior)
|
||||
op.execute(
|
||||
f'CREATE POLICY tenant_isolation ON "{table_name}" '
|
||||
f"USING (tenant_id = current_setting('app.current_tenant_id')::uuid)"
|
||||
)
|
||||
logger.info("Reverted %s to USING-only policy (removed FORCE, removed WITH CHECK)", table_name)
|
||||
Reference in New Issue
Block a user