-- ============================================================================= -- Constraint Tests -- ============================================================================= -- Tests business constraints: -- 1. Separation of Duty (SoD) — mutually exclusive roles -- 2. Negative role assignments — explicit blocking of a role -- 3. Role cardinality constraints — min/max users per role -- -- Scenario: -- - auditor and accountant are mutually exclusive (no one can hold both) -- - Heidi (auditor) cannot be assigned accountant role -- - Ivan (accountant) cannot be assigned auditor role -- - Frank (contractor) is explicitly blocked from the employee role -- - compliance_officer role has a min 1, max 2 cardinality constraint -- -- Prerequisites: 00_setup.sql -> 03_delegation.sql -- ============================================================================= \echo '' \echo '================================================================' \echo '04 — CONSTRAINTS' \echo '================================================================' -- --------------------------------------------------------------------------- -- Section 1: Separation of Duty — define conflict -- --------------------------------------------------------------------------- \echo '' \echo '--- 1. Separation of Duty setup ---' INSERT INTO morbac.sod_conflicts (role_a_id, role_b_id, org_id, description) VALUES ( '20000000-0001-0000-0000-000000000006', -- auditor '20000000-0001-0000-0000-000000000007', -- accountant '10000000-0000-0000-0000-000000000001', 'Auditors and accountants are mutually exclusive to ensure financial independence' ); -- Verify the conflict was created SELECT morbac.t_eq('SoD conflict between auditor and accountant created', (SELECT COUNT(*) FROM morbac.sod_conflicts WHERE role_a_id = '20000000-0001-0000-0000-000000000006' AND role_b_id = '20000000-0001-0000-0000-000000000007')::bigint, 1); -- --------------------------------------------------------------------------- -- Section 2: SoD violation detection -- --------------------------------------------------------------------------- \echo '' \echo '--- 2. SoD violation detection ---' -- Heidi (auditor) — check if assigning accountant role would violate SoD SELECT morbac.t('Assigning accountant to Heidi (auditor) violates SoD', morbac.check_sod_violation( '30000000-0000-0000-0000-000000000008'::uuid, -- Heidi '20000000-0001-0000-0000-000000000007'::uuid, -- accountant role '10000000-0000-0000-0000-000000000001'::uuid ), TRUE); -- Ivan (accountant) — check if assigning auditor role would violate SoD SELECT morbac.t('Assigning auditor to Ivan (accountant) violates SoD [symmetric]', morbac.check_sod_violation( '30000000-0000-0000-0000-000000000009'::uuid, -- Ivan '20000000-0001-0000-0000-000000000006'::uuid, -- auditor role '10000000-0000-0000-0000-000000000001'::uuid ), TRUE); -- Dave (employee) — check if assigning accountant would violate SoD -- Dave is not an auditor — no conflict SELECT morbac.t('Assigning accountant to Dave (not an auditor) does not violate SoD', morbac.check_sod_violation( '30000000-0000-0000-0000-000000000004'::uuid, -- Dave '20000000-0001-0000-0000-000000000007'::uuid, -- accountant role '10000000-0000-0000-0000-000000000001'::uuid ), FALSE); -- Heidi (auditor) — assigning a non-conflicting role (manager) is fine SELECT morbac.t('Assigning manager to Heidi (auditor) does not violate SoD', morbac.check_sod_violation( '30000000-0000-0000-0000-000000000008'::uuid, -- Heidi '20000000-0001-0000-0000-000000000003'::uuid, -- manager role '10000000-0000-0000-0000-000000000001'::uuid ), FALSE); -- User in a different org has no SoD conflict with GlobalTech roles -- Judy is in Engineering/Sales, not GlobalTech SELECT morbac.t('Assigning accountant to Judy (no GlobalTech roles) does not violate SoD', morbac.check_sod_violation( '30000000-0000-0000-0000-000000000010'::uuid, -- Judy '20000000-0001-0000-0000-000000000007'::uuid, -- accountant role '10000000-0000-0000-0000-000000000001'::uuid ), FALSE); -- --------------------------------------------------------------------------- -- Section 3: Negative role assignments -- --------------------------------------------------------------------------- \echo '' \echo '--- 3. Negative role assignments ---' -- Baseline: Frank (contractor) has read documents permission SELECT morbac.t('Frank (contractor) reads documents [baseline before negative assignment]', morbac.is_allowed_nocache( '30000000-0000-0000-0000-000000000006'::uuid, '10000000-0000-0000-0000-000000000001'::uuid, 'read', 'documents' ), TRUE); -- Add employee role directly to Frank (unusual but valid) INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES ('30000000-0000-0000-0000-000000000006', '20000000-0001-0000-0000-000000000004', '10000000-0000-0000-0000-000000000001'); -- Frank now has both contractor and employee roles; employee grants write documents SELECT morbac.t('Frank (now also employee) writes documents [employee permission]', morbac.is_allowed_nocache( '30000000-0000-0000-0000-000000000006'::uuid, '10000000-0000-0000-0000-000000000001'::uuid, 'write', 'documents' ), TRUE); -- Add negative assignment blocking Frank from the employee role INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason) VALUES ( '30000000-0000-0000-0000-000000000006', -- Frank '20000000-0001-0000-0000-000000000004', -- employee role '10000000-0000-0000-0000-000000000001', 'Frank is a contractor and must not gain employee-level access' ); -- Frank's employee role is negated — verify via get_comprehensive_roles -- employee role must not appear (negated by negative assignment) SELECT morbac.t('Frank: employee role excluded by negative assignment', NOT EXISTS( SELECT 1 FROM morbac.get_comprehensive_roles( '30000000-0000-0000-0000-000000000006'::uuid, '10000000-0000-0000-0000-000000000001'::uuid ) WHERE role_id = '20000000-0001-0000-0000-000000000004' ), TRUE); -- Frank's contractor role still appears (only employee is negated) SELECT morbac.t('Frank: contractor role still present after employee negated', EXISTS( SELECT 1 FROM morbac.get_comprehensive_roles( '30000000-0000-0000-0000-000000000006'::uuid, '10000000-0000-0000-0000-000000000001'::uuid ) WHERE role_id = '20000000-0001-0000-0000-000000000009' ), TRUE); -- Negative assignment on a role the user never had is harmless -- Karl has no role — adding negative assignment for manager is a no-op INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason) VALUES ( '30000000-0000-0000-0000-000000000011', -- Karl '20000000-0001-0000-0000-000000000003', -- manager role (Karl never had this) '10000000-0000-0000-0000-000000000001', 'Preemptive block' ); -- Karl still has no access (negative assignment on non-held role is harmless) SELECT morbac.t('Karl (no role, preemptive negative assignment) reads documents [still no access]', morbac.is_allowed_nocache( '30000000-0000-0000-0000-000000000011'::uuid, '10000000-0000-0000-0000-000000000001'::uuid, 'read', 'documents' ), FALSE); -- Clean up Frank's extra employee assignment and negative assignment for later tests DELETE FROM morbac.user_roles WHERE user_id = '30000000-0000-0000-0000-000000000006' AND role_id = '20000000-0001-0000-0000-000000000004' AND org_id = '10000000-0000-0000-0000-000000000001'; DELETE FROM morbac.negative_role_assignments WHERE user_id = '30000000-0000-0000-0000-000000000006' AND role_id = '20000000-0001-0000-0000-000000000004'; -- --------------------------------------------------------------------------- -- Section 4: Role cardinality constraints -- --------------------------------------------------------------------------- \echo '' \echo '--- 4. Role cardinality constraints ---' -- Set a cardinality constraint: compliance_officer role — min 1, max 2 INSERT INTO morbac.role_cardinality (role_id, min_users, max_users, description) VALUES ( '20000000-0001-0000-0000-000000000010', -- compliance_officer 1, 2, 'Compliance officer role: at least 1, at most 2' ); -- Currently 0 users have compliance_officer — adding one should be fine (0 < max=2) SELECT morbac.t_null('Adding first compliance_officer (0 users, max=2) — no violation', morbac.check_cardinality_violation( '20000000-0001-0000-0000-000000000010'::uuid, TRUE -- adding )); -- Assign two users as compliance officer INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES ('30000000-0000-0000-0000-000000000004', '20000000-0001-0000-0000-000000000010', '10000000-0000-0000-0000-000000000001'), ('30000000-0000-0000-0000-000000000005', '20000000-0001-0000-0000-000000000010', '10000000-0000-0000-0000-000000000001'); -- Now 2 users — at max. Trying to add a 3rd should violate SELECT morbac.t_not_null('Adding 3rd compliance_officer (2 users, max=2) — violation returned', morbac.check_cardinality_violation( '20000000-0001-0000-0000-000000000010'::uuid, TRUE -- adding )); -- Removing one — 2 users, min=1 — removing leaves 1 which is ≥ min=1, should be fine SELECT morbac.t_null('Removing from 2 compliance_officers (min=1) — no violation (still above min)', morbac.check_cardinality_violation( '20000000-0001-0000-0000-000000000010'::uuid, FALSE -- removing )); -- Remove one user DELETE FROM morbac.user_roles WHERE user_id = '30000000-0000-0000-0000-000000000005' AND role_id = '20000000-0001-0000-0000-000000000010'; -- 1 user remaining = min. Removing the last one would violate min=1 SELECT morbac.t_not_null('Removing last compliance_officer (1 user, min=1) — violation returned', morbac.check_cardinality_violation( '20000000-0001-0000-0000-000000000010'::uuid, FALSE -- removing )); -- Adding again after being at 1 — 1 user, max=2 — ok SELECT morbac.t_null('Adding when at 1 compliance_officer (max=2) — no violation', morbac.check_cardinality_violation( '20000000-0001-0000-0000-000000000010'::uuid, TRUE -- adding )); -- Role with no cardinality constraint — no violation for any operation SELECT morbac.t_null('Checking cardinality for employee role (no constraint) — no violation', morbac.check_cardinality_violation( '20000000-0001-0000-0000-000000000004'::uuid, -- employee TRUE )); -- Clean up extra compliance_officer assignments DELETE FROM morbac.user_roles WHERE role_id = '20000000-0001-0000-0000-000000000010' AND user_id = '30000000-0000-0000-0000-000000000004'; \echo '' \echo '=== Constraint Tests Completed ==='