Files
pgmorbac/tests/07_audit.sql
T

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 ==='