-- ============================================================================= -- AUDIT LOG -- ============================================================================= -- Optional audit logging for tracking changes to security-critical tables -- Enable/disable per table with triggers CREATE TABLE morbac.audit_log ( id BIGSERIAL PRIMARY KEY, timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, user_id UUID, org_id UUID, table_name TEXT NOT NULL, operation TEXT NOT NULL, record_id UUID, old_data JSONB, new_data JSONB, changed_fields TEXT[], session_user TEXT DEFAULT SESSION_USER, client_addr INET DEFAULT INET_CLIENT_ADDR(), application_name TEXT DEFAULT CURRENT_SETTING('application_name', true), metadata JSONB DEFAULT '{}'::jsonb ); CREATE INDEX idx_audit_log_timestamp ON morbac.audit_log(timestamp DESC); CREATE INDEX idx_audit_log_user_id ON morbac.audit_log(user_id); CREATE INDEX idx_audit_log_org_id ON morbac.audit_log(org_id); CREATE INDEX idx_audit_log_table_operation ON morbac.audit_log(table_name, operation); CREATE INDEX idx_audit_log_record_id ON morbac.audit_log(record_id); COMMENT ON TABLE morbac.audit_log IS 'Audit trail for security-critical operations'; COMMENT ON COLUMN morbac.audit_log.user_id IS 'Application user (from morbac.current_user_id() if available)'; COMMENT ON COLUMN morbac.audit_log.org_id IS 'Organization context (from morbac.current_org_id() if available)'; COMMENT ON COLUMN morbac.audit_log.operation IS 'INSERT, UPDATE, DELETE, or custom operation name'; COMMENT ON COLUMN morbac.audit_log.changed_fields IS 'Array of field names that changed (for UPDATE operations)'; COMMENT ON COLUMN morbac.audit_log.session_user IS 'Database session user'; COMMENT ON COLUMN morbac.audit_log.client_addr IS 'Client IP address'; -- Generic audit trigger function CREATE OR REPLACE FUNCTION morbac.audit_trigger() RETURNS TRIGGER LANGUAGE plpgsql AS $$ DECLARE v_old_data JSONB; v_new_data JSONB; v_changed_fields TEXT[]; v_user_id UUID; v_org_id UUID; v_record_id UUID; BEGIN -- Try to get current user/org context BEGIN v_user_id := morbac.current_user_id(); EXCEPTION WHEN OTHERS THEN v_user_id := NULL; END; BEGIN v_org_id := morbac.current_org_id(); EXCEPTION WHEN OTHERS THEN v_org_id := NULL; END; -- Handle different operations IF TG_OP = 'DELETE' THEN v_old_data := row_to_json(OLD)::jsonb; v_new_data := NULL; v_record_id := (v_old_data->>'id')::uuid; ELSIF TG_OP = 'INSERT' THEN v_old_data := NULL; v_new_data := row_to_json(NEW)::jsonb; v_record_id := (v_new_data->>'id')::uuid; ELSIF TG_OP = 'UPDATE' THEN v_old_data := row_to_json(OLD)::jsonb; v_new_data := row_to_json(NEW)::jsonb; v_record_id := (v_new_data->>'id')::uuid; -- Identify changed fields SELECT array_agg(key) INTO v_changed_fields FROM jsonb_each(v_old_data) WHERE v_old_data->key IS DISTINCT FROM v_new_data->key; END IF; -- Override org_id from record if available IF v_org_id IS NULL THEN IF v_new_data ? 'org_id' THEN v_org_id := (v_new_data->>'org_id')::uuid; ELSIF v_old_data ? 'org_id' THEN v_org_id := (v_old_data->>'org_id')::uuid; END IF; END IF; -- Insert audit record INSERT INTO morbac.audit_log ( user_id, org_id, table_name, operation, record_id, old_data, new_data, changed_fields ) VALUES ( v_user_id, v_org_id, TG_TABLE_NAME, TG_OP, v_record_id, v_old_data, v_new_data, v_changed_fields ); -- Return appropriate value IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF; END; $$; COMMENT ON FUNCTION morbac.audit_trigger() IS 'Generic audit trigger function - captures INSERT/UPDATE/DELETE operations'; -- Helper function to enable audit logging on a table CREATE OR REPLACE FUNCTION morbac.enable_audit(p_table_name TEXT) RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE v_trigger_name TEXT; BEGIN v_trigger_name := 'audit_' || p_table_name; EXECUTE format( 'CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE ON morbac.%I ' || 'FOR EACH ROW EXECUTE FUNCTION morbac.audit_trigger()', v_trigger_name, p_table_name ); RAISE NOTICE 'Audit logging enabled for morbac.%', p_table_name; END; $$; COMMENT ON FUNCTION morbac.enable_audit(TEXT) IS 'Enable audit logging on a morbac table - creates audit trigger'; -- Helper function to disable audit logging on a table CREATE OR REPLACE FUNCTION morbac.disable_audit(p_table_name TEXT) RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE v_trigger_name TEXT; BEGIN v_trigger_name := 'audit_' || p_table_name; EXECUTE format( 'DROP TRIGGER IF EXISTS %I ON morbac.%I', v_trigger_name, p_table_name ); RAISE NOTICE 'Audit logging disabled for morbac.%', p_table_name; END; $$; COMMENT ON FUNCTION morbac.disable_audit(TEXT) IS 'Disable audit logging on a morbac table - removes audit trigger';