system-prompts-and-models-o.../salesflow-saas/backend/alembic/versions/20260417_0002_add_rls.py
Claude 38e9d02075
feat(dealix): close ALL 4 Tier-1 runtime gaps (Programs E, F, G, K, J)
Program F — Multi-Tenancy RLS (Row-Level Security):
  alembic 20260417_0002_add_rls.py: Enables RLS on 23 tenant-scoped tables.
  database_rls.py: set_tenant_context() helpers for SET LOCAL app.tenant_id.
  middleware/tenant_rls.py: Extracts tenant_id from JWT on every request.
  Default-deny when no context. PostgreSQL only (CI safe on SQLite).
  Result: OWASP A01:2025 — access control enforced at DB layer.

Program G — Idempotency Standard:
  models/idempotency_key.py: IdempotencyKey table with TTL + SHA256 hash.
  services/idempotency_service.py: get_existing/store with request fingerprint.
  middleware/idempotency.py: HTTP middleware on POST/PUT/PATCH.
  Result: Duplicate side effects prevented on retry.

Program E — Persistent Durable Execution:
  models/durable_checkpoint.py: DurableCheckpoint with sequence_num + status.
  services/durable_runtime.py: start_run/checkpoint/complete/resume/list_incomplete.
  Result: Workflows survive crashes — resume from last persisted checkpoint.

Program K — OpenTelemetry:
  observability/otel.py: init/span/inject_correlation_id with graceful
    degradation when OTel packages absent.
  openclaw/gateway.py: Wraps execute() in span, binds correlation_id to
    trace_id. Bridge between business correlation and production observability.

Program J — Release Gate Hardening:
  docs/governance/release-gates.md: Documents 3 mandatory gates.
  .github/workflows/dealix-ci.yml: Adds release_readiness_matrix as CI step.
  release_readiness_matrix.py: Updated to check 41/41 components.

Verification:
  architecture_brief.py:     40/40 PASS
  release_readiness_matrix.py: 41/41 PASS

https://claude.ai/code/session_01W1rJthWDkasijTdXCfxVHs
2026-04-17 10:12:04 +00:00

116 lines
3.9 KiB
Python

"""Enable PostgreSQL Row-Level Security on tenant-scoped tables.
Revision ID: 20260417_0002
Revises: 20260403_0001
Create Date: 2026-04-17
This migration enables RLS on all tenant-scoped tables. RLS policies
filter by current_setting('app.tenant_id') which the app sets via
SET LOCAL on each request (see app/database_rls.py).
OWASP A01:2025 — moves access control from app convention to DB-enforced
default-deny posture.
Skipped on SQLite (CI). Production PostgreSQL only.
"""
from typing import Sequence, Union
from alembic import op
revision: str = "20260417_0002"
down_revision: Union[str, None] = "20260403_0001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# Tables with tenant_id column that need RLS
TENANT_SCOPED_TABLES = [
"deals",
"leads",
"approval_requests",
"evidence_packs",
"contradictions",
"compliance_controls",
"ai_conversations",
"audit_logs",
"integration_sync_states",
"strategic_deals",
"domain_events",
"consents",
"complaints",
"messages",
"activities",
"proposals",
"sequences",
"company_profiles",
"deal_matches",
"calls",
"auto_bookings",
"trust_scores",
"scorecards",
]
def upgrade() -> None:
"""Enable RLS on tenant-scoped tables (PostgreSQL only)."""
bind = op.get_bind()
if bind.dialect.name != "postgresql":
return # SQLite/CI: skip
for table in TENANT_SCOPED_TABLES:
# Check if table exists before applying RLS
op.execute(f"""
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '{table}') THEN
ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;
ALTER TABLE {table} FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation_select ON {table};
CREATE POLICY tenant_isolation_select ON {table}
FOR SELECT
USING (tenant_id::text = current_setting('app.tenant_id', true));
DROP POLICY IF EXISTS tenant_isolation_insert ON {table};
CREATE POLICY tenant_isolation_insert ON {table}
FOR INSERT
WITH CHECK (tenant_id::text = current_setting('app.tenant_id', true));
DROP POLICY IF EXISTS tenant_isolation_update ON {table};
CREATE POLICY tenant_isolation_update ON {table}
FOR UPDATE
USING (tenant_id::text = current_setting('app.tenant_id', true))
WITH CHECK (tenant_id::text = current_setting('app.tenant_id', true));
DROP POLICY IF EXISTS tenant_isolation_delete ON {table};
CREATE POLICY tenant_isolation_delete ON {table}
FOR DELETE
USING (tenant_id::text = current_setting('app.tenant_id', true));
END IF;
END $$;
""")
def downgrade() -> None:
"""Disable RLS on all tenant-scoped tables."""
bind = op.get_bind()
if bind.dialect.name != "postgresql":
return
for table in TENANT_SCOPED_TABLES:
op.execute(f"""
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '{table}') THEN
DROP POLICY IF EXISTS tenant_isolation_select ON {table};
DROP POLICY IF EXISTS tenant_isolation_insert ON {table};
DROP POLICY IF EXISTS tenant_isolation_update ON {table};
DROP POLICY IF EXISTS tenant_isolation_delete ON {table};
ALTER TABLE {table} NO FORCE ROW LEVEL SECURITY;
ALTER TABLE {table} DISABLE ROW LEVEL SECURITY;
END IF;
END $$;
""")