feat(all): add scope handling in rules
This commit is contained in:
+46
-72
@@ -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)'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ==='
|
||||
@@ -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
@@ -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 ==='
|
||||
|
||||
@@ -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 ==='
|
||||
Reference in New Issue
Block a user