-- +goose Up -- Audit M7: make audit_log append-only. A BEFORE trigger rejects UPDATE and -- DELETE so the security-event history cannot be altered or erased after the -- fact — even by a database account with broad table privileges. The -- application only ever INSERTs into this table. -- The audit_log table itself was never created by a goose migration — it is -- only built by GORM AutoMigrate in the test harness, and production never -- runs AutoMigrate. CREATE TABLE IF NOT EXISTS brings it under migration -- control without disturbing an existing table: a no-op on a DB that already -- has it, and a correct build (matching models.AuditLog) on a from-scratch -- redeploy — so the trigger below has a table to attach to and a clean -- redeploy comes up with a working, append-only audit log. CREATE TABLE IF NOT EXISTS audit_log ( id BIGSERIAL PRIMARY KEY, user_id BIGINT, event_type VARCHAR(50) NOT NULL, ip_address VARCHAR(45), user_agent TEXT, details JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_audit_log_user_id ON audit_log (user_id); CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log (created_at); -- +goose StatementBegin CREATE OR REPLACE FUNCTION audit_log_append_only() RETURNS trigger AS $$ BEGIN RAISE EXCEPTION 'audit_log is append-only: % is not permitted', TG_OP; END; $$ LANGUAGE plpgsql; -- +goose StatementEnd -- DROP ... IF EXISTS before CREATE keeps this idempotent (CREATE TRIGGER has -- no OR REPLACE on older PostgreSQL). DROP TRIGGER IF EXISTS audit_log_no_update ON audit_log; CREATE TRIGGER audit_log_no_update BEFORE UPDATE ON audit_log FOR EACH ROW EXECUTE FUNCTION audit_log_append_only(); DROP TRIGGER IF EXISTS audit_log_no_delete ON audit_log; CREATE TRIGGER audit_log_no_delete BEFORE DELETE ON audit_log FOR EACH ROW EXECUTE FUNCTION audit_log_append_only(); -- +goose Down -- Reverses only the append-only guard, which is this migration's purpose. -- The audit_log table is intentionally NOT dropped — it may hold security -- history that predates this migration. DROP TRIGGER IF EXISTS audit_log_no_delete ON audit_log; DROP TRIGGER IF EXISTS audit_log_no_update ON audit_log; DROP FUNCTION IF EXISTS audit_log_append_only();