feat(all): add scope handling in rules

This commit is contained in:
2026-04-01 23:07:20 +02:00
parent 8273d56d4e
commit 5ef8bae1b4
26 changed files with 1129 additions and 1330 deletions
+46 -72
View File
@@ -173,10 +173,11 @@ INSERT INTO morbac.activities (name, description) VALUES
('read', 'Read / view data'),
('write', 'Create or modify data'),
('delete', 'Delete data'),
('manage', 'Full management access'),
('approve', 'Approve requests or documents'),
('export', 'Export data to external format'),
('audit', 'Audit trail review'),
('manage', 'Full administrative control');
('audit', 'Audit trail review')
ON CONFLICT (name) DO NOTHING;
INSERT INTO morbac.views (name, description) VALUES
('documents', 'General company documents'),
@@ -185,7 +186,8 @@ INSERT INTO morbac.views (name, description) VALUES
('hr_data', 'Human resources data'),
('contracts', 'Legal contracts'),
('audit_logs', 'System audit logs'),
('public_data', 'Publicly accessible data');
('public_data', 'Publicly accessible data')
ON CONFLICT (name) DO NOTHING;
-- ---------------------------------------------------------------------------
-- CONTEXTS
@@ -219,77 +221,49 @@ INSERT INTO morbac.contexts (name, description, evaluator) VALUES
SELECT name, description FROM morbac.contexts ORDER BY name;
-- ---------------------------------------------------------------------------
-- POLICIES (via Policy DSL — resolved by compile_policy)
-- RULES
-- ---------------------------------------------------------------------------
\echo ''
\echo '=== Setup: Policies ==='
\echo '=== Setup: Rules ==='
-- GlobalTech HQ policies
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
-- Intern: read public data only
('GlobalTech HQ', 'intern', 'read', 'public_data', 'permission', 'always'),
-- Employee: read documents and reports; write documents only during business hours
('GlobalTech HQ', 'employee', 'read', 'documents', 'permission', 'always'),
('GlobalTech HQ', 'employee', 'write', 'documents', 'permission', 'business_hours'),
('GlobalTech HQ', 'employee', 'read', 'reports', 'permission', 'always'),
('GlobalTech HQ', 'employee', 'read', 'public_data', 'permission', 'always'),
-- Obligation: employees must review reports weekly
('GlobalTech HQ', 'employee', 'read', 'reports', 'obligation', 'always'),
-- Manager: approve and delete documents; read financial data
('GlobalTech HQ', 'manager', 'approve','documents', 'permission', 'always'),
('GlobalTech HQ', 'manager', 'delete', 'documents', 'permission', 'always'),
('GlobalTech HQ', 'manager', 'read', 'financial_data', 'permission', 'always'),
-- HR Manager: full access to HR data
('GlobalTech HQ', 'hr_manager', 'read', 'hr_data', 'permission', 'always'),
('GlobalTech HQ', 'hr_manager', 'write', 'hr_data', 'permission', 'always'),
('GlobalTech HQ', 'hr_manager', 'delete', 'hr_data', 'permission', 'always'),
-- Auditor: read audit logs, financial data, and documents
('GlobalTech HQ', 'auditor', 'read', 'audit_logs', 'permission', 'always'),
('GlobalTech HQ', 'auditor', 'read', 'financial_data', 'permission', 'always'),
('GlobalTech HQ', 'auditor', 'read', 'documents', 'permission', 'always'),
-- Accountant: read and write financial data
('GlobalTech HQ', 'accountant', 'read', 'financial_data', 'permission', 'always'),
('GlobalTech HQ', 'accountant', 'write', 'financial_data', 'permission', 'always'),
-- Contractor: read documents; PROHIBITED from financial and HR data
('GlobalTech HQ', 'contractor', 'read', 'documents', 'permission', 'always'),
('GlobalTech HQ', 'contractor', 'read', 'financial_data', 'prohibition', 'always'),
('GlobalTech HQ', 'contractor', 'read', 'hr_data', 'prohibition', 'always'),
-- Compliance Officer: read audit logs and financial data
('GlobalTech HQ', 'compliance_officer', 'read', 'audit_logs', 'permission', 'always'),
('GlobalTech HQ', 'compliance_officer', 'read', 'financial_data', 'permission', 'always'),
-- Recommendation: staff should review reports end of quarter
-- Note: voided by the obligation above (conflict resolution: obligation > recommendation)
('GlobalTech HQ', 'employee', 'read', 'reports', 'recommendation','end_of_quarter'),
-- Recommendation: staff should stay informed on public data (no conflicting obligation)
('GlobalTech HQ', 'employee', 'read', 'public_data', 'recommendation','always');
-- Engineering Dept policies
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
('Engineering Dept', 'engineer', 'read', 'documents', 'permission', 'always'),
('Engineering Dept', 'engineer', 'write', 'documents', 'permission', 'always'),
('Engineering Dept', 'tech_lead', 'approve', 'documents', 'permission', 'always');
-- Sales Dept policies
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
('Sales Dept', 'sales_rep', 'read', 'documents', 'permission', 'always'),
('Sales Dept', 'sales_rep', 'write', 'contracts', 'permission', 'always'),
('Sales Dept', 'sales_manager', 'read', 'reports', 'permission', 'always'),
('Sales Dept', 'sales_manager', 'approve', 'contracts', 'permission', 'always');
-- Compile all policies into rules
\echo ''
\echo '=== Setup: Compiling Policies ==='
SELECT * FROM morbac.compile_policy();
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
SELECT o.id, r.id, v.activity, v.view, c.id, v.modality::morbac.modality
FROM (VALUES
('GlobalTech HQ', 'intern', 'read', 'public_data', 'always', 'permission'),
('GlobalTech HQ', 'employee', 'read', 'documents', 'always', 'permission'),
('GlobalTech HQ', 'employee', 'write', 'documents', 'business_hours','permission'),
('GlobalTech HQ', 'employee', 'read', 'reports', 'always', 'permission'),
('GlobalTech HQ', 'employee', 'read', 'public_data', 'always', 'permission'),
('GlobalTech HQ', 'employee', 'read', 'reports', 'always', 'obligation'),
('GlobalTech HQ', 'employee', 'read', 'reports', 'end_of_quarter','recommendation'),
('GlobalTech HQ', 'employee', 'read', 'public_data', 'always', 'recommendation'),
('GlobalTech HQ', 'manager', 'approve','documents', 'always', 'permission'),
('GlobalTech HQ', 'manager', 'delete', 'documents', 'always', 'permission'),
('GlobalTech HQ', 'manager', 'read', 'financial_data', 'always', 'permission'),
('GlobalTech HQ', 'hr_manager', 'read', 'hr_data', 'always', 'permission'),
('GlobalTech HQ', 'hr_manager', 'write', 'hr_data', 'always', 'permission'),
('GlobalTech HQ', 'hr_manager', 'delete', 'hr_data', 'always', 'permission'),
('GlobalTech HQ', 'auditor', 'read', 'audit_logs', 'always', 'permission'),
('GlobalTech HQ', 'auditor', 'read', 'financial_data', 'always', 'permission'),
('GlobalTech HQ', 'auditor', 'read', 'documents', 'always', 'permission'),
('GlobalTech HQ', 'accountant', 'read', 'financial_data', 'always', 'permission'),
('GlobalTech HQ', 'accountant', 'write', 'financial_data', 'always', 'permission'),
('GlobalTech HQ', 'contractor', 'read', 'documents', 'always', 'permission'),
('GlobalTech HQ', 'contractor', 'read', 'financial_data', 'always', 'prohibition'),
('GlobalTech HQ', 'contractor', 'read', 'hr_data', 'always', 'prohibition'),
('GlobalTech HQ', 'compliance_officer', 'read', 'audit_logs', 'always', 'permission'),
('GlobalTech HQ', 'compliance_officer', 'read', 'financial_data', 'always', 'permission'),
('Engineering Dept', 'engineer', 'read', 'documents', 'always', 'permission'),
('Engineering Dept', 'engineer', 'write', 'documents', 'always', 'permission'),
('Engineering Dept', 'tech_lead', 'approve','documents', 'always', 'permission'),
('Sales Dept', 'sales_rep', 'read', 'documents', 'always', 'permission'),
('Sales Dept', 'sales_rep', 'write', 'contracts', 'always', 'permission'),
('Sales Dept', 'sales_manager', 'read', 'reports', 'always', 'permission'),
('Sales Dept', 'sales_manager', 'approve','contracts', 'always', 'permission')
) AS v(org_name, role_name, activity, view, context_name, modality)
JOIN morbac.orgs o ON o.name = v.org_name
JOIN morbac.roles r ON r.org_id = o.id AND r.name = v.role_name
JOIN morbac.contexts c ON c.name = v.context_name;
-- ---------------------------------------------------------------------------
-- Test infrastructure
@@ -343,6 +317,6 @@ $$;
\echo ' Organizations : GlobalTech HQ, Engineering Dept, Sales Dept'
\echo ' Roles : 14 across 3 orgs'
\echo ' Users : Alice, Bob, Carol, Dave, Eve, Frank, Grace, Heidi, Ivan, Judy, Karl, Leo'
\echo ' Activities : read, write, delete, approve, export, audit, manage'
\echo ' Activities : read, write, delete, manage, approve, export, audit (+ built-ins: create, update)'
\echo ' Views : documents, reports, financial_data, hr_data, contracts, audit_logs, public_data'
\echo ' Contexts : always (true), business_hours (true), after_hours (false), end_of_quarter (true)'
+7 -4
View File
@@ -36,10 +36,13 @@ INSERT INTO morbac.roles (id, org_id, name, description) VALUES
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
('30000000-0000-0000-0000-000000000014', '20000000-0002-0000-0000-000000000003', '10000000-0000-0000-0000-000000000002');
-- Policies for eng_auditor in Engineering
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name)
VALUES ('Engineering Dept', 'eng_auditor', 'read', 'audit_logs', 'permission', 'always');
SELECT * FROM morbac.compile_policy();
-- Rules for eng_auditor in Engineering
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
SELECT o.id, r.id, 'read', 'audit_logs', c.id, 'permission'
FROM morbac.orgs o
JOIN morbac.roles r ON r.org_id = o.id AND r.name = 'eng_auditor'
JOIN morbac.contexts c ON c.name = 'always'
WHERE o.name = 'Engineering Dept';
-- Verify nina's role was set up correctly
SELECT morbac.t('Nina has eng_auditor role at Engineering',
-342
View File
@@ -1,342 +0,0 @@
-- =============================================================================
-- Administration Rules Tests
-- =============================================================================
-- Tests the admin meta-policy system: who can manage policies, roles, and users.
--
-- is_admin_allowed(user_id, org_id, admin_activity, admin_target) is the main API.
-- Helper functions: can_manage_roles(), can_manage_policies(), can_manage_user_role().
--
-- Scenarios:
-- 1. Default deny: no admin rule = no admin access
-- 2. Grant admin permission to a role, verify the role holder can manage
-- 3. Admin prohibition blocks management even with permission
-- 4. Role hierarchy applies: senior role inherits admin permissions
-- 5. Admin helper functions
-- 6. can_manage_user_role() per-role management permission
-- 7. Admin rules are org-scoped
--
-- Company context:
-- Grace (hr_manager) can manage users (assign_role) for employee-level roles
-- Carol (manager) can create rules
-- Alice (CEO) inherits Carol's admin perms via hierarchy
--
-- Prerequisites: 00_setup.sql -> 07_audit.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '08 — ADMINISTRATION RULES'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: Default deny — no admin rule means no admin access
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. Default deny: no admin rule ---'
-- Nobody has admin rules yet — all should be denied
SELECT morbac.t('Alice (CEO, no admin rule yet) creates rules [default deny]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create_rule', 'rules'
), FALSE);
SELECT morbac.t('Grace (hr_manager, no admin rule) assigns roles [default deny]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000007'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'assign_role', 'employee'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 2: Grant admin permissions
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Grant admin permissions ---'
-- Grant manager role: can create and delete rules
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, context_id, modality)
VALUES
('10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000003', -- manager
'create_rule', 'rules',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'),
('10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000003', -- manager
'delete_rule', 'rules',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission');
-- Grant hr_manager role: can assign employee and intern roles
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, context_id, modality)
VALUES
('10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000008', -- hr_manager
'assign_role', 'employee',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'),
('10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000008', -- hr_manager
'assign_role', 'intern',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission');
-- Grant director role: can manage roles and policies
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, context_id, modality)
VALUES
('10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000002', -- director
'manage', 'roles',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'),
('10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000002', -- director
'manage', 'policies',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission');
-- ---------------------------------------------------------------------------
-- Section 3: Verify admin permissions
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. Verify admin permissions ---'
-- Carol (manager) can create rules
SELECT morbac.t('Carol (manager) creates rules',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create_rule', 'rules'
), TRUE);
-- Carol (manager) can delete rules
SELECT morbac.t('Carol (manager) deletes rules',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'delete_rule', 'rules'
), TRUE);
-- Grace (hr_manager) can assign employee role
SELECT morbac.t('Grace (hr_manager) assigns employee role',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000007'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'assign_role', 'employee'
), TRUE);
-- Grace (hr_manager) can assign intern role
SELECT morbac.t('Grace (hr_manager) assigns intern role',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000007'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'assign_role', 'intern'
), TRUE);
-- Grace (hr_manager) cannot assign manager role (no rule for that)
SELECT morbac.t('Grace (hr_manager) assigns manager role [no admin rule for manager]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000007'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'assign_role', 'manager'
), FALSE);
-- Dave (employee) has no admin permissions
SELECT morbac.t('Dave (employee) creates rules [no admin rule for employee]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create_rule', 'rules'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 4: Role hierarchy applies to admin permissions
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. Role hierarchy applies to admin permissions ---'
-- Bob (director) has own admin perm (manage roles/policies) and
-- inherits manager's admin perms (create_rule, delete_rule) via director->manager hierarchy
-- Bob (director) creates rules — inherited from manager via director->manager hierarchy
SELECT morbac.t('Bob (director, inherits manager admin) creates rules',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000002'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create_rule', 'rules'
), TRUE);
-- Bob (director) manages roles — own admin perm
SELECT morbac.t('Bob (director) manages roles [own admin perm]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000002'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'manage', 'roles'
), TRUE);
-- Alice (CEO) inherits everything through CEO->director->manager chain
SELECT morbac.t('Alice (CEO, inherits director+manager) manages roles',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'manage', 'roles'
), TRUE);
SELECT morbac.t('Alice (CEO) manages policies',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'manage', 'policies'
), TRUE);
-- Eve (intern) inherits nothing useful for admin
SELECT morbac.t('Eve (intern) creates rules [no admin permission in hierarchy]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create_rule', 'rules'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 5: Admin prohibition overrides permission
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Admin prohibition overrides permission ---'
-- Carol (manager) currently can create rules. Add a prohibition.
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, context_id, modality)
VALUES (
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000003', -- manager
'create_rule', 'rules',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'prohibition'
);
-- Prohibition overrides permission
SELECT morbac.t('Carol (manager, now prohibited) creates rules [prohibition wins]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create_rule', 'rules'
), FALSE);
-- Carol can still delete rules (prohibition only applied to create_rule)
SELECT morbac.t('Carol (manager, prohibition only on create) deletes rules [still allowed]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'delete_rule', 'rules'
), TRUE);
-- Remove the prohibition for subsequent tests
DELETE FROM morbac.admin_rules
WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND role_id = '20000000-0001-0000-0000-000000000003'
AND admin_activity = 'create_rule' AND admin_target = 'rules'
AND modality = 'prohibition';
-- Carol can again create rules after prohibition removed
SELECT morbac.t('Carol (manager) creates rules after prohibition removed',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create_rule', 'rules'
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 6: Admin helper functions
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 6. Admin helper functions ---'
-- can_manage_roles: wraps is_admin_allowed('manage', 'roles')
SELECT morbac.t('Bob (director) can_manage_roles',
morbac.can_manage_roles(
'30000000-0000-0000-0000-000000000002'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
), TRUE);
SELECT morbac.t('Dave (employee) can_manage_roles [denied]',
morbac.can_manage_roles(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
), FALSE);
-- can_manage_policies: wraps is_admin_allowed('manage', 'policies')
SELECT morbac.t('Bob (director) can_manage_policies',
morbac.can_manage_policies(
'30000000-0000-0000-0000-000000000002'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
), TRUE);
SELECT morbac.t('Carol (manager) can_manage_policies [no manage policies rule for manager]',
morbac.can_manage_policies(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 7: can_manage_user_role — per-role management check
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. can_manage_user_role ---'
-- Grace (hr_manager) has assign_role perm for 'employee'
-- can_manage_user_role checks is_admin_allowed('assign_role', role_name)
SELECT morbac.t('Grace (hr_manager) can manage employee role assignments',
morbac.can_manage_user_role(
'30000000-0000-0000-0000-000000000007'::uuid, -- Grace
'10000000-0000-0000-0000-000000000001'::uuid,
'20000000-0001-0000-0000-000000000004'::uuid -- employee role
), TRUE);
SELECT morbac.t('Grace (hr_manager) can manage intern role assignments',
morbac.can_manage_user_role(
'30000000-0000-0000-0000-000000000007'::uuid, -- Grace
'10000000-0000-0000-0000-000000000001'::uuid,
'20000000-0001-0000-0000-000000000005'::uuid -- intern role
), TRUE);
SELECT morbac.t('Grace (hr_manager) cannot manage manager role assignments [no rule]',
morbac.can_manage_user_role(
'30000000-0000-0000-0000-000000000007'::uuid, -- Grace
'10000000-0000-0000-0000-000000000001'::uuid,
'20000000-0001-0000-0000-000000000003'::uuid -- manager role
), FALSE);
SELECT morbac.t('Dave (employee) cannot manage any role assignments',
morbac.can_manage_user_role(
'30000000-0000-0000-0000-000000000004'::uuid, -- Dave
'10000000-0000-0000-0000-000000000001'::uuid,
'20000000-0001-0000-0000-000000000005'::uuid -- intern role
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 8: Admin rules are org-scoped
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 8. Admin rules are org-scoped ---'
-- Carol (manager at GlobalTech) cannot manage Engineering rules
-- (her admin rule is for GlobalTech org only)
SELECT morbac.t('Carol (GlobalTech manager) creates Engineering rules [wrong org]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid, -- Engineering org
'create_rule', 'rules'
), FALSE);
-- Alice (CEO at GlobalTech) cannot manage Engineering rules via admin (same org scoping)
SELECT morbac.t('Alice (GlobalTech CEO) creates Engineering rules [org-scoped]',
morbac.is_admin_allowed(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid, -- Engineering org
'manage', 'roles'
), FALSE);
\echo ''
\echo '=== Administration Rules Tests Completed ==='
+290
View File
@@ -0,0 +1,290 @@
-- =============================================================================
-- System Access Tests
-- =============================================================================
-- morbac system tables are protected by RLS.
-- is_allowed() and its internals are SECURITY DEFINER to avoid recursion.
-- System view names are config-driven (system_view.* keys, default: orgs/roles/rules/...).
--
-- Scenarios:
-- 1. Default deny: no rule = no access to system views via is_allowed()
-- 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
-- 6. RLS on morbac tables: session user cannot read/write without rules
-- 7. Rules are org-scoped
--
-- Prerequisites: 00_setup.sql -> 07_audit.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '08 — SYSTEM ACCESS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: Default deny
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. Default deny: no rule = no access to system views ---'
SELECT morbac.t('Carol (manager) create rules [default deny]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'rules'
), FALSE);
SELECT morbac.t('Grace (hr_manager) create user_roles [default deny]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000007'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'user_roles'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 2: Grant permissions via regular rules
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Grant permissions ---'
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
SELECT o.id, r.id, v.activity, v.view, c.id, v.modality::morbac.modality
FROM (VALUES
('GlobalTech HQ', 'manager', 'create', 'rules', 'always', 'permission'),
('GlobalTech HQ', 'manager', 'delete', 'rules', 'always', 'permission'),
('GlobalTech HQ', 'hr_manager', 'read', 'user_roles', 'always', 'permission'),
('GlobalTech HQ', 'hr_manager', 'create', 'user_roles', 'always', 'permission'),
('GlobalTech HQ', 'hr_manager', 'delete', 'user_roles', 'always', 'permission'),
('GlobalTech HQ', 'director', 'create', 'roles', 'always', 'permission'),
('GlobalTech HQ', 'director', 'read', 'roles', 'always', 'permission'),
('GlobalTech HQ', 'director', 'update', 'roles', 'always', 'permission'),
('GlobalTech HQ', 'director', 'delete', 'roles', 'always', 'permission')
) AS v(org_name, role_name, activity, view, context_name, modality)
JOIN morbac.orgs o ON o.name = v.org_name
JOIN morbac.roles r ON r.org_id = o.id AND r.name = v.role_name
JOIN morbac.contexts c ON c.name = v.context_name;
\echo ''
\echo '--- 3. Verify permissions ---'
SELECT morbac.t('Carol (manager) create rules',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'rules'
), TRUE);
SELECT morbac.t('Carol (manager) delete rules',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'delete', 'rules'
), TRUE);
SELECT morbac.t('Carol (manager) update rules [no rule]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'update', 'rules'
), FALSE);
SELECT morbac.t('Grace (hr_manager) create user_roles',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000007'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'user_roles'
), TRUE);
SELECT morbac.t('Dave (employee) create rules [no rule]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'rules'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 4: Role hierarchy applies
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. Role hierarchy applies ---'
SELECT morbac.t('Bob (director, inherits manager) create rules',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000002'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'rules'
), TRUE);
SELECT morbac.t('Bob (director) create roles [own rule]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000002'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'roles'
), TRUE);
SELECT morbac.t('Alice (CEO) create roles [inherits director]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'roles'
), TRUE);
SELECT morbac.t('Eve (intern) create rules [nothing in hierarchy]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'rules'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 5: Prohibition overrides permission
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Prohibition overrides permission ---'
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
SELECT o.id, r.id, 'create', 'rules', c.id, 'prohibition'
FROM morbac.orgs o
JOIN morbac.roles r ON r.org_id = o.id AND r.name = 'manager'
JOIN morbac.contexts c ON c.name = 'always'
WHERE o.name = 'GlobalTech HQ';
SELECT morbac.t('Carol (manager, prohibited) create rules [prohibition wins]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'create', 'rules'
), FALSE);
SELECT morbac.t('Carol (prohibition only on create) delete rules [still allowed]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'delete', 'rules'
), TRUE);
DELETE FROM morbac.rules
WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND role_id = '20000000-0001-0000-0000-000000000003'
AND activity = 'create' AND view = 'rules'
AND modality = 'prohibition';
-- ---------------------------------------------------------------------------
-- Section 6: assign_role / revoke_role
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 6. assign_role / revoke_role ---'
-- As DB owner (bypasses RLS), assign Karl as intern to verify constraint logic
SELECT morbac.assign_role(
'30000000-0000-0000-0000-000000000011'::uuid, -- Karl
'20000000-0001-0000-0000-000000000005'::uuid, -- intern
'10000000-0000-0000-0000-000000000001'::uuid
);
SELECT morbac.t('Karl assigned intern role via assign_role()',
EXISTS(
SELECT 1 FROM morbac.user_roles
WHERE user_id = '30000000-0000-0000-0000-000000000011'
AND role_id = '20000000-0001-0000-0000-000000000005'
), TRUE);
SELECT morbac.revoke_role(
'30000000-0000-0000-0000-000000000011'::uuid,
'20000000-0001-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
);
SELECT morbac.t('Karl intern role revoked via revoke_role()',
NOT EXISTS(
SELECT 1 FROM morbac.user_roles
WHERE user_id = '30000000-0000-0000-0000-000000000011'
AND role_id = '20000000-0001-0000-0000-000000000005'
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 7: RLS on morbac tables
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. RLS on morbac tables ---'
-- Create a non-superuser role so RLS policies are enforced (superusers bypass RLS by default)
DROP ROLE IF EXISTS morbac_rls_tester;
CREATE ROLE morbac_rls_tester;
GRANT USAGE ON SCHEMA morbac TO morbac_rls_tester;
GRANT SELECT, INSERT, DELETE ON morbac.user_roles TO morbac_rls_tester;
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
SET morbac.user_id = '30000000-0000-0000-0000-000000000007';
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
VALUES (
'30000000-0000-0000-0000-000000000011',
'20000000-0001-0000-0000-000000000005',
'10000000-0000-0000-0000-000000000001'
);
SELECT morbac.t('Grace (hr_manager) inserted user_role via RLS',
EXISTS(
SELECT 1 FROM morbac.user_roles
WHERE user_id = '30000000-0000-0000-0000-000000000011'
AND role_id = '20000000-0001-0000-0000-000000000005'
), TRUE);
DELETE FROM morbac.user_roles
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
SET morbac.user_id = '30000000-0000-0000-0000-000000000004';
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
DO $$
BEGIN
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
VALUES (
'30000000-0000-0000-0000-000000000011',
'20000000-0001-0000-0000-000000000005',
'10000000-0000-0000-0000-000000000001'
);
RAISE NOTICE 'CHECK FAIL: Dave (employee) inserted user_role [should have been blocked by RLS]';
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'CHECK PASS: Dave (employee) blocked from inserting user_role by RLS';
END;
$$;
RESET SESSION AUTHORIZATION;
RESET morbac.user_id;
RESET morbac.org_id;
DROP OWNED BY morbac_rls_tester;
DROP ROLE IF EXISTS morbac_rls_tester;
-- ---------------------------------------------------------------------------
-- Section 8: Rules are org-scoped
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 8. Rules are org-scoped ---'
SELECT morbac.t('Carol (GlobalTech manager) create Engineering rules [wrong org]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'create', 'rules'
), FALSE);
SELECT morbac.t('Alice (GlobalTech CEO) create Engineering roles [org-scoped]',
morbac.is_allowed(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'create', 'roles'
), FALSE);
\echo ''
\echo '=== System Access Tests Completed ==='
+13 -75
View File
@@ -1,5 +1,5 @@
-- =============================================================================
-- Utilities, Derived Roles, RLS, and Policy DSL Tests
-- Utilities, Derived Roles, and RLS Tests
-- =============================================================================
-- Tests miscellaneous utility functions and advanced features:
--
@@ -10,17 +10,15 @@
-- 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. Policy DSL idempotency — compile_policy() is safe to run multiple times
-- 8. Policy DSL error handling — reports errors for missing org/role
-- 9. Derived roles — computed via evaluator function
-- 10. RLS helpers: get_user_orgs, current_org_ids, rls_check() org scoping
-- 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_admin.sql
-- Prerequisites: 00_setup.sql -> 08_system_access.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '09 — UTILITIES, DERIVED ROLES, RLS, POLICY DSL'
\echo '09 — UTILITIES, DERIVED ROLES, RLS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
@@ -327,70 +325,10 @@ SELECT morbac.t('eval_context(end_of_quarter) = TRUE',
TRUE);
-- ---------------------------------------------------------------------------
-- Section 7: Policy DSL — idempotency
-- Section 7: Derived roles
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. Policy DSL idempotency ---'
-- All existing policies are already compiled (compiled=TRUE)
-- Running compile_policy() again should compile 0 new entries
SELECT morbac.t_eq('Re-running compile_policy() gives compiled_count=0 (all already compiled)',
(SELECT compiled_count FROM morbac.compile_policy()),
0);
SELECT morbac.t_eq('Re-running compile_policy() gives error_count=0',
(SELECT error_count FROM morbac.compile_policy()),
0);
-- Adding a new policy and verifying it compiles once but not twice
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name)
VALUES ('GlobalTech HQ', 'employee', 'read', 'contracts', 'permission', 'always');
SELECT morbac.t_eq('compile_policy() compiles 1 new policy entry',
(SELECT compiled_count FROM morbac.compile_policy()),
1);
SELECT morbac.t_eq('compile_policy() is idempotent: compiled_count=0 on second run',
(SELECT compiled_count FROM morbac.compile_policy()),
0);
-- Verify the rule was actually created
SELECT morbac.t_eq('Rule for employee read contracts was created',
(SELECT COUNT(*) FROM morbac.rules r
JOIN morbac.roles ro ON ro.id = r.role_id
WHERE ro.name = 'employee'
AND r.activity = 'read' AND r.view = 'contracts' AND r.modality = 'permission')::bigint,
1);
-- ---------------------------------------------------------------------------
-- Section 8: Policy DSL — error handling
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 8. Policy DSL error handling ---'
-- Insert a policy for a non-existent organization
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name)
VALUES ('Nonexistent Corp', 'employee', 'read', 'documents', 'permission', 'always');
-- Should produce errors (org not found)
SELECT morbac.t('compile_policy: nonexistent org produces errors',
(SELECT error_count > 0 FROM morbac.compile_policy()),
TRUE);
-- Insert a policy with a valid org but nonexistent role
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name)
VALUES ('GlobalTech HQ', 'ghost_role', 'read', 'documents', 'permission', 'always');
-- Should produce errors (role not found)
SELECT morbac.t('compile_policy: nonexistent role produces errors',
(SELECT error_count > 0 FROM morbac.compile_policy()),
TRUE);
-- ---------------------------------------------------------------------------
-- Section 9: Derived roles
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 9. Derived roles ---'
\echo '--- 7. Derived roles ---'
-- Create a derived role: 'senior_employee' — dynamically granted to Dave (only)
INSERT INTO morbac.roles (id, org_id, name, description)
@@ -497,10 +435,10 @@ WHERE user_id = '30000000-0000-0000-0000-000000000004'
AND role_id = '20000000-0001-0000-0000-000000000011';
-- ---------------------------------------------------------------------------
-- Section 10: RLS helper functions
-- Section 8: RLS helper functions
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 10. RLS helpers: get_user_orgs, current_org_ids, rls_check ---'
\echo '--- 8. RLS helpers: get_user_orgs, current_org_ids, rls_check ---'
-- Without session variables set, current_user_id / current_org_id / current_org_ids return NULL
SELECT morbac.t_null('current_user_id() returns NULL without session var',
@@ -593,9 +531,9 @@ SELECT morbac.t('rls_check all-orgs mode: Judy reads documents in Engineering [a
morbac.rls_check('read', 'documents', '10000000-0000-0000-0000-000000000002'),
TRUE);
-- Judy has no role at GlobalTech -> denied even in all-orgs mode
SELECT morbac.t('rls_check all-orgs mode: Judy reads documents in GlobalTech [no role, denied]',
morbac.rls_check('read', 'documents', '10000000-0000-0000-0000-000000000001'),
-- Judy has no access to contracts at GlobalTech (no cross-org rule, no view hierarchy) -> denied
SELECT morbac.t('rls_check all-orgs mode: Judy reads contracts in GlobalTech [no rule, denied]',
morbac.rls_check('read', 'contracts', '10000000-0000-0000-0000-000000000001'),
FALSE);
-- No row org and no session org -> denied
@@ -613,4 +551,4 @@ SELECT morbac.t('rls_check(read, documents) as Karl (no role) [denied]',
RESET morbac.user_id;
\echo ''
\echo '=== Utilities, Derived Roles, RLS, and Policy DSL Tests Completed ==='
\echo '=== Utilities, Derived Roles, and RLS Tests Completed ==='
+259
View File
@@ -0,0 +1,259 @@
-- =============================================================================
-- Scope Rules and Global Cross-Org Rules Tests
-- =============================================================================
-- 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)
--
-- Prerequisites: 00_setup.sql -> 10_activity_view_bindings.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '11 — SCOPE RULES AND GLOBAL CROSS-ORG RULES'
\echo '================================================================'
-- Setup: create a dedicated role for scope tests (avoid polluting existing rules)
INSERT INTO morbac.roles (id, org_id, name, description)
VALUES (
'20000000-0001-0000-0000-000000000012',
'10000000-0000-0000-0000-000000000001',
'analyst',
'Data analyst — scope tests'
);
-- Assign Karl (previously no role) as analyst at GlobalTech
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
VALUES (
'30000000-0000-0000-0000-000000000011',
'20000000-0001-0000-0000-000000000012',
'10000000-0000-0000-0000-000000000001'
);
-- ---------------------------------------------------------------------------
-- Section 1: scope='self' (default) — exact org only
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. scope=self (default) ---'
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, scope)
VALUES (
'c0000000-0000-0000-0000-000000000001',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000012',
'read', 'reports',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission', 'self'
);
SELECT morbac.t('Karl (analyst, scope=self) reads reports in GlobalTech HQ [allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'reports'
), TRUE);
SELECT morbac.t('Karl (analyst, scope=self) reads reports in Engineering [denied, self only]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'read', 'reports'
), FALSE);
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000001';
-- ---------------------------------------------------------------------------
-- Section 2: scope='subtree' — root + all descendants
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. scope=subtree ---'
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, scope)
VALUES (
'c0000000-0000-0000-0000-000000000002',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000012',
'read', 'reports',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission', 'subtree'
);
SELECT morbac.t('Karl (analyst, scope=subtree) reads reports in GlobalTech HQ [allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'reports'
), TRUE);
SELECT morbac.t('Karl (analyst, scope=subtree) reads reports in Engineering [allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'read', 'reports'
), TRUE);
SELECT morbac.t('Karl (analyst, scope=subtree) reads reports in Sales [allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000003'::uuid,
'read', 'reports'
), TRUE);
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000002';
-- ---------------------------------------------------------------------------
-- Section 3: scope='descendants' — children only, NOT self
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. scope=descendants ---'
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, scope)
VALUES (
'c0000000-0000-0000-0000-000000000003',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000012',
'read', 'reports',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission', 'descendants'
);
SELECT morbac.t('Karl (analyst, scope=descendants) reads reports in GlobalTech HQ [denied, not self]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'reports'
), FALSE);
SELECT morbac.t('Karl (analyst, scope=descendants) reads reports in Engineering [allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'read', 'reports'
), TRUE);
SELECT morbac.t('Karl (analyst, scope=descendants) reads reports in Sales [allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000003'::uuid,
'read', 'reports'
), TRUE);
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000003';
-- ---------------------------------------------------------------------------
-- Section 4: scope='children' — direct children only
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. scope=children ---'
-- Add a grandchild org (child of Engineering)
INSERT INTO morbac.orgs (id, name, parent_id)
VALUES (
'10000000-0000-0000-0000-000000000004',
'Backend Team',
'10000000-0000-0000-0000-000000000002'
);
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, scope)
VALUES (
'c0000000-0000-0000-0000-000000000004',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000012',
'read', 'reports',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission', 'children'
);
SELECT morbac.t('Karl (analyst, scope=children) reads reports in Engineering [direct child, allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'read', 'reports'
), TRUE);
SELECT morbac.t('Karl (analyst, scope=children) reads reports in Backend Team [grandchild, denied]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000004'::uuid,
'read', 'reports'
), FALSE);
SELECT morbac.t('Karl (analyst, scope=children) reads reports in GlobalTech HQ [self, denied]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
'read', 'reports'
), FALSE);
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
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Dynamic scope: new org covered automatically ---'
-- Create a subtree-scoped rule at GlobalTech HQ for the analyst role
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, scope)
VALUES (
'c0000000-0000-0000-0000-000000000005',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000012',
'read', 'documents',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission', 'subtree'
);
-- Verify the rule works for existing child orgs
SELECT morbac.t('Karl (analyst) reads documents in Engineering [existing child, allowed]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000002'::uuid,
'read', 'documents'
), TRUE);
-- Add a new org AFTER the rule was created
INSERT INTO morbac.orgs (id, name, parent_id)
VALUES (
'10000000-0000-0000-0000-000000000005',
'Legal Dept',
'10000000-0000-0000-0000-000000000001'
);
-- 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,
'10000000-0000-0000-0000-000000000005'::uuid,
'read', 'documents'
), TRUE);
-- A scope=self rule at GlobalTech HQ does NOT cover Legal Dept
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, scope)
VALUES (
'c0000000-0000-0000-0000-000000000006',
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000012',
'read', 'reports',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission', 'self'
);
SELECT morbac.t('Karl (analyst, scope=self) reads reports in Legal Dept [new org, not covered]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
'10000000-0000-0000-0000-000000000005'::uuid,
'read', 'reports'
), FALSE);
-- Cleanup
DELETE FROM morbac.rules WHERE id IN ('c0000000-0000-0000-0000-000000000005', 'c0000000-0000-0000-0000-000000000006');
DELETE FROM morbac.orgs WHERE id = '10000000-0000-0000-0000-000000000005';
\echo ''
\echo '=== Scope Rules and Global Cross-Org Rules Tests Completed ==='