Files
pgmorbac/tests/05_temporal.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

330 lines
14 KiB
SQL

-- =============================================================================
-- Temporal Constraint Tests
-- =============================================================================
-- Tests time-bounded rules using valid_from and valid_until on the rules table.
--
-- 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 '================================================================'
\echo '05 - TEMPORAL CONSTRAINTS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Setup: dedicated view for temporal tests
-- ---------------------------------------------------------------------------
INSERT INTO morbac.views (name, description) VALUES
('temp_view', 'Scratch view for temporal tests');
-- ---------------------------------------------------------------------------
-- Section 1: is_rule_valid() helper function
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. is_rule_valid() helper ---'
-- 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 (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000004', -- employee
'audit', 'temp_view',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
now() - interval '1 day',
now() + interval '1 day'
);
-- 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 ==='