-- Bundle-6 / Audit M-017 / HIPAA §164.312(b) (audit controls): -- -- ARCH-L3 (2026-05-13): this migration uses CREATE OR REPLACE FUNCTION -- + DROP TRIGGER IF EXISTS + CREATE TRIGGER + DO $$ ... pg_roles -- existence check. These patterns produce equivalent idempotency to -- IF NOT EXISTS but in shapes Postgres' CREATE TRIGGER + REVOKE -- syntax do not literally accept. Re-running this migration is safe. -- -- audit_events is append-only at the database layer. The application -- role cannot UPDATE or DELETE rows. Compliance superusers (legal hold, -- retention purges) use a separate role provisioned out-of-band that -- bypasses this trigger; see docs/compliance.md for the operator -- pattern. -- -- Pre-Bundle-6 enforcement was app-layer only (no DELETE/UPDATE method -- on AuditService). A buggy migration script, a manual psql session, or -- an attacker with the app role's DB credentials could rewrite history. -- This trigger is the load-bearing defence; the REVOKE below is -- defence-in-depth. CREATE OR REPLACE FUNCTION audit_events_block_modification() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'audit_events is append-only (Bundle-6 / M-017 / HIPAA §164.312(b))' USING ERRCODE = 'check_violation', HINT = 'Use a compliance superuser role for legitimate retention operations.'; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS audit_events_worm_trigger ON audit_events; CREATE TRIGGER audit_events_worm_trigger BEFORE UPDATE OR DELETE ON audit_events FOR EACH ROW EXECUTE FUNCTION audit_events_block_modification(); -- Defence-in-depth: revoke UPDATE + DELETE from the app role too. -- The role is conventionally `certctl` (matches docker-compose POSTGRES_USER -- and Helm values.yaml postgresql.username). If the role doesn't exist -- (test fixtures, single-superuser setups), the DO block is a no-op so -- the migration stays idempotent across all environments. DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'certctl') THEN REVOKE UPDATE, DELETE ON audit_events FROM certctl; END IF; END $$;