feat(rules): add rules for users

This commit is contained in:
2026-04-01 23:18:01 +02:00
parent 5ef8bae1b4
commit f64cd73159
8 changed files with 437 additions and 13 deletions
+269
View File
@@ -0,0 +1,269 @@
-- =============================================================================
-- 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 ==='