"""Create append-only, redacted audit event storage.""" from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "0005_audit_events" down_revision = "0004_space_data_policy" branch_labels = None depends_on = None _TRIGGER_FUNCTION = "prevent_audit_event_mutation" _ROW_TRIGGER = "trg_audit_events_append_only_row" _TRUNCATE_TRIGGER = "trg_audit_events_append_only_truncate" def upgrade() -> None: """Create audit storage and reject database-level mutation attempts.""" op.execute(sa.text("CREATE SCHEMA IF NOT EXISTS audit")) op.create_table( "audit_events", sa.Column("id", sa.Uuid(), primary_key=True), sa.Column("workspace_id", sa.Uuid(), nullable=False), sa.Column("event_type", sa.String(128), nullable=False), sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False), sa.Column("trace_id", sa.String(128), nullable=False), sa.Column("correlation_id", sa.String(128), nullable=True), sa.Column("actor_id", sa.Uuid(), nullable=True), sa.Column("application_id", sa.Uuid(), nullable=True), sa.Column("resource_type", sa.String(128), nullable=True), sa.Column("resource_id", sa.Uuid(), nullable=True), sa.Column("details", sa.JSON(), nullable=False), sa.ForeignKeyConstraint( ["workspace_id"], ["control.workspaces.id"], ondelete="RESTRICT" ), schema="audit", ) op.create_index( "ix_audit_events_workspace_occurred", "audit_events", ["workspace_id", "occurred_at"], schema="audit", ) op.create_index( "ix_audit_events_workspace_type", "audit_events", ["workspace_id", "event_type"], schema="audit", ) op.execute( sa.text( f""" CREATE FUNCTION audit.{_TRIGGER_FUNCTION}() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'audit events are append-only'; END; $$ """ ) ) op.execute( sa.text( f""" CREATE TRIGGER {_ROW_TRIGGER} BEFORE UPDATE OR DELETE ON audit.audit_events FOR EACH ROW EXECUTE FUNCTION audit.{_TRIGGER_FUNCTION}() """ ) ) op.execute( sa.text( f""" CREATE TRIGGER {_TRUNCATE_TRIGGER} BEFORE TRUNCATE ON audit.audit_events FOR EACH STATEMENT EXECUTE FUNCTION audit.{_TRIGGER_FUNCTION}() """ ) ) def downgrade() -> None: """Remove only the audit objects introduced by this migration.""" op.execute(sa.text(f"DROP TRIGGER IF EXISTS {_TRUNCATE_TRIGGER} ON audit.audit_events")) op.execute(sa.text(f"DROP TRIGGER IF EXISTS {_ROW_TRIGGER} ON audit.audit_events")) op.drop_index( "ix_audit_events_workspace_type", table_name="audit_events", schema="audit" ) op.drop_index( "ix_audit_events_workspace_occurred", table_name="audit_events", schema="audit" ) op.drop_table("audit_events", schema="audit") op.execute(sa.text(f"DROP FUNCTION IF EXISTS audit.{_TRIGGER_FUNCTION}()")) op.execute(sa.text("DROP SCHEMA IF EXISTS audit"))