cc3d65a013
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>
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 ==='
|