"""Create audit_logs table with RLS policy. Revision ID: 007 Revises: 006 Create Date: 2026-03-02 This migration: 1. Creates audit_logs table for centralized audit trail. 2. Applies RLS policy for tenant isolation. 3. Creates indexes for fast paginated and filtered queries. 4. Grants SELECT, INSERT to app_user (read and write audit entries). """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "007" down_revision: Union[str, None] = "006" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: conn = op.get_bind() # ========================================================================= # CREATE audit_logs TABLE # ========================================================================= conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE SET NULL, action VARCHAR(100) NOT NULL, resource_type VARCHAR(50), resource_id VARCHAR(255), device_id UUID REFERENCES devices(id) ON DELETE SET NULL, details JSONB NOT NULL DEFAULT '{}'::jsonb, ip_address VARCHAR(45), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """)) # ========================================================================= # RLS POLICY # ========================================================================= conn.execute(sa.text( "ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY" )) conn.execute(sa.text(""" CREATE POLICY audit_logs_tenant_isolation ON audit_logs USING (tenant_id = current_setting('app.current_tenant')::uuid) """)) # Grant SELECT + INSERT to app_user (no UPDATE/DELETE -- audit logs are immutable) conn.execute(sa.text( "GRANT SELECT, INSERT ON audit_logs TO app_user" )) # Poller user gets full access for cross-tenant audit logging conn.execute(sa.text( "GRANT ALL ON audit_logs TO poller_user" )) # ========================================================================= # INDEXES # ========================================================================= conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_created " "ON audit_logs (tenant_id, created_at DESC)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_action " "ON audit_logs (tenant_id, action)" )) def downgrade() -> None: conn = op.get_bind() conn.execute(sa.text("DROP TABLE IF EXISTS audit_logs CASCADE"))