270 lines
11 KiB
SQL
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 ==='
|