feat: tenant isolation Phase 3 — audit_logs, tree_shares, remaining RLS
P3-A: Add account_id to audit_logs model + migration (backfill via user_id → users.account_id). log_audit() gains optional account_id param with fallback SELECT to avoid churn across 40 call sites. P3-B: Add account_id to tree_shares model + migration (backfill via created_by → users.account_id). TreeShare constructor updated in trees.py. P3-C: Enable RLS on 6 remaining tables: step_ratings, step_usage_log, target_lists, session_shares, audit_logs, tree_shares. P3-D: Drop team_id from target_lists — endpoint, schema, and model now use account_id as the sole isolation key. P3-E: Append Phase 3 RLS isolation tests for all 6 tables. test_target_lists.py: fix cross-account test to use Account model (not Team) and set account_id on new User. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
59
backend/alembic/versions/04f013768235_enable_rls_phase3.py
Normal file
59
backend/alembic/versions/04f013768235_enable_rls_phase3.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Enable RLS on Phase 3 tables.
|
||||
|
||||
Tables covered:
|
||||
- step_ratings (account_id NOT NULL since migration 7167e9374b0c)
|
||||
- step_usage_log (account_id NOT NULL since migration 7167e9374b0c)
|
||||
- target_lists (account_id NOT NULL since migration 2c6aabd89bc6)
|
||||
- session_shares (account_id NOT NULL since session_share model)
|
||||
- audit_logs (account_id NOT NULL since migration 2a9056eddd90)
|
||||
- tree_shares (account_id NOT NULL since migration a05e1a1bea7c)
|
||||
|
||||
All use a standard intra-tenant isolation policy.
|
||||
Token-based access to session_shares and tree_shares goes through
|
||||
endpoints that use get_admin_db (BYPASSRLS), so a strict tenant
|
||||
policy here is correct.
|
||||
|
||||
Revision ID: 04f013768235
|
||||
Revises: a05e1a1bea7c
|
||||
Create Date: 2026-04-11 00:00:00.000000
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = '04f013768235'
|
||||
down_revision: Union[str, None] = 'a05e1a1bea7c'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
_CURRENT_ACCOUNT = (
|
||||
"COALESCE(NULLIF(current_setting('app.current_account_id', TRUE), ''), "
|
||||
"'00000000-0000-0000-0000-000000000000')::uuid"
|
||||
)
|
||||
|
||||
_STANDARD_USING = f"account_id = {_CURRENT_ACCOUNT}"
|
||||
|
||||
_PHASE3_TABLES = [
|
||||
"step_ratings",
|
||||
"step_usage_log",
|
||||
"target_lists",
|
||||
"session_shares",
|
||||
"audit_logs",
|
||||
"tree_shares",
|
||||
]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for table in _PHASE3_TABLES:
|
||||
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
|
||||
op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY")
|
||||
op.execute(f"""
|
||||
CREATE POLICY tenant_isolation ON {table}
|
||||
USING ({_STANDARD_USING})
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for table in _PHASE3_TABLES:
|
||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table}")
|
||||
op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY")
|
||||
op.execute(f"ALTER TABLE {table} NO FORCE ROW LEVEL SECURITY")
|
||||
Reference in New Issue
Block a user