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>
This commit is contained in:
+23
-23
@@ -1,11 +1,11 @@
|
||||
-- =============================================================================
|
||||
-- Test Setup — GlobalTech Inc. Company Scenario
|
||||
-- Test Setup - GlobalTech Inc. Company Scenario
|
||||
-- =============================================================================
|
||||
-- This file establishes the full company structure used across all test files:
|
||||
--
|
||||
-- GlobalTech HQ (root)
|
||||
-- ├── Engineering Dept (child)
|
||||
-- └── Sales Dept (child)
|
||||
-- +-- Engineering Dept (child)
|
||||
-- \-- Sales Dept (child)
|
||||
--
|
||||
-- Role hierarchy in GlobalTech (senior -> junior, i.e. senior inherits junior perms):
|
||||
-- ceo -> director -> manager -> employee -> intern
|
||||
@@ -15,18 +15,18 @@
|
||||
-- tech_lead -> engineer
|
||||
--
|
||||
-- Users:
|
||||
-- Alice — CEO at GlobalTech
|
||||
-- Bob — Director at GlobalTech
|
||||
-- Carol — Manager at GlobalTech
|
||||
-- Dave — Employee at GlobalTech
|
||||
-- Eve — Intern at GlobalTech
|
||||
-- Frank — Contractor at GlobalTech
|
||||
-- Grace — HR Manager at GlobalTech
|
||||
-- Heidi — Auditor at GlobalTech
|
||||
-- Ivan — Accountant at GlobalTech
|
||||
-- Judy — Engineer at Engineering + Sales Rep at Sales (multi-org)
|
||||
-- Karl — No role (unauthorized user)
|
||||
-- Leo — Employee at GlobalTech (used for delegation target)
|
||||
-- Alice - CEO at GlobalTech
|
||||
-- Bob - Director at GlobalTech
|
||||
-- Carol - Manager at GlobalTech
|
||||
-- Dave - Employee at GlobalTech
|
||||
-- Eve - Intern at GlobalTech
|
||||
-- Frank - Contractor at GlobalTech
|
||||
-- Grace - HR Manager at GlobalTech
|
||||
-- Heidi - Auditor at GlobalTech
|
||||
-- Ivan - Accountant at GlobalTech
|
||||
-- Judy - Engineer at Engineering + Sales Rep at Sales (multi-org)
|
||||
-- Karl - No role (unauthorized user)
|
||||
-- Leo - Employee at GlobalTech (used for delegation target)
|
||||
-- =============================================================================
|
||||
|
||||
-- Clean slate
|
||||
@@ -55,7 +55,7 @@ WHERE id IN (
|
||||
'10000000-0000-0000-0000-000000000003'
|
||||
);
|
||||
|
||||
SELECT o.name, COALESCE(p.name, '—') AS parent
|
||||
SELECT o.name, COALESCE(p.name, ' - ') AS parent
|
||||
FROM morbac.orgs o LEFT JOIN morbac.orgs p ON o.parent_id = p.id
|
||||
ORDER BY o.parent_id NULLS FIRST, o.name;
|
||||
|
||||
@@ -122,7 +122,7 @@ JOIN morbac.roles jr ON jr.id = rh.junior_role_id
|
||||
ORDER BY sr.name;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- USER — ROLE ASSIGNMENTS
|
||||
-- USER - ROLE ASSIGNMENTS
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: User-Role Assignments ==='
|
||||
@@ -155,7 +155,7 @@ INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('30000000-0000-0000-0000-000000000010', '20000000-0002-0000-0000-000000000002', '10000000-0000-0000-0000-000000000002'),
|
||||
('30000000-0000-0000-0000-000000000010', '20000000-0003-0000-0000-000000000002', '10000000-0000-0000-0000-000000000003');
|
||||
|
||||
-- Karl: no role (unauthorized user — intentionally not assigned any role)
|
||||
-- Karl: no role (unauthorized user - intentionally not assigned any role)
|
||||
|
||||
SELECT ur.user_id, r.name AS role, o.name AS organization
|
||||
FROM morbac.user_roles ur
|
||||
@@ -214,7 +214,7 @@ BEGIN RETURN TRUE; END;
|
||||
$$;
|
||||
|
||||
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||
('business_hours', 'During business hours (Mon–Fri 9–17)', 'morbac.ctx_business_hours'::regproc),
|
||||
('business_hours', 'During business hours (Mon-Fri 9-17)', 'morbac.ctx_business_hours'::regproc),
|
||||
('after_hours', 'Outside business hours', 'morbac.ctx_after_hours'::regproc),
|
||||
('end_of_quarter', 'End-of-quarter reporting window', 'morbac.ctx_end_of_quarter'::regproc);
|
||||
|
||||
@@ -274,7 +274,7 @@ JOIN morbac.contexts c ON c.name = v.context_name;
|
||||
-- Disable auth cache so state changes between calls are always reflected
|
||||
SELECT morbac.set_config('cache_ttl_seconds', '0');
|
||||
|
||||
-- morbac.t(label, actual, expected) — boolean assertion
|
||||
-- morbac.t(label, actual, expected) - boolean assertion
|
||||
CREATE OR REPLACE FUNCTION morbac.t(label TEXT, actual BOOLEAN, expect BOOLEAN)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual IS NOT DISTINCT FROM expect
|
||||
@@ -284,7 +284,7 @@ RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- morbac.t_null(label, actual) — assert value is NULL
|
||||
-- morbac.t_null(label, actual) - assert value is NULL
|
||||
CREATE OR REPLACE FUNCTION morbac.t_null(label TEXT, actual TEXT)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual IS NULL
|
||||
@@ -293,7 +293,7 @@ RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- morbac.t_not_null(label, actual) — assert value is NOT NULL
|
||||
-- morbac.t_not_null(label, actual) - assert value is NOT NULL
|
||||
CREATE OR REPLACE FUNCTION morbac.t_not_null(label TEXT, actual TEXT)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual IS NOT NULL
|
||||
@@ -302,7 +302,7 @@ RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- morbac.t_eq(label, actual, expected) — assert two numeric values are equal
|
||||
-- morbac.t_eq(label, actual, expected) - assert two numeric values are equal
|
||||
CREATE OR REPLACE FUNCTION morbac.t_eq(label TEXT, actual NUMERIC, expect NUMERIC)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual = expect
|
||||
|
||||
+40
-40
@@ -5,25 +5,25 @@
|
||||
-- No activity or view hierarchy is active yet (added in 02_hierarchies.sql).
|
||||
--
|
||||
-- Users and their roles at GlobalTech HQ:
|
||||
-- Alice — ceo (permission: inherits all via hierarchy)
|
||||
-- Bob — director (inherits manager -> employee -> intern)
|
||||
-- Carol — manager (inherits employee -> intern)
|
||||
-- Dave — employee (inherits intern)
|
||||
-- Eve — intern
|
||||
-- Frank — contractor (has prohibition on financial/hr data)
|
||||
-- Grace — hr_manager
|
||||
-- Heidi — auditor
|
||||
-- Ivan — accountant
|
||||
-- Judy — engineer@Engineering + sales_rep@Sales (multi-org)
|
||||
-- Karl — no role
|
||||
-- Leo — employee
|
||||
-- Alice - ceo (permission: inherits all via hierarchy)
|
||||
-- Bob - director (inherits manager -> employee -> intern)
|
||||
-- Carol - manager (inherits employee -> intern)
|
||||
-- Dave - employee (inherits intern)
|
||||
-- Eve - intern
|
||||
-- Frank - contractor (has prohibition on financial/hr data)
|
||||
-- Grace - hr_manager
|
||||
-- Heidi - auditor
|
||||
-- Ivan - accountant
|
||||
-- Judy - engineer@Engineering + sales_rep@Sales (multi-org)
|
||||
-- Karl - no role
|
||||
-- Leo - employee
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '01 — CORE AUTHORIZATION DECISIONS'
|
||||
\echo '01 - CORE AUTHORIZATION DECISIONS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -32,7 +32,7 @@
|
||||
\echo ''
|
||||
\echo '--- 1. Basic permission grants ---'
|
||||
|
||||
-- Dave (employee) can read documents — has explicit permission
|
||||
-- Dave (employee) can read documents - has explicit permission
|
||||
SELECT morbac.t('Dave (employee) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -40,7 +40,7 @@ SELECT morbac.t('Dave (employee) reads documents',
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) can write documents — context is business_hours (evaluates TRUE)
|
||||
-- Dave (employee) can write documents - context is business_hours (evaluates TRUE)
|
||||
SELECT morbac.t('Dave (employee) writes documents [business_hours context=true]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -89,12 +89,12 @@ SELECT morbac.t('Ivan (accountant) writes financial_data',
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Default deny — no rule exists for the combination
|
||||
-- Section 2: Default deny - no rule exists for the combination
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Default deny (no permission rule) ---'
|
||||
|
||||
-- Dave (employee) cannot delete documents — no delete permission for employee
|
||||
-- Dave (employee) cannot delete documents - no delete permission for employee
|
||||
SELECT morbac.t('Dave (employee) deletes documents [no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -102,7 +102,7 @@ SELECT morbac.t('Dave (employee) deletes documents [no permission]',
|
||||
'delete', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Eve (intern) cannot read documents — intern only has public_data permission
|
||||
-- Eve (intern) cannot read documents - intern only has public_data permission
|
||||
SELECT morbac.t('Eve (intern) reads documents [intern has no docs permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
@@ -110,7 +110,7 @@ SELECT morbac.t('Eve (intern) reads documents [intern has no docs permission]',
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Dave (employee) cannot read financial_data — no rule for employee -> financial_data
|
||||
-- Dave (employee) cannot read financial_data - no rule for employee -> financial_data
|
||||
SELECT morbac.t('Dave (employee) reads financial_data [no permission before view hierarchy]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -118,7 +118,7 @@ SELECT morbac.t('Dave (employee) reads financial_data [no permission before view
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- Grace (hr_manager) cannot read audit_logs — no rule for hr_manager -> audit_logs
|
||||
-- Grace (hr_manager) cannot read audit_logs - no rule for hr_manager -> audit_logs
|
||||
SELECT morbac.t('Grace (hr_manager) reads audit_logs [no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000007'::uuid,
|
||||
@@ -126,7 +126,7 @@ SELECT morbac.t('Grace (hr_manager) reads audit_logs [no permission]',
|
||||
'read', 'audit_logs'
|
||||
), FALSE);
|
||||
|
||||
-- Dave (employee) cannot approve documents — no approve permission for employee
|
||||
-- Dave (employee) cannot approve documents - no approve permission for employee
|
||||
SELECT morbac.t('Dave (employee) approves documents [no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -135,12 +135,12 @@ SELECT morbac.t('Dave (employee) approves documents [no permission]',
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Default deny — user has no role at all
|
||||
-- Section 3: Default deny - user has no role at all
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Default deny (user has no role) ---'
|
||||
|
||||
-- Karl has no role anywhere — all actions denied
|
||||
-- Karl has no role anywhere - all actions denied
|
||||
SELECT morbac.t('Karl (no role) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
@@ -148,7 +148,7 @@ SELECT morbac.t('Karl (no role) reads documents',
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Completely unknown user UUID — EXPECT FALSE
|
||||
-- Completely unknown user UUID - EXPECT FALSE
|
||||
SELECT morbac.t('Unknown user reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid,
|
||||
@@ -162,7 +162,7 @@ SELECT morbac.t('Unknown user reads documents',
|
||||
\echo ''
|
||||
\echo '--- 4. Prohibition overrides permission ---'
|
||||
|
||||
-- Frank (contractor) reads documents — permission granted, no prohibition
|
||||
-- Frank (contractor) reads documents - permission granted, no prohibition
|
||||
SELECT morbac.t('Frank (contractor) reads documents [has permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
@@ -170,7 +170,7 @@ SELECT morbac.t('Frank (contractor) reads documents [has permission]',
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Frank (contractor) reads financial_data — PROHIBITED
|
||||
-- Frank (contractor) reads financial_data - PROHIBITED
|
||||
SELECT morbac.t('Frank (contractor) reads financial_data [prohibited]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
@@ -178,7 +178,7 @@ SELECT morbac.t('Frank (contractor) reads financial_data [prohibited]',
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- Frank (contractor) reads hr_data — prohibited
|
||||
-- Frank (contractor) reads hr_data - prohibited
|
||||
SELECT morbac.t('Frank (contractor) reads hr_data [prohibited]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
@@ -211,7 +211,7 @@ WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND activity = 'read' AND view = 'financial_data' AND modality = 'permission';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: Context filtering — rule only applies when context is TRUE
|
||||
-- Section 5: Context filtering - rule only applies when context is TRUE
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Context filtering ---'
|
||||
@@ -256,7 +256,7 @@ WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND activity = 'export' AND view = 'documents';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: Wrong organization — user has no role in the target org
|
||||
-- Section 6: Wrong organization - user has no role in the target org
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. Wrong organization ---'
|
||||
@@ -278,12 +278,12 @@ SELECT morbac.t('Judy (Engineering engineer) reads GlobalTech financial_data [no
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Multi-organization user — access scoped to each org independently
|
||||
-- Section 7: Multi-organization user - access scoped to each org independently
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 7. Multi-org user (Judy) ---'
|
||||
|
||||
-- Judy is engineer at Engineering — can read documents there
|
||||
-- Judy is engineer at Engineering - can read documents there
|
||||
SELECT morbac.t('Judy (engineer) reads Engineering documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
@@ -291,7 +291,7 @@ SELECT morbac.t('Judy (engineer) reads Engineering documents',
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Judy is sales_rep at Sales — can write contracts there
|
||||
-- Judy is sales_rep at Sales - can write contracts there
|
||||
SELECT morbac.t('Judy (sales_rep) writes Sales contracts',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
@@ -313,7 +313,7 @@ SELECT morbac.t('Judy writes documents at Sales [engineer perms dont carry over]
|
||||
\echo ''
|
||||
\echo '--- 8. Role hierarchy inheritance ---'
|
||||
|
||||
-- Carol (manager) inherits employee permissions — can read documents (employee perm)
|
||||
-- Carol (manager) inherits employee permissions - can read documents (employee perm)
|
||||
SELECT morbac.t('Carol (manager, inherits employee) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
@@ -321,7 +321,7 @@ SELECT morbac.t('Carol (manager, inherits employee) reads documents',
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Carol (manager) has own permission — can approve documents
|
||||
-- Carol (manager) has own permission - can approve documents
|
||||
SELECT morbac.t('Carol (manager) approves documents [own permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
@@ -329,7 +329,7 @@ SELECT morbac.t('Carol (manager) approves documents [own permission]',
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Bob (director) inherits manager -> employee chain — can read documents
|
||||
-- Bob (director) inherits manager -> employee chain - can read documents
|
||||
SELECT morbac.t('Bob (director, inherits manager+employee) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000002'::uuid,
|
||||
@@ -337,7 +337,7 @@ SELECT morbac.t('Bob (director, inherits manager+employee) reads documents',
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Bob (director) inherits manager — can approve documents
|
||||
-- Bob (director) inherits manager - can approve documents
|
||||
SELECT morbac.t('Bob (director, inherits manager) approves documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000002'::uuid,
|
||||
@@ -345,7 +345,7 @@ SELECT morbac.t('Bob (director, inherits manager) approves documents',
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Alice (CEO) inherits the entire hierarchy — can do everything below
|
||||
-- Alice (CEO) inherits the entire hierarchy - can do everything below
|
||||
SELECT morbac.t('Alice (CEO, inherits all) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
@@ -367,7 +367,7 @@ SELECT morbac.t('Alice (CEO, inherits all) deletes documents',
|
||||
'delete', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) cannot approve — intern has no approve permission
|
||||
-- Eve (intern) cannot approve - intern has no approve permission
|
||||
SELECT morbac.t('Eve (intern) approves documents [intern has no approve permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
@@ -464,7 +464,7 @@ SELECT morbac.t('Dave reads contracts: prohibition priority=10 beats permission
|
||||
-- Equal priorities: prohibition wins (modality tiebreaker)
|
||||
UPDATE morbac.rules SET priority = 5 WHERE id = 'e0000000-0000-0000-0000-000000000001';
|
||||
|
||||
SELECT morbac.t('Dave reads contracts: equal priority — prohibition wins by modality precedence [denied]',
|
||||
SELECT morbac.t('Dave reads contracts: equal priority - prohibition wins by modality precedence [denied]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
@@ -477,7 +477,7 @@ UPDATE morbac.rules SET priority = NULL WHERE id IN (
|
||||
'e0000000-0000-0000-0000-000000000002'
|
||||
);
|
||||
|
||||
SELECT morbac.t('Dave reads contracts: no priority set — prohibition wins by default [denied]',
|
||||
SELECT morbac.t('Dave reads contracts: no priority set - prohibition wins by default [denied]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
|
||||
+38
-38
@@ -2,10 +2,10 @@
|
||||
-- Hierarchy Tests
|
||||
-- =============================================================================
|
||||
-- Tests all four hierarchy types:
|
||||
-- 1. Role hierarchy — senior roles inherit permissions of junior roles (transitive)
|
||||
-- 2. Activity hierarchy — requesting a senior activity also matches junior-activity rules
|
||||
-- 3. View hierarchy — requesting a senior view also matches junior-view rules
|
||||
-- 4. Org hierarchy — get_org_ancestors / get_org_descendants traversal
|
||||
-- 1. Role hierarchy - senior roles inherit permissions of junior roles (transitive)
|
||||
-- 2. Activity hierarchy - requesting a senior activity also matches junior-activity rules
|
||||
-- 3. View hierarchy - requesting a senior view also matches junior-view rules
|
||||
-- 4. Org hierarchy - get_org_ancestors / get_org_descendants traversal
|
||||
--
|
||||
-- Hierarchy semantics in this system:
|
||||
-- Activity: (senior='write', junior='read') means get_effective_activities('write')
|
||||
@@ -19,50 +19,50 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '02 — HIERARCHIES'
|
||||
\echo '02 - HIERARCHIES'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Role hierarchy — get_effective_roles and get_inherited_roles
|
||||
-- Section 1: Role hierarchy - get_effective_roles and get_inherited_roles
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Role hierarchy introspection ---'
|
||||
|
||||
-- Eve (intern) direct assignment only — 1 effective role
|
||||
-- Eve (intern) direct assignment only - 1 effective role
|
||||
SELECT morbac.t_eq('Eve has 1 effective role (intern only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 1);
|
||||
|
||||
-- Dave (employee) has employee + intern via hierarchy — 2 effective roles
|
||||
-- Dave (employee) has employee + intern via hierarchy - 2 effective roles
|
||||
SELECT morbac.t_eq('Dave has 2 effective roles (employee, intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 2);
|
||||
|
||||
-- Carol (manager) has manager + employee + intern via hierarchy — 3 effective roles
|
||||
-- Carol (manager) has manager + employee + intern via hierarchy - 3 effective roles
|
||||
SELECT morbac.t_eq('Carol has 3 effective roles (manager, employee, intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 3);
|
||||
|
||||
-- Alice (CEO) has ceo, director, manager, employee, intern — 5 effective roles
|
||||
-- Alice (CEO) has ceo, director, manager, employee, intern - 5 effective roles
|
||||
SELECT morbac.t_eq('Alice has 5 effective roles (ceo through intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 5);
|
||||
|
||||
-- get_inherited_roles for manager — manager itself + employee + intern = 3
|
||||
-- get_inherited_roles for manager - manager itself + employee + intern = 3
|
||||
SELECT morbac.t_eq('get_inherited_roles(manager) returns 3 roles (manager, employee, intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_inherited_roles(
|
||||
'20000000-0001-0000-0000-000000000003'::uuid
|
||||
))::bigint, 3);
|
||||
|
||||
-- get_inherited_roles for ceo — entire chain = 5
|
||||
-- get_inherited_roles for ceo - entire chain = 5
|
||||
SELECT morbac.t_eq('get_inherited_roles(ceo) returns 5 roles (ceo through intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_inherited_roles(
|
||||
'20000000-0001-0000-0000-000000000001'::uuid
|
||||
@@ -83,10 +83,10 @@ SELECT morbac.t('intern role appears in manager inherited roles',
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Role hierarchy — authorization via inheritance (transitive)
|
||||
-- Section 2: Role hierarchy - authorization via inheritance (transitive)
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Role hierarchy — authorization via inheritance ---'
|
||||
\echo '--- 2. Role hierarchy - authorization via inheritance ---'
|
||||
|
||||
-- Carol (manager) has employee permission (read documents) via 1-level inheritance
|
||||
SELECT morbac.t('Carol (manager, 1-level inherit) reads documents',
|
||||
@@ -128,7 +128,7 @@ SELECT morbac.t('Carol (manager) approves documents (own perm)',
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) cannot approve — no role above intern has approve perm
|
||||
-- Eve (intern) cannot approve - no role above intern has approve perm
|
||||
SELECT morbac.t('Eve (intern) approves documents [inheritance only goes up in seniority]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
@@ -306,7 +306,7 @@ SELECT morbac.t_eq('get_effective_activities(export) returns 2 (export, read)',
|
||||
-- Rule: employee has 'read documents' permission.
|
||||
-- With hierarchy (write->read), requesting 'write' also matches the 'read' rule.
|
||||
|
||||
-- Dave (employee) requests 'write' — matches 'read' rule via write->read hierarchy
|
||||
-- Dave (employee) requests 'write' - matches 'read' rule via write->read hierarchy
|
||||
SELECT morbac.t('Dave (employee, has read perm) writes docs via activity hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -314,7 +314,7 @@ SELECT morbac.t('Dave (employee, has read perm) writes docs via activity hierarc
|
||||
'write', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) requests 'delete' — delete->write->read chain, 'read' rule matches
|
||||
-- Dave (employee) requests 'delete' - delete->write->read chain, 'read' rule matches
|
||||
SELECT morbac.t('Dave (employee, has read perm) deletes docs via delete->write->read hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -322,7 +322,7 @@ SELECT morbac.t('Dave (employee, has read perm) deletes docs via delete->write->
|
||||
'delete', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) requests 'export' — export->read, 'read' rule matches
|
||||
-- Dave (employee) requests 'export' - export->read, 'read' rule matches
|
||||
SELECT morbac.t('Dave (employee, has read perm) exports docs via export->read hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -330,7 +330,7 @@ SELECT morbac.t('Dave (employee, has read perm) exports docs via export->read hi
|
||||
'export', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) has only 'read public_data' — requesting 'write public_data' also matches
|
||||
-- Eve (intern) has only 'read public_data' - requesting 'write public_data' also matches
|
||||
SELECT morbac.t('Eve (intern, has read perm) writes public_data via activity hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
@@ -338,7 +338,7 @@ SELECT morbac.t('Eve (intern, has read perm) writes public_data via activity hie
|
||||
'write', 'public_data'
|
||||
), TRUE);
|
||||
|
||||
-- 'audit' is not in the hierarchy — no junior, no senior — no match for employee
|
||||
-- 'audit' is not in the hierarchy - no junior, no senior - no match for employee
|
||||
SELECT morbac.t('Dave (employee) audits documents [audit not in hierarchy, no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -385,7 +385,7 @@ SELECT morbac.t('documents is in effective views of financial_data',
|
||||
-- requesting 'financial_data' -> get_effective_views('financial_data') = {financial_data, documents}
|
||||
-- the 'documents' rule matches -> access granted
|
||||
|
||||
-- Dave (employee) reads financial_data — matches employee's 'read documents' rule via view hierarchy
|
||||
-- Dave (employee) reads financial_data - matches employee's 'read documents' rule via view hierarchy
|
||||
SELECT morbac.t('Dave (employee, has read documents) reads financial_data via view hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -393,7 +393,7 @@ SELECT morbac.t('Dave (employee, has read documents) reads financial_data via vi
|
||||
'read', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) reads hr_data — matches 'read documents' via view hierarchy
|
||||
-- Dave (employee) reads hr_data - matches 'read documents' via view hierarchy
|
||||
SELECT morbac.t('Dave (employee, has read documents) reads hr_data via view hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
@@ -401,7 +401,7 @@ SELECT morbac.t('Dave (employee, has read documents) reads hr_data via view hier
|
||||
'read', 'hr_data'
|
||||
), TRUE);
|
||||
|
||||
-- Frank (contractor) reads documents — permitted
|
||||
-- Frank (contractor) reads documents - permitted
|
||||
SELECT morbac.t('Frank (contractor) reads documents [has permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
@@ -409,8 +409,8 @@ SELECT morbac.t('Frank (contractor) reads documents [has permission]',
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Frank (contractor) reads financial_data — matches contractor's 'read documents' rule via view hierarchy
|
||||
-- BUT contractor has a PROHIBITION on financial_data — prohibition wins
|
||||
-- Frank (contractor) reads financial_data - matches contractor's 'read documents' rule via view hierarchy
|
||||
-- BUT contractor has a PROHIBITION on financial_data - prohibition wins
|
||||
SELECT morbac.t('Frank (contractor) reads financial_data [prohibition overrides view-hierarchy match]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
@@ -436,7 +436,7 @@ SELECT morbac.t_eq('Sales Dept has 2 ancestors (self + GlobalTech HQ)',
|
||||
'10000000-0000-0000-0000-000000000003'::uuid
|
||||
))::bigint, 2);
|
||||
|
||||
-- GlobalTech HQ is the root — only 1 ancestor (itself)
|
||||
-- GlobalTech HQ is the root - only 1 ancestor (itself)
|
||||
SELECT morbac.t_eq('GlobalTech HQ (root) has 1 ancestor (itself only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_ancestors(
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
@@ -478,17 +478,17 @@ SELECT morbac.t('Dave (GlobalTech employee) reads Engineering docs [no cross-org
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: get_org_scope — named scope helper
|
||||
-- Section 6: get_org_scope - named scope helper
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Org tree used in tests:
|
||||
-- GlobalTech HQ (root) id: 10000000-0000-0000-0000-000000000001
|
||||
-- ├── Engineering Dept id: 10000000-0000-0000-0000-000000000002
|
||||
-- └── Sales Dept id: 10000000-0000-0000-0000-000000000003
|
||||
-- +-- Engineering Dept id: 10000000-0000-0000-0000-000000000002
|
||||
-- \-- Sales Dept id: 10000000-0000-0000-0000-000000000003
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. get_org_scope ---'
|
||||
|
||||
-- 'self' — always returns exactly the org itself
|
||||
-- 'self' - always returns exactly the org itself
|
||||
SELECT morbac.t_eq('scope self (root) returns 1 row',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'self'))::bigint, 1);
|
||||
|
||||
@@ -501,7 +501,7 @@ SELECT morbac.t('scope self returns the org itself at depth 0',
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000002' AND depth = 0
|
||||
), TRUE);
|
||||
|
||||
-- 'children' — direct children only (depth = 1 descendants)
|
||||
-- 'children' - direct children only (depth = 1 descendants)
|
||||
SELECT morbac.t_eq('scope children of root returns 2 rows (Engineering + Sales)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'children'))::bigint, 2);
|
||||
|
||||
@@ -520,7 +520,7 @@ SELECT morbac.t('scope children includes Engineering at depth 1',
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000002' AND depth = 1
|
||||
), TRUE);
|
||||
|
||||
-- 'descendants' — all descendants excluding self
|
||||
-- 'descendants' - all descendants excluding self
|
||||
SELECT morbac.t_eq('scope descendants of root returns 2 rows (Engineering + Sales, no self)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'descendants'))::bigint, 2);
|
||||
|
||||
@@ -533,7 +533,7 @@ SELECT morbac.t('scope descendants does not include self',
|
||||
SELECT morbac.t_eq('scope descendants of leaf returns 0 rows',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'descendants'))::bigint, 0);
|
||||
|
||||
-- 'subtree' — self + all descendants
|
||||
-- 'subtree' - self + all descendants
|
||||
SELECT morbac.t_eq('scope subtree of root returns 3 rows (self + Engineering + Sales)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree'))::bigint, 3);
|
||||
|
||||
@@ -546,7 +546,7 @@ SELECT morbac.t('scope subtree includes self at depth 0',
|
||||
SELECT morbac.t_eq('scope subtree of leaf returns 1 row (self only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'subtree'))::bigint, 1);
|
||||
|
||||
-- 'parent' — direct parent only
|
||||
-- 'parent' - direct parent only
|
||||
SELECT morbac.t_eq('scope parent of Engineering returns 1 row (GlobalTech HQ)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'parent'))::bigint, 1);
|
||||
|
||||
@@ -559,7 +559,7 @@ SELECT morbac.t('scope parent of Engineering returns GlobalTech HQ at depth 1',
|
||||
SELECT morbac.t_eq('scope parent of root returns 0 rows (no parent)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'parent'))::bigint, 0);
|
||||
|
||||
-- 'ancestors' — all ancestors excluding self
|
||||
-- 'ancestors' - all ancestors excluding self
|
||||
SELECT morbac.t_eq('scope ancestors of Engineering returns 1 row (GlobalTech HQ only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'ancestors'))::bigint, 1);
|
||||
|
||||
@@ -572,7 +572,7 @@ SELECT morbac.t('scope ancestors does not include self',
|
||||
SELECT morbac.t_eq('scope ancestors of root returns 0 rows',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'ancestors'))::bigint, 0);
|
||||
|
||||
-- 'lineage' — self + all ancestors
|
||||
-- 'lineage' - self + all ancestors
|
||||
SELECT morbac.t_eq('scope lineage of Engineering returns 2 rows (self + GlobalTech HQ)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'lineage'))::bigint, 2);
|
||||
|
||||
@@ -585,7 +585,7 @@ SELECT morbac.t('scope lineage includes self at depth 0',
|
||||
SELECT morbac.t_eq('scope lineage of root returns 1 row (self only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'lineage'))::bigint, 1);
|
||||
|
||||
-- 'root' — topmost ancestor only
|
||||
-- 'root' - topmost ancestor only
|
||||
SELECT morbac.t_eq('scope root of Engineering returns 1 row',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'root'))::bigint, 1);
|
||||
|
||||
@@ -604,7 +604,7 @@ SELECT morbac.t('scope root of root returns the org itself',
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
), TRUE);
|
||||
|
||||
-- p_max_depth — depth limiting
|
||||
-- p_max_depth - depth limiting
|
||||
SELECT morbac.t_eq('scope subtree max_depth=0 returns only self',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree', 0))::bigint, 1);
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '03 — DELEGATION'
|
||||
\echo '03 - DELEGATION'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Baseline — Leo (employee) before any delegation
|
||||
-- Section 1: Baseline - Leo (employee) before any delegation
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Baseline: Leo before delegation ---'
|
||||
@@ -37,7 +37,7 @@ SELECT morbac.t('Leo (employee) reads documents before delegation',
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Leo cannot approve documents — that requires manager role
|
||||
-- Leo cannot approve documents - that requires manager role
|
||||
SELECT morbac.t('Leo (employee) approves documents before delegation [no manager perm]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
@@ -66,7 +66,7 @@ SELECT morbac.t('Leo (employee) does not have approve permission before delegati
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Active delegation — Carol delegates manager role to Leo
|
||||
-- Section 2: Active delegation - Carol delegates manager role to Leo
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Active delegation: Carol -> Leo (manager role, 1 day) ---'
|
||||
@@ -83,7 +83,7 @@ VALUES (
|
||||
now() + interval '1 day'
|
||||
);
|
||||
|
||||
-- Leo now has delegated manager role — can approve documents
|
||||
-- Leo now has delegated manager role - can approve documents
|
||||
SELECT morbac.t('Leo (delegated manager) approves documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
@@ -217,7 +217,7 @@ VALUES (
|
||||
TRUE -- revoked
|
||||
);
|
||||
|
||||
-- Dave should NOT get accountant from revoked delegation — verify revoked flag is set
|
||||
-- Dave should NOT get accountant from revoked delegation - verify revoked flag is set
|
||||
-- Note: employee already has write financial_data via view hierarchy;
|
||||
-- so we verify the delegation is actually revoked in the DB.
|
||||
SELECT morbac.t('Revoked accountant delegation has revoked=TRUE in DB',
|
||||
@@ -296,7 +296,7 @@ INSERT INTO morbac.delegations
|
||||
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
|
||||
VALUES (
|
||||
'de000001-0000-0000-0000-000000000005',
|
||||
'30000000-0000-0000-0000-000000000004', -- Dave (employee — does NOT hold manager)
|
||||
'30000000-0000-0000-0000-000000000004', -- Dave (employee - does NOT hold manager)
|
||||
'30000000-0000-0000-0000-000000000005', -- Eve
|
||||
'20000000-0001-0000-0000-000000000003', -- manager role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
@@ -330,7 +330,7 @@ SELECT morbac.t('Invalid delegation not in Eve comprehensive roles',
|
||||
\echo ''
|
||||
\echo '--- 8. get_comprehensive_roles source reporting ---'
|
||||
|
||||
-- Alice (CEO, direct) — source should be 'direct'
|
||||
-- Alice (CEO, direct) - source should be 'direct'
|
||||
SELECT morbac.t('Alice CEO role has source=direct in comprehensive roles',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
|
||||
+30
-30
@@ -2,10 +2,10 @@
|
||||
-- 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
|
||||
-- 4. Rule conflict detection — modality conflicts on same tuple
|
||||
-- 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
|
||||
-- 4. Rule conflict detection - modality conflicts on same tuple
|
||||
--
|
||||
-- Scenario:
|
||||
-- - auditor and accountant are mutually exclusive (no one can hold both)
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '04 — CONSTRAINTS'
|
||||
\echo '04 - CONSTRAINTS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Separation of Duty — define conflict
|
||||
-- Section 1: Separation of Duty - define conflict
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Separation of Duty setup ---'
|
||||
@@ -49,7 +49,7 @@ SELECT morbac.t_eq('SoD conflict between auditor and accountant created',
|
||||
\echo ''
|
||||
\echo '--- 2. SoD violation detection ---'
|
||||
|
||||
-- Heidi (auditor) — check if assigning accountant role would violate SoD
|
||||
-- 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
|
||||
@@ -57,7 +57,7 @@ SELECT morbac.t('Assigning accountant to Heidi (auditor) violates SoD',
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), TRUE);
|
||||
|
||||
-- Ivan (accountant) — check if assigning auditor role would violate SoD
|
||||
-- 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
|
||||
@@ -65,8 +65,8 @@ SELECT morbac.t('Assigning auditor to Ivan (accountant) violates SoD [symmetric]
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) — check if assigning accountant would violate SoD
|
||||
-- Dave is not an auditor — no conflict
|
||||
-- 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
|
||||
@@ -74,7 +74,7 @@ SELECT morbac.t('Assigning accountant to Dave (not an auditor) does not violate
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), FALSE);
|
||||
|
||||
-- Heidi (auditor) — assigning a non-conflicting role (manager) is fine
|
||||
-- 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
|
||||
@@ -126,7 +126,7 @@ VALUES (
|
||||
'Frank is a contractor and must not gain employee-level access'
|
||||
);
|
||||
|
||||
-- Frank's employee role is negated — verify via get_comprehensive_roles
|
||||
-- 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(
|
||||
@@ -148,7 +148,7 @@ SELECT morbac.t('Frank: contractor role still present after employee negated',
|
||||
), 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
|
||||
-- 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
|
||||
@@ -181,7 +181,7 @@ WHERE user_id = '30000000-0000-0000-0000-000000000006'
|
||||
\echo ''
|
||||
\echo '--- 4. Role cardinality constraints ---'
|
||||
|
||||
-- Set a cardinality constraint: compliance_officer role — min 1, max 2
|
||||
-- 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
|
||||
@@ -189,8 +189,8 @@ VALUES (
|
||||
'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',
|
||||
-- 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
|
||||
@@ -201,15 +201,15 @@ 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',
|
||||
-- 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)',
|
||||
-- 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
|
||||
@@ -221,21 +221,21 @@ 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',
|
||||
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',
|
||||
-- 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',
|
||||
-- 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
|
||||
@@ -263,8 +263,8 @@ VALUES (
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- No conflict yet — only a permission exists
|
||||
SELECT morbac.t_eq('detect_rule_conflicts: permission alone — no conflicts',
|
||||
-- No conflict yet - only a permission exists
|
||||
SELECT morbac.t_eq('detect_rule_conflicts: permission alone - no conflicts',
|
||||
(SELECT COUNT(*) FROM morbac.detect_rule_conflicts(
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004',
|
||||
@@ -309,7 +309,7 @@ SELECT morbac.t('detect_rule_conflicts: conflicting rule is the permission',
|
||||
WHERE conflicting_modality = 'permission'
|
||||
), TRUE);
|
||||
|
||||
-- Insert an obligation for the same tuple — conflicts with the prohibition
|
||||
-- Insert an obligation for the same tuple - conflicts with the prohibition
|
||||
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'f0000000-0000-0000-0000-000000000003',
|
||||
@@ -331,7 +331,7 @@ SELECT morbac.t_eq('detect_rule_conflicts: obligation conflicts with existing pr
|
||||
))::bigint,
|
||||
1);
|
||||
|
||||
-- Insert a recommendation — conflicts with both obligation and prohibition
|
||||
-- Insert a recommendation - conflicts with both obligation and prohibition
|
||||
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'f0000000-0000-0000-0000-000000000004',
|
||||
@@ -354,7 +354,7 @@ SELECT morbac.t_eq('detect_rule_conflicts: recommendation conflicts with prohibi
|
||||
2);
|
||||
|
||||
-- No conflict between permission and recommendation (they coexist meaningfully)
|
||||
SELECT morbac.t_eq('detect_rule_conflicts: permission vs recommendation — no conflict',
|
||||
SELECT morbac.t_eq('detect_rule_conflicts: permission vs recommendation - no conflict',
|
||||
(SELECT COUNT(*) FROM morbac.detect_rule_conflicts(
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004',
|
||||
|
||||
+14
-14
@@ -9,7 +9,7 @@
|
||||
-- 3. Expired rule (valid_until in the past) -> denied
|
||||
-- 4. Future rule (valid_from in the future) -> denied
|
||||
-- 5. Active time window (valid_from past, valid_until future) -> allowed
|
||||
-- 6. Multiple rules for same combination — only active ones count
|
||||
-- 6. Multiple rules for same combination - only active ones count
|
||||
-- 7. Expired prohibition: no longer blocks access after it expires
|
||||
-- 8. Temporal rules interact correctly with role hierarchy
|
||||
--
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '05 — TEMPORAL CONSTRAINTS'
|
||||
\echo '05 - TEMPORAL CONSTRAINTS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -37,35 +37,35 @@ INSERT INTO morbac.views (name, description) VALUES
|
||||
\echo '--- 1. is_rule_valid() helper ---'
|
||||
|
||||
-- No bounds: always valid
|
||||
SELECT morbac.t('is_rule_valid(NULL, NULL) — always valid',
|
||||
SELECT morbac.t('is_rule_valid(NULL, NULL) - always valid',
|
||||
morbac.is_rule_valid(NULL::timestamptz, NULL::timestamptz), TRUE);
|
||||
|
||||
-- Past valid_from, no valid_until: currently active
|
||||
SELECT morbac.t('is_rule_valid(past, NULL) — started in past, no end',
|
||||
SELECT morbac.t('is_rule_valid(past, NULL) - started in past, no end',
|
||||
morbac.is_rule_valid('2000-01-01'::timestamptz, NULL), TRUE);
|
||||
|
||||
-- Future valid_from: not yet active
|
||||
SELECT morbac.t('is_rule_valid(future, NULL) — not yet started',
|
||||
SELECT morbac.t('is_rule_valid(future, NULL) - not yet started',
|
||||
morbac.is_rule_valid('2099-01-01'::timestamptz, NULL), FALSE);
|
||||
|
||||
-- Past valid_until: expired
|
||||
SELECT morbac.t('is_rule_valid(NULL, past) — already expired',
|
||||
SELECT morbac.t('is_rule_valid(NULL, past) - already expired',
|
||||
morbac.is_rule_valid(NULL, '2000-01-01'::timestamptz), FALSE);
|
||||
|
||||
-- Future valid_until, no valid_from: currently active
|
||||
SELECT morbac.t('is_rule_valid(NULL, future) — no start, future end',
|
||||
SELECT morbac.t('is_rule_valid(NULL, future) - no start, future end',
|
||||
morbac.is_rule_valid(NULL, '2099-01-01'::timestamptz), TRUE);
|
||||
|
||||
-- Active window: past start, future end
|
||||
SELECT morbac.t('is_rule_valid(past, future) — within active window',
|
||||
SELECT morbac.t('is_rule_valid(past, future) - within active window',
|
||||
morbac.is_rule_valid('2000-01-01'::timestamptz, '2099-01-01'::timestamptz), TRUE);
|
||||
|
||||
-- Fully past window (both start and end in the past)
|
||||
SELECT morbac.t('is_rule_valid(past_start, past_end) — entirely expired',
|
||||
SELECT morbac.t('is_rule_valid(past_start, past_end) - entirely expired',
|
||||
morbac.is_rule_valid('2000-01-01'::timestamptz, '2001-01-01'::timestamptz), FALSE);
|
||||
|
||||
-- Fully future window (both start and end in the future)
|
||||
SELECT morbac.t('is_rule_valid(future_start, future_end) — entirely in the future',
|
||||
SELECT morbac.t('is_rule_valid(future_start, future_end) - entirely in the future',
|
||||
morbac.is_rule_valid('2090-01-01'::timestamptz, '2099-01-01'::timestamptz), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -105,7 +105,7 @@ SELECT morbac.t('is_active = FALSE after valid_until set to past',
|
||||
DELETE FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Expired rule — valid_until in the past
|
||||
-- Section 3: Expired rule - valid_until in the past
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Expired rule (valid_until in the past) ---'
|
||||
@@ -129,7 +129,7 @@ SELECT morbac.t('Dave (employee) exports temp_view via expired rule [denied]',
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Future rule — valid_from in the future
|
||||
-- Section 4: Future rule - valid_from in the future
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. Future rule (valid_from in the future) ---'
|
||||
@@ -209,7 +209,7 @@ SELECT morbac.t('Dave exports temp_view (export rule now active)',
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Temporal prohibition — expired prohibition no longer blocks
|
||||
-- Section 7: Temporal prohibition - expired prohibition no longer blocks
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 7. Expired prohibition no longer blocks ---'
|
||||
@@ -295,7 +295,7 @@ SELECT morbac.t('Carol (manager) approves temp_view [temporal rule, active]',
|
||||
'approve', 'temp_view'
|
||||
), TRUE);
|
||||
|
||||
-- Alice (CEO) inherits from manager — should also get the temporal permission
|
||||
-- Alice (CEO) inherits from manager - should also get the temporal permission
|
||||
SELECT morbac.t('Alice (CEO, inherits manager) approves temp_view [temporal rule, active]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '06 — CROSS-ORGANIZATIONAL RULES'
|
||||
\echo '06 - CROSS-ORGANIZATIONAL RULES'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -53,7 +53,7 @@ SELECT morbac.t('Nina has eng_auditor role at Engineering',
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: No cross-org rule — access between orgs is denied by default
|
||||
-- Section 1: No cross-org rule - access between orgs is denied by default
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. No cross-org rule: access denied by default ---'
|
||||
@@ -75,7 +75,7 @@ SELECT morbac.t('Judy (Engineering engineer) reads GlobalTech financial_data [no
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Cross-org permission — Sales sales_rep reads GlobalTech reports
|
||||
-- Section 2: Cross-org permission - Sales sales_rep reads GlobalTech reports
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Cross-org permission ---'
|
||||
@@ -123,7 +123,7 @@ SELECT morbac.t('Judy (Sales sales_rep) reads GlobalTech documents [no rule for
|
||||
\echo ''
|
||||
\echo '--- 3. Role must be held in source org ---'
|
||||
|
||||
-- Karl has no role anywhere — cannot use the Sales->GlobalTech cross-org rule
|
||||
-- Karl has no role anywhere - cannot use the Sales->GlobalTech cross-org rule
|
||||
SELECT morbac.t('Karl (no role) reads GlobalTech reports via cross-org rule [no role in source]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
@@ -140,7 +140,7 @@ SELECT morbac.t('Karl (no role) reads GlobalTech documents [no access anywhere]'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Cross-org prohibition — blocks access even with regular permission
|
||||
-- Section 4: Cross-org prohibition - blocks access even with regular permission
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. Cross-org prohibition ---'
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '07 — AUDIT LOGGING'
|
||||
\echo '07 - AUDIT LOGGING'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Clear any existing audit log entries to start fresh
|
||||
@@ -160,7 +160,7 @@ DELETE FROM morbac.rules WHERE id = 'a0000000-0000-0000-0000-000000000001';
|
||||
\echo ''
|
||||
\echo '--- 5. Query audit log by record_id ---'
|
||||
|
||||
-- The test rule had INSERT, UPDATE, DELETE — should be 3 entries
|
||||
-- The test rule had INSERT, UPDATE, DELETE - should be 3 entries
|
||||
SELECT morbac.t_eq('Audit log has 3 entries for test rule record (INSERT + UPDATE + DELETE)',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log
|
||||
WHERE table_name = 'rules'
|
||||
@@ -245,7 +245,7 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Perform another user_roles change — should NOT be logged
|
||||
-- Perform another user_roles change - should NOT be logged
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000011',
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
-- 2. Grant permissions via regular rules, verify access
|
||||
-- 3. Prohibition overrides permission (standard engine behavior)
|
||||
-- 4. Role hierarchy applies: senior role inherits permissions
|
||||
-- 5. assign_role() / revoke_role() — SoD/cardinality enforcement, RLS guards the INSERT/DELETE
|
||||
-- 5. assign_role() / revoke_role() - SoD/cardinality enforcement, RLS guards the INSERT/DELETE
|
||||
-- 6. RLS on morbac tables: session user cannot read/write without rules
|
||||
-- 7. Rules are org-scoped
|
||||
--
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '08 — SYSTEM ACCESS'
|
||||
\echo '08 - SYSTEM ACCESS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -218,7 +218,7 @@ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO morbac_rls_tester;
|
||||
|
||||
SET SESSION AUTHORIZATION morbac_rls_tester;
|
||||
|
||||
-- Grace (hr_manager) has create/delete on user_roles — RLS should allow
|
||||
-- Grace (hr_manager) has create/delete on user_roles - RLS should allow
|
||||
SET morbac.user_id = '30000000-0000-0000-0000-000000000007';
|
||||
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
@@ -241,7 +241,7 @@ WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000005'
|
||||
AND org_id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- Dave (employee) has no rules for user_roles — RLS should block
|
||||
-- Dave (employee) has no rules for user_roles - RLS should block
|
||||
SET morbac.user_id = '30000000-0000-0000-0000-000000000004';
|
||||
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
|
||||
+11
-11
@@ -3,14 +3,14 @@
|
||||
-- =============================================================================
|
||||
-- Tests miscellaneous utility functions and advanced features:
|
||||
--
|
||||
-- 1. pending_obligations — returns obligation rules for a user
|
||||
-- 2. pending_recommendations — returns recommendation rules for a user
|
||||
-- 1. pending_obligations - returns obligation rules for a user
|
||||
-- 2. pending_recommendations - returns recommendation rules for a user
|
||||
-- 3. Obligations/recommendations do NOT affect is_allowed()
|
||||
-- 3b. Conflict resolution: prohibition voids obligation; prohibition/obligation voids recommendation
|
||||
-- 4. user_has_role — checks if user holds a named role
|
||||
-- 5. user_roles_in_org — lists all roles for user in org
|
||||
-- 6. eval_context — evaluates context predicates directly
|
||||
-- 7. Derived roles — computed via evaluator function
|
||||
-- 4. user_has_role - checks if user holds a named role
|
||||
-- 5. user_roles_in_org - lists all roles for user in org
|
||||
-- 6. eval_context - evaluates context predicates directly
|
||||
-- 7. Derived roles - computed via evaluator function
|
||||
-- 8. RLS helpers: get_user_orgs, current_org_ids, rls_check() org scoping
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 08_system_access.sql
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '09 — UTILITIES, DERIVED ROLES, RLS'
|
||||
\echo '09 - UTILITIES, DERIVED ROLES, RLS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -139,7 +139,7 @@ SELECT morbac.t('Dave has recommendation for read audit_logs but no permission [
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3b: Conflict resolution — prohibition voids obligation/recommendation
|
||||
-- Section 3b: Conflict resolution - prohibition voids obligation/recommendation
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3b. Conflict resolution: prohibition voids obligation and recommendation ---'
|
||||
@@ -330,13 +330,13 @@ SELECT morbac.t('eval_context(end_of_quarter) = TRUE',
|
||||
\echo ''
|
||||
\echo '--- 7. Derived roles ---'
|
||||
|
||||
-- Create a derived role: 'senior_employee' — dynamically granted to Dave (only)
|
||||
-- Create a derived role: 'senior_employee' - dynamically granted to Dave (only)
|
||||
INSERT INTO morbac.roles (id, org_id, name, description)
|
||||
VALUES (
|
||||
'20000000-0001-0000-0000-000000000011',
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'senior_employee',
|
||||
'Senior employee — granted dynamically based on tenure'
|
||||
'Senior employee - granted dynamically based on tenure'
|
||||
);
|
||||
|
||||
-- Evaluator function: returns TRUE only for Dave at GlobalTech
|
||||
@@ -379,7 +379,7 @@ SELECT morbac.t('Dave has senior_employee derived role in comprehensive roles',
|
||||
AND source = 'derived'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) does NOT satisfy the evaluator — no derived role
|
||||
-- Eve (intern) does NOT satisfy the evaluator - no derived role
|
||||
SELECT morbac.t('Eve has no derived roles in comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
-- Activity-View Binding Tests
|
||||
-- =============================================================================
|
||||
-- Tests opt-in activity-to-view restrictions:
|
||||
-- 1. No bindings defined — any view is allowed
|
||||
-- 2. Binding defined — listed view is allowed, unlisted view is blocked
|
||||
-- 1. No bindings defined - any view is allowed
|
||||
-- 2. Binding defined - listed view is allowed, unlisted view is blocked
|
||||
-- 3. Blocking applies to cross_org_rules as well
|
||||
-- 4. Removing all bindings lifts the restriction
|
||||
--
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '10 — ACTIVITY-VIEW BINDINGS'
|
||||
\echo '10 - ACTIVITY-VIEW BINDINGS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: No bindings — unconstrained
|
||||
-- Section 1: No bindings - unconstrained
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. No bindings: any view is allowed ---'
|
||||
@@ -39,7 +39,7 @@ SELECT morbac.t('No bindings: audit/documents rule inserted successfully',
|
||||
DELETE FROM morbac.rules WHERE id = 'b0000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Binding defined — listed view allowed, unlisted view blocked
|
||||
-- Section 2: Binding defined - listed view allowed, unlisted view blocked
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Binding defined: allowed view works, unlisted view blocked ---'
|
||||
|
||||
+13
-13
@@ -3,18 +3,18 @@
|
||||
-- =============================================================================
|
||||
-- Tests rules.scope and cross_org_rules.source_org_id = NULL:
|
||||
--
|
||||
-- 1. scope='self' (default) — exact org only, unchanged behavior
|
||||
-- 2. scope='subtree' — rule at root covers self + Engineering + Sales
|
||||
-- 3. scope='descendants' — covers Engineering + Sales but NOT GlobalTech itself
|
||||
-- 4. scope='children' — covers direct children only
|
||||
-- 5. New org added after rule creation — picked up automatically (cache invalidation)
|
||||
-- 1. scope='self' (default) - exact org only, unchanged behavior
|
||||
-- 2. scope='subtree' - rule at root covers self + Engineering + Sales
|
||||
-- 3. scope='descendants' - covers Engineering + Sales but NOT GlobalTech itself
|
||||
-- 4. scope='children' - covers direct children only
|
||||
-- 5. New org added after rule creation - picked up automatically (cache invalidation)
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 10_activity_view_bindings.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '11 — SCOPE RULES AND GLOBAL CROSS-ORG RULES'
|
||||
\echo '11 - SCOPE RULES AND GLOBAL CROSS-ORG RULES'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Setup: create a dedicated role for scope tests (avoid polluting existing rules)
|
||||
@@ -23,7 +23,7 @@ VALUES (
|
||||
'20000000-0001-0000-0000-000000000012',
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'analyst',
|
||||
'Data analyst — scope tests'
|
||||
'Data analyst - scope tests'
|
||||
);
|
||||
|
||||
-- Assign Karl (previously no role) as analyst at GlobalTech
|
||||
@@ -35,7 +35,7 @@ VALUES (
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: scope='self' (default) — exact org only
|
||||
-- Section 1: scope='self' (default) - exact org only
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. scope=self (default) ---'
|
||||
@@ -67,7 +67,7 @@ SELECT morbac.t('Karl (analyst, scope=self) reads reports in Engineering [denied
|
||||
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: scope='subtree' — root + all descendants
|
||||
-- Section 2: scope='subtree' - root + all descendants
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. scope=subtree ---'
|
||||
@@ -106,7 +106,7 @@ SELECT morbac.t('Karl (analyst, scope=subtree) reads reports in Sales [allowed]'
|
||||
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000002';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: scope='descendants' — children only, NOT self
|
||||
-- Section 3: scope='descendants' - children only, NOT self
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. scope=descendants ---'
|
||||
@@ -145,7 +145,7 @@ SELECT morbac.t('Karl (analyst, scope=descendants) reads reports in Sales [allow
|
||||
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000003';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: scope='children' — direct children only
|
||||
-- Section 4: scope='children' - direct children only
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. scope=children ---'
|
||||
@@ -193,7 +193,7 @@ DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000004';
|
||||
DELETE FROM morbac.orgs WHERE id = '10000000-0000-0000-0000-000000000004';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: New org added after rule creation — scope picks it up automatically
|
||||
-- Section 5: New org added after rule creation - scope picks it up automatically
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Dynamic scope: new org covered automatically ---'
|
||||
@@ -225,7 +225,7 @@ VALUES (
|
||||
'10000000-0000-0000-0000-000000000001'
|
||||
);
|
||||
|
||||
-- The scoped rule was defined before Legal Dept existed — still covers it
|
||||
-- The scoped rule was defined before Legal Dept existed - still covers it
|
||||
SELECT morbac.t('Karl (analyst) reads documents in Legal Dept [new org, covered by subtree scope]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '12 — USER RULES'
|
||||
\echo '12 - USER RULES'
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: No user rule — Karl (no role) is denied
|
||||
-- Section 1: No user rule - Karl (no role) is denied
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. No user rule: access denied ---'
|
||||
@@ -41,7 +41,7 @@ SELECT morbac.t('Karl (no role) reads GlobalTech financial_data [no user rule]',
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Direct user permission — Karl gets access without a role
|
||||
-- Section 2: Direct user permission - Karl gets access without a role
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Direct user permission ---'
|
||||
@@ -78,7 +78,7 @@ SELECT morbac.t('Karl reads GlobalTech financial_data [documents rule covers it
|
||||
'read', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- contracts has no hierarchy relationship — documents rule does not cover it
|
||||
-- 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,
|
||||
@@ -141,7 +141,7 @@ SELECT morbac.t('Eve (intern) reads GlobalTech public_data [user prohibition blo
|
||||
'read', 'public_data'
|
||||
), FALSE);
|
||||
|
||||
-- Dave (employee) is unaffected — only Eve has the prohibition
|
||||
-- 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,
|
||||
@@ -150,7 +150,7 @@ SELECT morbac.t('Dave (employee) reads GlobalTech public_data [no user prohibiti
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: Priority — higher-priority user permission overrides prohibition
|
||||
-- Section 5: Priority - higher-priority user permission overrides prohibition
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Priority: user permission overrides user prohibition ---'
|
||||
@@ -236,7 +236,7 @@ SELECT morbac.t('rls_check passes without target_user_id filter',
|
||||
'30000000-0000-0000-0000-000000000004'::uuid
|
||||
), TRUE);
|
||||
|
||||
-- Set target_user_id to Dave — rows belonging to Dave pass
|
||||
-- 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',
|
||||
@@ -254,7 +254,7 @@ SELECT morbac.t('rls_check blocked when row user_id differs from target_user_id'
|
||||
'30000000-0000-0000-0000-000000000001'::uuid
|
||||
), FALSE);
|
||||
|
||||
-- No p_row_user_id passed — user filter does not apply
|
||||
-- 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',
|
||||
|
||||
+10
-10
@@ -37,7 +37,7 @@
|
||||
\echo '================================================================'
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: No user_id set — always FALSE
|
||||
-- Section 1: No user_id set - always FALSE
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. No user_id: always FALSE ---'
|
||||
@@ -64,19 +64,19 @@ SELECT morbac.t('rls_check without user_id, global row',
|
||||
SET morbac.user_id = '30000000-0000-0000-0000-000000000011'; -- Karl
|
||||
SET morbac.org_id = '10000000-0000-0000-0000-000000000001'; -- GlobalTech HQ
|
||||
|
||||
-- 2a: org-scoped row, matching org — Karl has user_rule for read documents
|
||||
-- 2a: org-scoped row, matching org - Karl has user_rule for read documents
|
||||
SELECT morbac.t('rls_check single org, org row matches session org (Karl/documents)',
|
||||
morbac.rls_check('read', 'documents',
|
||||
'10000000-0000-0000-0000-000000000001'::uuid),
|
||||
TRUE);
|
||||
|
||||
-- 2b: org-scoped row, different org — blocked before is_allowed
|
||||
-- 2b: org-scoped row, different org - blocked before is_allowed
|
||||
SELECT morbac.t('rls_check single org, org row from different org (blocked)',
|
||||
morbac.rls_check('read', 'documents',
|
||||
'10000000-0000-0000-0000-000000000002'::uuid),
|
||||
FALSE);
|
||||
|
||||
-- 2c: NULL row — filtered out under a single org pin (orphan not requested)
|
||||
-- 2c: NULL row - filtered out under a single org pin (orphan not requested)
|
||||
SELECT morbac.t('rls_check single org, NULL row filtered out under org pin',
|
||||
morbac.rls_check('read', 'documents', NULL),
|
||||
FALSE);
|
||||
@@ -98,19 +98,19 @@ RESET morbac.org_id;
|
||||
SET morbac.user_id = '30000000-0000-0000-0000-000000000011'; -- Karl
|
||||
SET morbac.org_ids = '["10000000-0000-0000-0000-000000000001"]'; -- [GlobalTech HQ]
|
||||
|
||||
-- 3a: org-scoped row in the list — Karl has user_rule for read documents in GlobalTech
|
||||
-- 3a: org-scoped row in the list - Karl has user_rule for read documents in GlobalTech
|
||||
SELECT morbac.t('rls_check org_ids, org row in list (Karl/documents/GlobalTech)',
|
||||
morbac.rls_check('read', 'documents',
|
||||
'10000000-0000-0000-0000-000000000001'::uuid),
|
||||
TRUE);
|
||||
|
||||
-- 3b: org-scoped row not in the list — blocked
|
||||
-- 3b: org-scoped row not in the list - blocked
|
||||
SELECT morbac.t('rls_check org_ids, org row not in list (blocked)',
|
||||
morbac.rls_check('read', 'documents',
|
||||
'10000000-0000-0000-0000-000000000002'::uuid),
|
||||
FALSE);
|
||||
|
||||
-- 3c: NULL row, list WITHOUT null marker — filtered out even with a global grant
|
||||
-- 3c: NULL row, list WITHOUT null marker - filtered out even with a global grant
|
||||
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||
@@ -123,7 +123,7 @@ SELECT morbac.t('rls_check org_ids without null marker, NULL row filtered out de
|
||||
morbac.rls_check('read', 'contracts', NULL),
|
||||
FALSE);
|
||||
|
||||
-- 3c2: NULL row, list WITH null marker + global permission — allowed
|
||||
-- 3c2: NULL row, list WITH null marker + global permission - allowed
|
||||
SET morbac.org_ids = '["10000000-0000-0000-0000-000000000001", null]';
|
||||
|
||||
SELECT morbac.t('rls_check org_ids with null marker, NULL row + global permission',
|
||||
@@ -167,7 +167,7 @@ RESET morbac.org_ids;
|
||||
|
||||
SET morbac.user_id = '30000000-0000-0000-0000-000000000011'; -- Karl
|
||||
|
||||
-- 4a: org-scoped row — uses row's org_id (Karl has user_rule in GlobalTech)
|
||||
-- 4a: org-scoped row - uses row's org_id (Karl has user_rule in GlobalTech)
|
||||
SELECT morbac.t('rls_check no org context, org row: uses row org (Karl/documents/GlobalTech)',
|
||||
morbac.rls_check('read', 'documents',
|
||||
'10000000-0000-0000-0000-000000000001'::uuid),
|
||||
@@ -179,7 +179,7 @@ SELECT morbac.t('rls_check no org context, org row: no permission in row org (En
|
||||
'10000000-0000-0000-0000-000000000002'::uuid),
|
||||
FALSE);
|
||||
|
||||
-- 4b: global row + global permission — now goes to is_allowed(Karl, NULL, ...) → global_rules only
|
||||
-- 4b: NULL row + global permission - is_allowed(Karl, NULL, ...) sees unattributed + global rules
|
||||
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||
|
||||
Reference in New Issue
Block a user