Files
pgmorbac/tests/07_audit.sql
T
marc cc3d65a013 docs: complete function reference, drop unicode punctuation
Documents refresh_hierarchy_cache, is_rule_valid and org_in_scope, and
corrects the get_org_scope scope list which still omitted unattributed
and all.

Replaces em dashes and other typographic unicode with ASCII throughout
the schema comments, the test suite and the documentation. Comments and
prose are ASCII only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 07:26:54 +02:00

281 lines
11 KiB
SQL

-- =============================================================================
-- Audit Logging Tests
-- =============================================================================
-- Tests the audit system: enable_audit / disable_audit and the resulting
-- audit_log entries for INSERT, UPDATE, and DELETE operations.
--
-- Cases:
-- 1. Enable audit creates trigger on specified table
-- 2. INSERT to audited table creates an audit log entry
-- 3. DELETE from audited table creates an audit log entry
-- 4. UPDATE to audited table creates an audit log entry with changed_fields
-- 5. All operations for a record_id are queryable
-- 6. Multiple tables can be audited independently
-- 7. Disable audit removes the trigger
-- 8. After disabling, changes are no longer logged
--
-- Prerequisites: 00_setup.sql -> 06_cross_org.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '07 - AUDIT LOGGING'
\echo '================================================================'
-- Clear any existing audit log entries to start fresh
TRUNCATE morbac.audit_log;
-- ---------------------------------------------------------------------------
-- Section 1: Enable audit on user_roles
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. Enable audit on user_roles ---'
SELECT morbac.enable_audit('user_roles');
-- Trigger must exist on user_roles after enable
SELECT morbac.t('Trigger exists on user_roles after enable_audit',
(SELECT COUNT(*) > 0 FROM information_schema.triggers
WHERE event_object_schema = 'morbac'
AND event_object_table = 'user_roles'),
TRUE);
-- No entries before any change
SELECT morbac.t_eq('Audit log count before operations = 0',
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'user_roles')::bigint,
0);
-- ---------------------------------------------------------------------------
-- Section 2: INSERT is logged
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. INSERT is logged ---'
-- Assign a new role to Karl (user who has no role)
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
VALUES (
'30000000-0000-0000-0000-000000000011', -- Karl
'20000000-0001-0000-0000-000000000005', -- intern role
'10000000-0000-0000-0000-000000000001'
);
-- Exactly 1 INSERT entry for user_roles
SELECT morbac.t_eq('Audit log has 1 INSERT entry after INSERT into user_roles',
(SELECT COUNT(*) FROM morbac.audit_log
WHERE table_name = 'user_roles' AND operation = 'INSERT')::bigint,
1);
-- new_data must contain the inserted record
SELECT morbac.t('INSERT audit entry contains user_id in new_data',
(SELECT new_data->>'user_id' FROM morbac.audit_log
WHERE table_name = 'user_roles' AND operation = 'INSERT'
LIMIT 1) = '30000000-0000-0000-0000-000000000011',
TRUE);
-- ---------------------------------------------------------------------------
-- Section 3: DELETE is logged
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. DELETE is logged ---'
DELETE FROM morbac.user_roles
WHERE user_id = '30000000-0000-0000-0000-000000000011'
AND role_id = '20000000-0001-0000-0000-000000000005'
AND org_id = '10000000-0000-0000-0000-000000000001';
-- Now 2 entries: INSERT + DELETE
SELECT morbac.t_eq('Audit log has 2 entries after DELETE (INSERT + DELETE)',
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'user_roles')::bigint,
2);
-- DELETE entry must have old_data populated
SELECT morbac.t('DELETE audit entry has old_data populated',
(SELECT old_data IS NOT NULL FROM morbac.audit_log
WHERE table_name = 'user_roles' AND operation = 'DELETE'
LIMIT 1),
TRUE);
-- ---------------------------------------------------------------------------
-- Section 4: Enable audit on rules and test UPDATE logging
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. UPDATE on rules table is logged (with changed_fields) ---'
SELECT morbac.enable_audit('rules');
-- Insert a rule to audit (using explicit id for record_id tracking)
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality)
VALUES (
'a0000000-0000-0000-0000-000000000001',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000009', -- contractor
'read', 'public_data',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'
);
-- 1 INSERT entry for rules
SELECT morbac.t_eq('Audit log has 1 INSERT entry for rules',
(SELECT COUNT(*) FROM morbac.audit_log
WHERE table_name = 'rules' AND operation = 'INSERT')::bigint,
1);
-- Update the modality (simulate a policy change)
UPDATE morbac.rules
SET modality = 'prohibition'
WHERE id = 'a0000000-0000-0000-0000-000000000001';
-- 1 UPDATE entry with changed_fields
SELECT morbac.t_eq('Audit log has 1 UPDATE entry for rules',
(SELECT COUNT(*) FROM morbac.audit_log
WHERE table_name = 'rules' AND operation = 'UPDATE')::bigint,
1);
-- changed_fields must be non-null for UPDATE
SELECT morbac.t('UPDATE audit entry has changed_fields populated',
(SELECT changed_fields IS NOT NULL FROM morbac.audit_log
WHERE table_name = 'rules' AND operation = 'UPDATE'
LIMIT 1),
TRUE);
-- old_data and new_data both populated for UPDATE
SELECT morbac.t('UPDATE audit entry: old modality = permission',
(SELECT old_data->>'modality' FROM morbac.audit_log
WHERE table_name = 'rules' AND operation = 'UPDATE'
LIMIT 1) = 'permission',
TRUE);
SELECT morbac.t('UPDATE audit entry: new modality = prohibition',
(SELECT new_data->>'modality' FROM morbac.audit_log
WHERE table_name = 'rules' AND operation = 'UPDATE'
LIMIT 1) = 'prohibition',
TRUE);
-- Clean up the audited rule
DELETE FROM morbac.rules WHERE id = 'a0000000-0000-0000-0000-000000000001';
-- ---------------------------------------------------------------------------
-- Section 5: All operations queryable by record_id
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Query audit log by record_id ---'
-- The test rule had INSERT, UPDATE, DELETE - should be 3 entries
SELECT morbac.t_eq('Audit log has 3 entries for test rule record (INSERT + UPDATE + DELETE)',
(SELECT COUNT(*) FROM morbac.audit_log
WHERE table_name = 'rules'
AND record_id = 'a0000000-0000-0000-0000-000000000001')::bigint,
3);
-- All three operations present
SELECT morbac.t('INSERT, UPDATE, DELETE all present for test rule record',
(SELECT COUNT(DISTINCT operation) FROM morbac.audit_log
WHERE table_name = 'rules'
AND record_id = 'a0000000-0000-0000-0000-000000000001')::bigint = 3,
TRUE);
-- ---------------------------------------------------------------------------
-- Section 6: Multiple tables audited independently
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 6. Multiple tables audited independently ---'
SELECT morbac.enable_audit('delegations');
-- Insert a delegation to trigger audit on the delegations table
INSERT INTO morbac.delegations
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
VALUES (
'de000099-0000-0000-0000-000000000099',
'30000000-0000-0000-0000-000000000001', -- Alice (CEO)
'30000000-0000-0000-0000-000000000011', -- Karl
'20000000-0001-0000-0000-000000000001', -- ceo role
'10000000-0000-0000-0000-000000000001',
now(),
now() + interval '1 hour'
);
-- delegations table now has audit entries
SELECT morbac.t('Audit log has at least 1 entry for delegations table',
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'delegations') >= 1,
TRUE);
-- All three tables have audit entries
SELECT morbac.t('user_roles, rules, and delegations all have audit entries',
(SELECT COUNT(DISTINCT table_name) FROM morbac.audit_log
WHERE table_name IN ('user_roles', 'rules', 'delegations'))::bigint = 3,
TRUE);
-- Clean up the test delegation
DELETE FROM morbac.delegations WHERE id = 'de000099-0000-0000-0000-000000000099';
-- ---------------------------------------------------------------------------
-- Section 7: Disable audit removes trigger
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. Disable audit removes trigger ---'
SELECT morbac.disable_audit('user_roles');
-- Audit trigger must be removed from user_roles (other system triggers may remain)
SELECT morbac.t('No audit trigger on user_roles after disable_audit',
(SELECT COUNT(*) = 0 FROM information_schema.triggers
WHERE event_object_schema = 'morbac'
AND event_object_table = 'user_roles'
AND trigger_name LIKE '%audit%'),
TRUE);
-- ---------------------------------------------------------------------------
-- Section 8: After disabling, changes are no longer logged
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 8. Disable audit stops logging ---'
-- Record current count for user_roles before post-disable operations
-- (2 entries from sections 2+3)
DO $$
DECLARE
count_before BIGINT;
BEGIN
SELECT COUNT(*) INTO count_before FROM morbac.audit_log WHERE table_name = 'user_roles';
-- Store in a temp table for later assertion
CREATE TEMP TABLE IF NOT EXISTS audit_count_check (cnt BIGINT);
DELETE FROM audit_count_check;
INSERT INTO audit_count_check VALUES (count_before);
END;
$$;
-- Perform another user_roles change - should NOT be logged
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
VALUES (
'30000000-0000-0000-0000-000000000011',
'20000000-0001-0000-0000-000000000005', -- intern
'10000000-0000-0000-0000-000000000001'
);
DELETE FROM morbac.user_roles
WHERE user_id = '30000000-0000-0000-0000-000000000011'
AND role_id = '20000000-0001-0000-0000-000000000005';
-- Count after operations must equal count before (no new entries)
SELECT morbac.t('user_roles audit count unchanged after post-disable operations',
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'user_roles')::bigint
= (SELECT cnt FROM audit_count_check),
TRUE);
DROP TABLE IF EXISTS audit_count_check;
-- Disable the remaining audit triggers
SELECT morbac.disable_audit('rules');
SELECT morbac.disable_audit('delegations');
-- All audit triggers removed
SELECT morbac.t('No audit triggers remain after all disable_audit calls',
(SELECT COUNT(*) = 0 FROM information_schema.triggers
WHERE event_object_schema = 'morbac'
AND trigger_name LIKE '%audit%'),
TRUE);
\echo ''
\echo '=== Audit Logging Tests Completed ==='