Files
pgmorbac/tests/12_user_rules.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

270 lines
11 KiB
SQL

-- =============================================================================
-- User Rules Tests
-- =============================================================================
-- Tests direct user-level rules via morbac.user_rules.
--
-- Scenarios:
-- 1. No user rule: Karl (no role) is denied by default
-- 2. Direct user permission: Karl gets access without any role assignment
-- 3. User rule covers activity/view hierarchy
-- 4. User-level prohibition overrides a role-based permission
-- 5. Priority: user permission with higher priority overrides user prohibition
-- 6. Temporal user rules (valid_from / valid_until)
-- 7. rls_check user filter: morbac.target_user_id scopes rows to a specific user
--
-- Prerequisites: 00_setup.sql -> 11_scope_rules.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '12 - USER RULES'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: No user rule - Karl (no role) is denied
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. No user rule: access denied ---'
SELECT morbac.t('Karl (no role) reads GlobalTech documents [no user rule]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'documents'
), FALSE);
SELECT morbac.t('Karl (no role) reads GlobalTech financial_data [no user rule]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'financial_data'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 2: Direct user permission - Karl gets access without a role
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Direct user permission ---'
INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality)
VALUES (
'30000000-0000-0000-0000-000000000011', -- Karl
'10000000-0000-0000-0000-000000000001', -- GlobalTech HQ
'read', 'documents',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'
);
SELECT morbac.t('Karl (no role) reads GlobalTech documents [user rule grants access]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'documents'
), TRUE);
-- Rule only covers GlobalTech, not Engineering
SELECT morbac.t('Karl reads Engineering documents [no user rule for Engineering]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'read', 'documents'
), FALSE);
-- financial_data is a subtype of documents (view hierarchy), so the documents rule covers it
SELECT morbac.t('Karl reads GlobalTech financial_data [documents rule covers it via view hierarchy]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'financial_data'
), TRUE);
-- contracts has no hierarchy relationship - documents rule does not cover it
SELECT morbac.t('Karl reads GlobalTech contracts [no user rule, no hierarchy coverage]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'contracts'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 3: User rule + activity/view hierarchy
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. User rule with activity/view hierarchy ---'
-- Add a user rule for 'read reports' (parent of financial_data via view hierarchy)
INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality)
VALUES (
'30000000-0000-0000-0000-000000000011', -- Karl
'10000000-0000-0000-0000-000000000001', -- GlobalTech HQ
'read', 'reports',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'
);
-- get_effective_activities('write') includes 'read', so read permission covers write requests
SELECT morbac.t('Karl writes GlobalTech documents [user rule read covers write via activity hierarchy]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'write', 'documents'
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 4: User-level prohibition overrides role-based permission
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. User prohibition overrides role permission ---'
-- Eve (intern) can read public_data via role
SELECT morbac.t('Eve (intern) reads GlobalTech public_data [role permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'public_data'
), TRUE);
-- Add a user prohibition for Eve on public_data
INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality)
VALUES (
'30000000-0000-0000-0000-000000000005', -- Eve
'10000000-0000-0000-0000-000000000001', -- GlobalTech HQ
'read', 'public_data',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'prohibition'
);
SELECT morbac.t('Eve (intern) reads GlobalTech public_data [user prohibition blocks role permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'public_data'
), FALSE);
-- Dave (employee) is unaffected - only Eve has the prohibition
SELECT morbac.t('Dave (employee) reads GlobalTech public_data [no user prohibition]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'public_data'
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 5: Priority - higher-priority user permission overrides prohibition
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Priority: user permission overrides user prohibition ---'
-- Eve has a prohibition (priority 0) on public_data; add a higher-priority permission
INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality, priority)
VALUES (
'30000000-0000-0000-0000-000000000005', -- Eve
'10000000-0000-0000-0000-000000000001', -- GlobalTech HQ
'read', 'public_data',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
10
);
SELECT morbac.t('Eve reads GlobalTech public_data [priority-10 user permission beats priority-0 prohibition]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'public_data'
), TRUE);
-- Clean up the priority override for the temporal test
DELETE FROM morbac.user_rules
WHERE user_id = '30000000-0000-0000-0000-000000000005'
AND modality = 'permission'
AND priority = 10;
-- ---------------------------------------------------------------------------
-- Section 6: Temporal user rules
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 6. Temporal user rules ---'
-- Use contracts: Karl has no other rules covering it, so expiry is conclusive
INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality, valid_from, valid_until)
VALUES (
'30000000-0000-0000-0000-000000000011', -- Karl
'10000000-0000-0000-0000-000000000001', -- GlobalTech HQ
'read', 'contracts',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission',
now() - interval '1 hour',
now() + interval '1 day'
);
SELECT morbac.t('Karl reads GlobalTech contracts [temporal user rule, active]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'contracts'
), TRUE);
-- Expire the rule
UPDATE morbac.user_rules
SET valid_until = now() - interval '1 second'
WHERE user_id = '30000000-0000-0000-0000-000000000011'
AND org_id = '10000000-0000-0000-0000-000000000001'
AND activity = 'read' AND view = 'contracts'
AND modality = 'permission';
SELECT morbac.t('Karl reads GlobalTech contracts [temporal user rule, expired]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'contracts'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 7: rls_check user filter via morbac.target_user_id
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. rls_check user filter ---'
SET morbac.user_id = '30000000-0000-0000-0000-000000000004'; -- Dave (employee)
SET morbac.org_id = '10000000-0000-0000-0000-000000000001'; -- GlobalTech HQ
-- No target_user_id set: row with any user_id passes the user filter
SELECT morbac.t('rls_check passes without target_user_id filter',
morbac.rls_check(
'read', 'documents',
'10000000-0000-0000-0000-000000000001'::uuid,
'30000000-0000-0000-0000-000000000004'::uuid
), TRUE);
-- Set target_user_id to Dave - rows belonging to Dave pass
SET morbac.target_user_id = '30000000-0000-0000-0000-000000000004';
SELECT morbac.t('rls_check passes when row user_id matches target_user_id',
morbac.rls_check(
'read', 'documents',
'10000000-0000-0000-0000-000000000001'::uuid,
'30000000-0000-0000-0000-000000000004'::uuid
), TRUE);
-- Row belonging to Alice is filtered out
SELECT morbac.t('rls_check blocked when row user_id differs from target_user_id',
morbac.rls_check(
'read', 'documents',
'10000000-0000-0000-0000-000000000001'::uuid,
'30000000-0000-0000-0000-000000000001'::uuid
), FALSE);
-- No p_row_user_id passed - user filter does not apply
SELECT morbac.t('rls_check passes when no row user_id passed (filter skipped)',
morbac.rls_check(
'read', 'documents',
'10000000-0000-0000-0000-000000000001'::uuid
), TRUE);
RESET morbac.target_user_id;
RESET morbac.user_id;
RESET morbac.org_id;
\echo ''
\echo '=== User Rules Tests Completed ==='