feat(tests): add more tests and make them pass

This commit is contained in:
2026-03-22 23:25:48 +01:00
parent 90b96079a0
commit c22d18063b
13 changed files with 3332 additions and 643 deletions
+314 -67
View File
@@ -1,82 +1,329 @@
-- ============================================================================
-- MORBAC PostgreSQL Extension - Temporal Constraint Tests
-- ============================================================================
-- This test file covers temporal constraints including:
-- - Rules with valid_from timestamps (future activation)
-- - Rules with valid_until timestamps (expiration)
-- - Rules with both valid_from and valid_until (time windows)
-- - Authorization decisions respecting temporal bounds
-- =============================================================================
-- Temporal Constraint Tests
-- =============================================================================
-- Tests time-bounded rules using valid_from and valid_until on the rules table.
--
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
-- ============================================================================
-- Cases:
-- 1. is_rule_valid() helper function tested directly
-- 2. is_active flag set by trigger on insert/update
-- 3. Expired rule (valid_until in the past) -> denied
-- 4. Future rule (valid_from in the future) -> denied
-- 5. Active time window (valid_from past, valid_until future) -> allowed
-- 6. Multiple rules for same combination — only active ones count
-- 7. Expired prohibition: no longer blocks access after it expires
-- 8. Temporal rules interact correctly with role hierarchy
--
-- Note: Direct rule inserts are used (bypassing Policy DSL) to control timestamps.
-- Uses a dedicated view 'temp_view' for isolation so existing rules aren't disturbed.
--
-- Prerequisites: 00_setup.sql -> 04_constraints.sql
-- =============================================================================
\echo ''
\echo '=== Temporal Constraints on Rules ==='
\echo '================================================================'
\echo '05 — TEMPORAL CONSTRAINTS'
\echo '================================================================'
-- Create a time-limited rule (expires in the past)
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until)
VALUES (
'11111111-1111-1111-1111-111111111111',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', -- employee role
'write',
'temp_documents',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
'2020-01-01 00:00:00+00' -- Expired rule
);
-- ---------------------------------------------------------------------------
-- Setup: dedicated view for temporal tests
-- ---------------------------------------------------------------------------
INSERT INTO morbac.views (name, description) VALUES
('temp_view', 'Scratch view for temporal tests');
-- Create a future rule (not yet active)
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from)
VALUES (
'11111111-1111-1111-1111-111111111111',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'write',
'future_documents',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
'2030-01-01 00:00:00+00' -- Future rule
);
-- ---------------------------------------------------------------------------
-- Section 1: is_rule_valid() helper function
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. is_rule_valid() helper ---'
-- Create an active time-bounded rule
-- No bounds: always valid
SELECT morbac.t('is_rule_valid(NULL, NULL) — always valid',
morbac.is_rule_valid(NULL::timestamptz, NULL::timestamptz), TRUE);
-- Past valid_from, no valid_until: currently active
SELECT morbac.t('is_rule_valid(past, NULL) — started in past, no end',
morbac.is_rule_valid('2000-01-01'::timestamptz, NULL), TRUE);
-- Future valid_from: not yet active
SELECT morbac.t('is_rule_valid(future, NULL) — not yet started',
morbac.is_rule_valid('2099-01-01'::timestamptz, NULL), FALSE);
-- Past valid_until: expired
SELECT morbac.t('is_rule_valid(NULL, past) — already expired',
morbac.is_rule_valid(NULL, '2000-01-01'::timestamptz), FALSE);
-- Future valid_until, no valid_from: currently active
SELECT morbac.t('is_rule_valid(NULL, future) — no start, future end',
morbac.is_rule_valid(NULL, '2099-01-01'::timestamptz), TRUE);
-- Active window: past start, future end
SELECT morbac.t('is_rule_valid(past, future) — within active window',
morbac.is_rule_valid('2000-01-01'::timestamptz, '2099-01-01'::timestamptz), TRUE);
-- Fully past window (both start and end in the past)
SELECT morbac.t('is_rule_valid(past_start, past_end) — entirely expired',
morbac.is_rule_valid('2000-01-01'::timestamptz, '2001-01-01'::timestamptz), FALSE);
-- Fully future window (both start and end in the future)
SELECT morbac.t('is_rule_valid(future_start, future_end) — entirely in the future',
morbac.is_rule_valid('2090-01-01'::timestamptz, '2099-01-01'::timestamptz), FALSE);
-- ---------------------------------------------------------------------------
-- Section 2: is_active flag set by trigger on insert
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. is_active trigger on insert ---'
-- Insert a currently active rule
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
VALUES (
'11111111-1111-1111-1111-111111111111',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'write',
'active_documents',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000004', -- employee
'audit', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
'2020-01-01 00:00:00+00', -- Started in the past
'2030-12-31 23:59:59+00' -- Ends in the future
now() - interval '1 day',
now() + interval '1 day'
);
\echo 'Alice (employee) access to temporal-constrained documents:'
SELECT
'write to temp_documents (expired rule)' as action,
morbac.is_allowed(
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
'11111111-1111-1111-1111-111111111111'::uuid,
'write',
'temp_documents'
) as allowed
UNION ALL
SELECT
'write to future_documents (not yet active)' as action,
morbac.is_allowed(
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
'11111111-1111-1111-1111-111111111111'::uuid,
'write',
'future_documents'
) as allowed
UNION ALL
SELECT
'write to active_documents (currently valid)' as action,
morbac.is_allowed(
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
'11111111-1111-1111-1111-111111111111'::uuid,
'write',
'active_documents'
) as allowed;
-- is_active should be TRUE for the active rule
SELECT morbac.t('is_active = TRUE for rule with active time window',
(SELECT is_active FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view'),
TRUE);
-- Update to expired window, trigger should recalculate is_active
UPDATE morbac.rules
SET valid_until = now() - interval '1 second'
WHERE activity = 'audit' AND view = 'temp_view';
-- is_active should be FALSE after expiry update
SELECT morbac.t('is_active = FALSE after valid_until set to past',
(SELECT is_active FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view'),
FALSE);
-- Clean up
DELETE FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view';
-- ---------------------------------------------------------------------------
-- Section 3: Expired rule — valid_until in the past
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. Expired rule (valid_until in the past) ---'
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until)
VALUES (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000004', -- employee
'export', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
'2020-01-01 00:00:00+00' -- expired years ago
);
-- Rule expired, no valid permission
SELECT morbac.t('Dave (employee) exports temp_view via expired rule [denied]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'export', 'temp_view'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 4: Future rule — valid_from in the future
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. Future rule (valid_from in the future) ---'
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from)
VALUES (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000004', -- employee
'manage', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
'2099-01-01 00:00:00+00' -- won't start for decades
);
-- Rule not yet active
SELECT morbac.t('Dave (employee) manages temp_view via future rule [not yet active]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'manage', 'temp_view'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 5: Active time window (valid_from past, valid_until future)
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Active time window ---'
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
VALUES (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000004', -- employee
'read', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
'2020-01-01 00:00:00+00',
'2099-12-31 23:59:59+00'
);
-- Rule is within its validity window
SELECT morbac.t('Dave (employee) reads temp_view via active window rule',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'temp_view'
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 6: Multiple rules for same combination, only active ones count
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 6. Multiple rules for same combination, only active ones count ---'
-- At this point, employee has expired export rule (section 3) and active read rule (section 5).
-- Since export->read in activity hierarchy, the active read rule grants export access.
SELECT morbac.t('Dave exports temp_view (expired export + active read via export->read hierarchy)',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'export', 'temp_view'
), TRUE);
-- Activate the expired export rule (unique constraint prevents a second rule with same modality)
UPDATE morbac.rules
SET valid_from = now() - interval '1 hour',
valid_until = now() + interval '1 hour'
WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND role_id = '20000000-0001-0000-0000-000000000004'
AND activity = 'export' AND view = 'temp_view' AND modality = 'permission';
-- Now export rule is active directly (not just via read hierarchy)
SELECT morbac.t('Dave exports temp_view (export rule now active)',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'export', 'temp_view'
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 7: Temporal prohibition — expired prohibition no longer blocks
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. Expired prohibition no longer blocks ---'
-- Add an active permission for intern -> read temp_view
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
VALUES (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000005', -- intern
'read', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'
);
-- Add an EXPIRED prohibition for intern -> read temp_view
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until)
VALUES (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000005', -- intern
'read', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'prohibition',
'2020-01-01 00:00:00+00' -- prohibition expired years ago
);
-- Prohibition expired, permission stands
SELECT morbac.t('Eve (intern) reads temp_view [active permission, expired prohibition]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'temp_view'
), TRUE);
-- Now replace the prohibition with an active one
UPDATE morbac.rules
SET valid_until = now() + interval '1 day'
WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND role_id = '20000000-0001-0000-0000-000000000005'
AND activity = 'read' AND view = 'temp_view' AND modality = 'prohibition';
-- Active prohibition now blocks
SELECT morbac.t('Eve (intern) reads temp_view [active prohibition blocks]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'temp_view'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 8: Temporal rule with role hierarchy
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 8. Temporal rule with role hierarchy ---'
-- Expire the intern read temp_view prohibition (activated in section 7) so it does not
-- block Carol (manager) and Alice (CEO) who inherit from intern via role hierarchy.
UPDATE morbac.rules
SET valid_until = now() - interval '1 second'
WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND role_id = '20000000-0001-0000-0000-000000000005'
AND activity = 'read' AND view = 'temp_view' AND modality = 'prohibition';
-- Add an active 'approve temp_view' permission for manager.
-- 'approve' is NOT in the activity hierarchy (manage->delete->write->read, export->read),
-- so employee's 'read temp_view' rule does NOT cover 'approve temp_view'.
-- This ensures the temporal test cleanly verifies expiry without hierarchy interference.
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
VALUES (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000003', -- manager
'approve', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
now() - interval '1 hour',
now() + interval '1 hour'
);
-- Carol (manager) can approve temp_view directly
SELECT morbac.t('Carol (manager) approves temp_view [temporal rule, active]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'approve', 'temp_view'
), TRUE);
-- Alice (CEO) inherits from manager — should also get the temporal permission
SELECT morbac.t('Alice (CEO, inherits manager) approves temp_view [temporal rule, active]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'approve', 'temp_view'
), TRUE);
-- Expire the manager approve rule
UPDATE morbac.rules
SET valid_until = now() - interval '1 second'
WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND role_id = '20000000-0001-0000-0000-000000000003'
AND activity = 'approve' AND view = 'temp_view';
-- Rule expired for both manager and CEO
SELECT morbac.t('Carol (manager) approves temp_view after rule expiry [denied]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'approve', 'temp_view'
), FALSE);
SELECT morbac.t('Alice (CEO, inherits manager) approves temp_view after rule expiry [denied]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'approve', 'temp_view'
), FALSE);
\echo ''
\echo '=== Temporal Constraint Tests Completed ==='