diff --git a/alembic/versions/0064_rls_all_tenant_tables.py b/alembic/versions/0064_rls_all_tenant_tables.py new file mode 100644 index 0000000..9117493 --- /dev/null +++ b/alembic/versions/0064_rls_all_tenant_tables.py @@ -0,0 +1,86 @@ +"""Enable RLS on all remaining tenant tables. + +Revision ID: 0064 +Revises: 0063 +Create Date: 2026-07-29 + +Currently RLS is only on contacts. This migration enables RLS on all +tenant-scoped tables that have a tenant_id column but no RLS yet. + +System tables (users, tenants, groups, roles) are excluded — they need +special handling for the login bootstrap process. +""" + +from alembic import op + +revision = "0064" +down_revision = "0063" +branch_labels = None +depends_on = None + +# Tables that should have RLS (tenant-scoped data) +TENANT_TABLES = [ + "addresses", + "attachments", + "bank_accounts", + "contact_folders", + "contact_merge_history", + "workflows", + "sequences", + "saved_filters", + "saved_views", + "webhooks", + "custom_field_definitions", + "notifications", + "ai_conversations", + "contact_persons", + "tags", + "entity_links", + "dms_files", + "dms_folders", + "calendar_events", + "calendars", + "tasks", + "task_lists", + "mail_messages", + "mail_accounts", + "mail_folders", + "conversations", + "conversation_messages", + "conversation_participants", + "audit_log", + "permission_delegations", + "guest_invitations", +] + + +def upgrade() -> None: + for table in TENANT_TABLES: + # Enable RLS if not already enabled + op.execute(f""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_class c + WHERE c.relname = '{table}' + AND c.relrowsecurity = true + ) AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '{table}' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE {table} ENABLE ROW LEVEL SECURITY; + + CREATE POLICY {table}_tenant_isolation ON {table} + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid) + WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + END IF; + END $$; + """) + + +def downgrade() -> None: + for table in TENANT_TABLES: + op.execute(f"DROP POLICY IF EXISTS {table}_tenant_isolation ON {table}") + op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY") diff --git a/app/routes/guest_auth.py b/app/routes/guest_auth.py index 127c4a6..7427caa 100644 --- a/app/routes/guest_auth.py +++ b/app/routes/guest_auth.py @@ -112,6 +112,9 @@ async def guest_login( 1800, # 30 min TTL json.dumps(session_data), ) + # Track session in guest index for revocation (P1.6 fix) + await redis.sadd(f"guest_sessions:{guest.id}", session_id) + await redis.expire(f"guest_sessions:{guest.id}", 1800) from fastapi.responses import JSONResponse @@ -145,6 +148,14 @@ async def guest_logout( session_id = request.cookies.get("guest_session") if session_id: redis = get_redis() + # Remove from guest sessions index (P1.6 fix) + guest_data = await redis.get(f"guest_session:{session_id}") + if guest_data: + import json + data = json.loads(guest_data) + gid = data.get("guest_user_id") + if gid: + await redis.srem(f"guest_sessions:{gid}", session_id) await redis.delete(f"guest_session:{session_id}") from fastapi.responses import JSONResponse