feat(tests): add more tests and make them pass
This commit is contained in:
@@ -67,10 +67,10 @@ BEGIN
|
||||
WHERE ur.user_id = p_user_id
|
||||
AND ur.org_id = p_org_id
|
||||
UNION
|
||||
-- Recursive case: senior roles (roles that inherit from assigned roles)
|
||||
SELECT rh.senior_role_id, er.depth + 1
|
||||
-- Recursive case: junior roles (inherited by the user's assigned roles)
|
||||
SELECT rh.junior_role_id, er.depth + 1
|
||||
FROM effective_roles er
|
||||
INNER JOIN morbac.role_hierarchy rh ON rh.junior_role_id = er.role_id
|
||||
INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = er.role_id
|
||||
)
|
||||
SELECT DISTINCT ON (effective_roles.role_id) effective_roles.role_id, effective_roles.depth
|
||||
FROM effective_roles
|
||||
@@ -207,10 +207,10 @@ BEGIN
|
||||
|
||||
UNION
|
||||
|
||||
-- Role hierarchy (senior roles)
|
||||
SELECT rh.senior_role_id, er.source || '_inherited', er.depth + 1
|
||||
-- Role hierarchy (junior roles inherited by user's roles)
|
||||
SELECT rh.junior_role_id, er.source || '_inherited', er.depth + 1
|
||||
FROM effective_roles er
|
||||
INNER JOIN morbac.role_hierarchy rh ON rh.junior_role_id = er.role_id
|
||||
INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = er.role_id
|
||||
)
|
||||
SELECT DISTINCT ON (effective_roles.role_id) effective_roles.role_id, effective_roles.source, effective_roles.depth
|
||||
FROM effective_roles
|
||||
@@ -277,9 +277,9 @@ BEGIN
|
||||
FROM inherited_roles ir
|
||||
INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = ir.role_id
|
||||
)
|
||||
SELECT DISTINCT ON (role_id) role_id, depth
|
||||
SELECT DISTINCT ON (inherited_roles.role_id) inherited_roles.role_id, inherited_roles.depth
|
||||
FROM inherited_roles
|
||||
ORDER BY role_id, depth;
|
||||
ORDER BY inherited_roles.role_id, inherited_roles.depth;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
+8
-8
@@ -4,7 +4,7 @@
|
||||
-- Helper functions for Row-Level Security policies
|
||||
-- Compatible with PostgREST
|
||||
|
||||
-- Get current user ID from request header
|
||||
-- Get current user ID from session variable
|
||||
CREATE OR REPLACE FUNCTION morbac.current_user_id()
|
||||
RETURNS UUID
|
||||
LANGUAGE plpgsql
|
||||
@@ -13,8 +13,8 @@ AS $$
|
||||
DECLARE
|
||||
v_user_id TEXT;
|
||||
BEGIN
|
||||
-- Read from PostgREST request.header.x-user-id setting
|
||||
v_user_id := current_setting('request.header.x-user-id', TRUE);
|
||||
-- Read from morbac.user_id GUC (set via SET morbac.user_id = '...' or PostgREST header mapping)
|
||||
v_user_id := current_setting('morbac.user_id', TRUE);
|
||||
|
||||
IF v_user_id IS NULL OR v_user_id = '' THEN
|
||||
RETURN NULL;
|
||||
@@ -28,9 +28,9 @@ END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.current_user_id() IS
|
||||
'Returns current user ID from request.header.x-user-id (PostgREST compatible)';
|
||||
'Returns current user ID from morbac.user_id session variable';
|
||||
|
||||
-- Get current organization ID from request header
|
||||
-- Get current organization ID from session variable
|
||||
CREATE OR REPLACE FUNCTION morbac.current_org_id()
|
||||
RETURNS UUID
|
||||
LANGUAGE plpgsql
|
||||
@@ -39,8 +39,8 @@ AS $$
|
||||
DECLARE
|
||||
v_org_id TEXT;
|
||||
BEGIN
|
||||
-- Read from PostgREST request.header.x-org-id setting
|
||||
v_org_id := current_setting('request.header.x-org-id', TRUE);
|
||||
-- Read from morbac.org_id GUC (set via SET morbac.org_id = '...' or PostgREST header mapping)
|
||||
v_org_id := current_setting('morbac.org_id', TRUE);
|
||||
|
||||
IF v_org_id IS NULL OR v_org_id = '' THEN
|
||||
RETURN NULL;
|
||||
@@ -54,7 +54,7 @@ END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.current_org_id() IS
|
||||
'Returns current organization ID from request.header.x-org-id (PostgREST compatible)';
|
||||
'Returns current organization ID from morbac.org_id session variable';
|
||||
|
||||
-- RLS check function
|
||||
CREATE OR REPLACE FUNCTION morbac.rls_check(
|
||||
|
||||
+278
-120
@@ -1,186 +1,344 @@
|
||||
-- =============================================================================
|
||||
-- Test Setup - Creates test data for all tests
|
||||
-- Test Setup — GlobalTech Inc. Company Scenario
|
||||
-- =============================================================================
|
||||
-- This file creates organizations, roles, users, activities, views, and contexts
|
||||
-- that are used across multiple test files.
|
||||
-- This file establishes the full company structure used across all test files:
|
||||
--
|
||||
-- GlobalTech HQ (root)
|
||||
-- ├── Engineering Dept (child)
|
||||
-- └── Sales Dept (child)
|
||||
--
|
||||
-- Role hierarchy in GlobalTech (senior -> junior, i.e. senior inherits junior perms):
|
||||
-- ceo -> director -> manager -> employee -> intern
|
||||
-- + specialized roles: auditor, accountant, hr_manager, contractor, compliance_officer
|
||||
--
|
||||
-- Role hierarchy in Engineering:
|
||||
-- tech_lead -> engineer
|
||||
--
|
||||
-- Users:
|
||||
-- Alice — CEO at GlobalTech
|
||||
-- Bob — Director at GlobalTech
|
||||
-- Carol — Manager at GlobalTech
|
||||
-- Dave — Employee at GlobalTech
|
||||
-- Eve — Intern at GlobalTech
|
||||
-- Frank — Contractor at GlobalTech
|
||||
-- Grace — HR Manager at GlobalTech
|
||||
-- Heidi — Auditor at GlobalTech
|
||||
-- Ivan — Accountant at GlobalTech
|
||||
-- Judy — Engineer at Engineering + Sales Rep at Sales (multi-org)
|
||||
-- Karl — No role (unauthorized user)
|
||||
-- Leo — Employee at GlobalTech (used for delegation target)
|
||||
-- =============================================================================
|
||||
|
||||
-- Clean up any previous test
|
||||
-- Clean slate
|
||||
DROP EXTENSION IF EXISTS pgmorbac CASCADE;
|
||||
DROP SCHEMA IF EXISTS morbac CASCADE;
|
||||
|
||||
-- Install the extension
|
||||
CREATE EXTENSION pgmorbac;
|
||||
|
||||
-- Verify schema and tables exist
|
||||
\echo '=== Schema and Tables Created ==='
|
||||
SELECT schemaname, tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'morbac'
|
||||
ORDER BY tablename;
|
||||
\echo '=== Setup: Verify schema created ==='
|
||||
SELECT tablename FROM pg_tables WHERE schemaname = 'morbac' ORDER BY tablename;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ORGANIZATIONS
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: Organizations ==='
|
||||
|
||||
-- Create two organizations
|
||||
INSERT INTO morbac.orgs (id, name) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', 'Acme Corp'),
|
||||
('22222222-2222-2222-2222-222222222222', 'Beta Inc');
|
||||
('10000000-0000-0000-0000-000000000001', 'GlobalTech HQ'),
|
||||
('10000000-0000-0000-0000-000000000002', 'Engineering Dept'),
|
||||
('10000000-0000-0000-0000-000000000003', 'Sales Dept');
|
||||
|
||||
-- Create hierarchical organization (Acme Subsidiary under Acme Corp)
|
||||
INSERT INTO morbac.orgs (id, name, parent_id) VALUES
|
||||
('33333333-3333-3333-3333-333333333333', 'Acme Subsidiary', '11111111-1111-1111-1111-111111111111');
|
||||
-- Engineering and Sales are children of GlobalTech
|
||||
UPDATE morbac.orgs SET parent_id = '10000000-0000-0000-0000-000000000001'
|
||||
WHERE id IN (
|
||||
'10000000-0000-0000-0000-000000000002',
|
||||
'10000000-0000-0000-0000-000000000003'
|
||||
);
|
||||
|
||||
SELECT
|
||||
o.name,
|
||||
COALESCE(p.name, 'None') as parent_organization
|
||||
FROM morbac.orgs o
|
||||
LEFT JOIN morbac.orgs p ON o.parent_id = p.id
|
||||
SELECT o.name, COALESCE(p.name, '—') AS parent
|
||||
FROM morbac.orgs o LEFT JOIN morbac.orgs p ON o.parent_id = p.id
|
||||
ORDER BY o.parent_id NULLS FIRST, o.name;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ROLES
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: Roles ==='
|
||||
|
||||
-- Create roles in Acme Corp
|
||||
-- GlobalTech HQ roles
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', 'admin', 'Administrator role'),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'employee', 'Regular employee'),
|
||||
('cccccccc-cccc-cccc-cccc-cccccccccccc', '11111111-1111-1111-1111-111111111111', 'contractor', 'External contractor'),
|
||||
('ffffffff-ffff-ffff-ffff-ffffffffffff', '11111111-1111-1111-1111-111111111111', 'viewer', 'Read-only viewer');
|
||||
('20000000-0001-0000-0000-000000000001', '10000000-0000-0000-0000-000000000001', 'ceo', 'Chief Executive Officer'),
|
||||
('20000000-0001-0000-0000-000000000002', '10000000-0000-0000-0000-000000000001', 'director', 'Senior Director'),
|
||||
('20000000-0001-0000-0000-000000000003', '10000000-0000-0000-0000-000000000001', 'manager', 'Department Manager'),
|
||||
('20000000-0001-0000-0000-000000000004', '10000000-0000-0000-0000-000000000001', 'employee', 'Regular Employee'),
|
||||
('20000000-0001-0000-0000-000000000005', '10000000-0000-0000-0000-000000000001', 'intern', 'Intern'),
|
||||
('20000000-0001-0000-0000-000000000006', '10000000-0000-0000-0000-000000000001', 'auditor', 'Internal Auditor'),
|
||||
('20000000-0001-0000-0000-000000000007', '10000000-0000-0000-0000-000000000001', 'accountant', 'Accountant'),
|
||||
('20000000-0001-0000-0000-000000000008', '10000000-0000-0000-0000-000000000001', 'hr_manager', 'Human Resources Manager'),
|
||||
('20000000-0001-0000-0000-000000000009', '10000000-0000-0000-0000-000000000001', 'contractor', 'External Contractor'),
|
||||
('20000000-0001-0000-0000-000000000010', '10000000-0000-0000-0000-000000000001', 'compliance_officer', 'Compliance Officer');
|
||||
|
||||
-- Create roles in Beta Inc
|
||||
-- Engineering Dept roles
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('dddddddd-dddd-dddd-dddd-dddddddddddd', '22222222-2222-2222-2222-222222222222', 'manager', 'Manager role'),
|
||||
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '22222222-2222-2222-2222-222222222222', 'staff', 'Staff member');
|
||||
('20000000-0002-0000-0000-000000000001', '10000000-0000-0000-0000-000000000002', 'tech_lead', 'Technical Lead'),
|
||||
('20000000-0002-0000-0000-000000000002', '10000000-0000-0000-0000-000000000002', 'engineer', 'Software Engineer');
|
||||
|
||||
SELECT r.name as role, o.name as organization
|
||||
FROM morbac.roles r
|
||||
JOIN morbac.orgs o ON r.org_id = o.id
|
||||
-- Sales Dept roles
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('20000000-0003-0000-0000-000000000001', '10000000-0000-0000-0000-000000000003', 'sales_manager', 'Sales Manager'),
|
||||
('20000000-0003-0000-0000-000000000002', '10000000-0000-0000-0000-000000000003', 'sales_rep', 'Sales Representative');
|
||||
|
||||
SELECT r.name AS role, o.name AS organization
|
||||
FROM morbac.roles r JOIN morbac.orgs o ON r.org_id = o.id
|
||||
ORDER BY o.name, r.name;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ROLE HIERARCHY (GlobalTech)
|
||||
-- Senior role inherits all permissions granted to junior role
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: User Role Assignments ==='
|
||||
\echo '=== Setup: Role Hierarchy (GlobalTech HQ) ==='
|
||||
|
||||
-- User Alice (admin at Acme)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('aaaaaaaa-0000-0000-0000-000000000001', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111');
|
||||
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id) VALUES
|
||||
-- CEO inherits from Director
|
||||
('20000000-0001-0000-0000-000000000001', '20000000-0001-0000-0000-000000000002'),
|
||||
-- Director inherits from Manager
|
||||
('20000000-0001-0000-0000-000000000002', '20000000-0001-0000-0000-000000000003'),
|
||||
-- Manager inherits from Employee
|
||||
('20000000-0001-0000-0000-000000000003', '20000000-0001-0000-0000-000000000004'),
|
||||
-- Employee inherits from Intern
|
||||
('20000000-0001-0000-0000-000000000004', '20000000-0001-0000-0000-000000000005');
|
||||
|
||||
-- User Bob (employee at Acme)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('bbbbbbbb-0000-0000-0000-000000000002', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111');
|
||||
|
||||
-- User Charlie (contractor at Acme AND staff at Beta - multi-org user)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('cccccccc-0000-0000-0000-000000000003', 'cccccccc-cccc-cccc-cccc-cccccccccccc', '11111111-1111-1111-1111-111111111111'),
|
||||
('cccccccc-0000-0000-0000-000000000003', 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '22222222-2222-2222-2222-222222222222');
|
||||
|
||||
-- User Diana (manager at Beta)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('dddddddd-0000-0000-0000-000000000004', 'dddddddd-dddd-dddd-dddd-dddddddddddd', '22222222-2222-2222-2222-222222222222');
|
||||
-- Engineering: tech_lead inherits from engineer
|
||||
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id) VALUES
|
||||
('20000000-0002-0000-0000-000000000001', '20000000-0002-0000-0000-000000000002');
|
||||
|
||||
SELECT
|
||||
ur.user_id,
|
||||
r.name as role,
|
||||
o.name as organization
|
||||
sr.name AS senior_role,
|
||||
jr.name AS junior_role
|
||||
FROM morbac.role_hierarchy rh
|
||||
JOIN morbac.roles sr ON sr.id = rh.senior_role_id
|
||||
JOIN morbac.roles jr ON jr.id = rh.junior_role_id
|
||||
ORDER BY sr.name;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- USER — ROLE ASSIGNMENTS
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: User-Role Assignments ==='
|
||||
|
||||
-- GlobalTech HQ users
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
-- Alice: CEO
|
||||
('30000000-0000-0000-0000-000000000001', '20000000-0001-0000-0000-000000000001', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Bob: Director
|
||||
('30000000-0000-0000-0000-000000000002', '20000000-0001-0000-0000-000000000002', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Carol: Manager
|
||||
('30000000-0000-0000-0000-000000000003', '20000000-0001-0000-0000-000000000003', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Dave: Employee
|
||||
('30000000-0000-0000-0000-000000000004', '20000000-0001-0000-0000-000000000004', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Eve: Intern
|
||||
('30000000-0000-0000-0000-000000000005', '20000000-0001-0000-0000-000000000005', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Frank: Contractor
|
||||
('30000000-0000-0000-0000-000000000006', '20000000-0001-0000-0000-000000000009', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Grace: HR Manager
|
||||
('30000000-0000-0000-0000-000000000007', '20000000-0001-0000-0000-000000000008', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Heidi: Auditor
|
||||
('30000000-0000-0000-0000-000000000008', '20000000-0001-0000-0000-000000000006', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Ivan: Accountant
|
||||
('30000000-0000-0000-0000-000000000009', '20000000-0001-0000-0000-000000000007', '10000000-0000-0000-0000-000000000001'),
|
||||
-- Leo: Employee (delegation target)
|
||||
('30000000-0000-0000-0000-000000000012', '20000000-0001-0000-0000-000000000004', '10000000-0000-0000-0000-000000000001');
|
||||
|
||||
-- Judy: Engineer at Engineering + Sales Rep at Sales (multi-org user)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('30000000-0000-0000-0000-000000000010', '20000000-0002-0000-0000-000000000002', '10000000-0000-0000-0000-000000000002'),
|
||||
('30000000-0000-0000-0000-000000000010', '20000000-0003-0000-0000-000000000002', '10000000-0000-0000-0000-000000000003');
|
||||
|
||||
-- Karl: no role (unauthorized user — intentionally not assigned any role)
|
||||
|
||||
SELECT ur.user_id, r.name AS role, o.name AS organization
|
||||
FROM morbac.user_roles ur
|
||||
JOIN morbac.roles r ON ur.role_id = r.id
|
||||
JOIN morbac.orgs o ON ur.org_id = o.id
|
||||
ORDER BY ur.user_id;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ACTIVITIES AND VIEWS
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: Activities and Views ==='
|
||||
|
||||
INSERT INTO morbac.activities (name, description) VALUES
|
||||
('read', 'Read/view data'),
|
||||
('write', 'Create/modify data'),
|
||||
('delete', 'Delete data'),
|
||||
('approve', 'Approve actions');
|
||||
('read', 'Read / view data'),
|
||||
('write', 'Create or modify data'),
|
||||
('delete', 'Delete data'),
|
||||
('approve', 'Approve requests or documents'),
|
||||
('export', 'Export data to external format'),
|
||||
('audit', 'Audit trail review'),
|
||||
('manage', 'Full administrative control');
|
||||
|
||||
INSERT INTO morbac.views (name, description) VALUES
|
||||
('documents', 'Document resources'),
|
||||
('reports', 'Report resources'),
|
||||
('sensitive_data', 'Sensitive/confidential data'),
|
||||
('temp_documents', 'Temporary documents'),
|
||||
('future_documents', 'Future documents'),
|
||||
('active_documents', 'Active documents');
|
||||
('documents', 'General company documents'),
|
||||
('reports', 'Business reports'),
|
||||
('financial_data','Financial records'),
|
||||
('hr_data', 'Human resources data'),
|
||||
('contracts', 'Legal contracts'),
|
||||
('audit_logs', 'System audit logs'),
|
||||
('public_data', 'Publicly accessible data');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- CONTEXTS
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: Additional Contexts ==='
|
||||
\echo '=== Setup: Contexts ==='
|
||||
|
||||
-- Business hours context (simplified - always true for this test)
|
||||
CREATE OR REPLACE FUNCTION morbac.context_business_hours()
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN TRUE;
|
||||
END;
|
||||
-- business_hours: always TRUE for tests (simulates daytime access)
|
||||
CREATE OR REPLACE FUNCTION morbac.ctx_business_hours()
|
||||
RETURNS BOOLEAN LANGUAGE plpgsql STABLE AS $$
|
||||
BEGIN RETURN TRUE; END;
|
||||
$$;
|
||||
|
||||
-- after_hours: always FALSE for tests (simulates outside-hours access)
|
||||
CREATE OR REPLACE FUNCTION morbac.ctx_after_hours()
|
||||
RETURNS BOOLEAN LANGUAGE plpgsql STABLE AS $$
|
||||
BEGIN RETURN FALSE; END;
|
||||
$$;
|
||||
|
||||
-- end_of_quarter: always TRUE for tests (simulates quarterly reporting period)
|
||||
CREATE OR REPLACE FUNCTION morbac.ctx_end_of_quarter()
|
||||
RETURNS BOOLEAN LANGUAGE plpgsql STABLE AS $$
|
||||
BEGIN RETURN TRUE; END;
|
||||
$$;
|
||||
|
||||
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||
('business_hours', 'During business hours', 'morbac.context_business_hours'::regproc);
|
||||
('business_hours', 'During business hours (Mon–Fri 9–17)', 'morbac.ctx_business_hours'::regproc),
|
||||
('after_hours', 'Outside business hours', 'morbac.ctx_after_hours'::regproc),
|
||||
('end_of_quarter', 'End-of-quarter reporting window', 'morbac.ctx_end_of_quarter'::regproc);
|
||||
|
||||
-- Weekend context (always false for test)
|
||||
CREATE OR REPLACE FUNCTION morbac.context_weekend()
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||
('weekend', 'During weekend', 'morbac.context_weekend'::regproc);
|
||||
SELECT name, description FROM morbac.contexts ORDER BY name;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- POLICIES (via Policy DSL — resolved by compile_policy)
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: Policy DSL ==='
|
||||
\echo '=== Setup: Policies ==='
|
||||
|
||||
-- Acme Corp policies
|
||||
-- GlobalTech HQ policies
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||
-- Admins can do everything on documents
|
||||
('Acme Corp', 'admin', 'read', 'documents', 'permission', 'always'),
|
||||
('Acme Corp', 'admin', 'write', 'documents', 'permission', 'always'),
|
||||
('Acme Corp', 'admin', 'delete', 'documents', 'permission', 'always'),
|
||||
-- Intern: read public data only
|
||||
('GlobalTech HQ', 'intern', 'read', 'public_data', 'permission', 'always'),
|
||||
|
||||
-- Employees can read and write documents
|
||||
('Acme Corp', 'employee', 'read', 'documents', 'permission', 'always'),
|
||||
('Acme Corp', 'employee', 'write', 'documents', 'permission', 'business_hours'),
|
||||
-- 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'),
|
||||
|
||||
-- Contractors can only read documents
|
||||
('Acme Corp', 'contractor', 'read', 'documents', 'permission', 'always'),
|
||||
-- Obligation: employees must review reports weekly
|
||||
('GlobalTech HQ', 'employee', 'read', 'reports', 'obligation', 'always'),
|
||||
|
||||
-- PROHIBITION: Contractors cannot access sensitive data
|
||||
('Acme Corp', 'contractor', 'read', 'sensitive_data', 'prohibition', 'always'),
|
||||
('Acme Corp', 'contractor', 'write', 'sensitive_data', 'prohibition', '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'),
|
||||
|
||||
-- OBLIGATION: Employees must review reports weekly
|
||||
('Acme Corp', 'employee', 'read', 'reports', 'obligation', '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'),
|
||||
|
||||
-- Admins can access reports
|
||||
('Acme Corp', 'admin', 'read', 'reports', 'permission', 'always'),
|
||||
('Acme Corp', 'admin', 'approve', 'reports', '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'),
|
||||
|
||||
-- Beta Inc policies
|
||||
-- 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
|
||||
('GlobalTech HQ', 'employee', 'read', 'reports', 'recommendation','end_of_quarter');
|
||||
|
||||
-- Engineering Dept policies
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||
-- Managers have full access
|
||||
('Beta Inc', 'manager', 'read', 'documents', 'permission', 'always'),
|
||||
('Beta Inc', 'manager', 'write', 'documents', 'permission', 'always'),
|
||||
('Beta Inc', 'manager', 'read', 'reports', 'permission', 'always'),
|
||||
('Engineering Dept', 'engineer', 'read', 'documents', 'permission', 'always'),
|
||||
('Engineering Dept', 'engineer', 'write', 'documents', 'permission', 'always'),
|
||||
('Engineering Dept', 'tech_lead', 'approve', 'documents', 'permission', 'always');
|
||||
|
||||
-- Staff can read documents
|
||||
('Beta Inc', 'staff', 'read', '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');
|
||||
|
||||
-- PROHIBITION: No document modifications on weekends (example)
|
||||
('Beta Inc', 'staff', 'write', 'documents', 'prohibition', 'weekend'),
|
||||
|
||||
-- RECOMMENDATION: Staff should review reports
|
||||
('Beta Inc', 'staff', 'read', 'reports', 'recommendation', 'always');
|
||||
|
||||
-- Compile policy
|
||||
-- Compile all policies into rules
|
||||
\echo ''
|
||||
\echo '=== Setup: Compiling Policies ==='
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Test infrastructure
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Setup: Test Infrastructure ==='
|
||||
|
||||
-- Disable auth cache so state changes between calls are always reflected
|
||||
SELECT morbac.set_config('cache_ttl_seconds', '0');
|
||||
|
||||
-- morbac.t(label, actual, expected) — boolean assertion
|
||||
CREATE OR REPLACE FUNCTION morbac.t(label TEXT, actual BOOLEAN, expect BOOLEAN)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual IS NOT DISTINCT FROM expect
|
||||
THEN 'CHECK PASS: ' || label
|
||||
ELSE 'CHECK FAIL: ' || label
|
||||
|| ' (expected ' || expect::text || ', got ' || COALESCE(actual::text, 'NULL') || ')'
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- morbac.t_null(label, actual) — assert value is NULL
|
||||
CREATE OR REPLACE FUNCTION morbac.t_null(label TEXT, actual TEXT)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual IS NULL
|
||||
THEN 'CHECK PASS: ' || label
|
||||
ELSE 'CHECK FAIL: ' || label || ' (expected NULL, got: ' || actual || ')'
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- morbac.t_not_null(label, actual) — assert value is NOT NULL
|
||||
CREATE OR REPLACE FUNCTION morbac.t_not_null(label TEXT, actual TEXT)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual IS NOT NULL
|
||||
THEN 'CHECK PASS: ' || label
|
||||
ELSE 'CHECK FAIL: ' || label || ' (expected a value, got NULL)'
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- morbac.t_eq(label, actual, expected) — assert two numeric values are equal
|
||||
CREATE OR REPLACE FUNCTION morbac.t_eq(label TEXT, actual NUMERIC, expect NUMERIC)
|
||||
RETURNS TEXT LANGUAGE sql STABLE AS $$
|
||||
SELECT CASE WHEN actual = expect
|
||||
THEN 'CHECK PASS: ' || label
|
||||
ELSE 'CHECK FAIL: ' || label
|
||||
|| ' (expected ' || expect || ', got ' || actual || ')'
|
||||
END;
|
||||
$$;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Setup Complete ==='
|
||||
\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 ' 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)'
|
||||
|
||||
+406
-167
@@ -1,179 +1,418 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Authorization Decision Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers authorization decision tests including:
|
||||
-- - Basic authorization decisions (is_allowed function)
|
||||
-- - Role hierarchy inheritance
|
||||
-- - Organization hierarchy
|
||||
-- - Prohibition precedence
|
||||
-- - Multi-organization user access
|
||||
-- =============================================================================
|
||||
-- Authorization Decision Tests
|
||||
-- =============================================================================
|
||||
-- Tests the core is_allowed() function across a variety of scenarios.
|
||||
-- No activity or view hierarchy is active yet (added in 02_hierarchies.sql).
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- Users and their roles at GlobalTech HQ:
|
||||
-- Alice — ceo (permission: inherits all via hierarchy)
|
||||
-- Bob — director (inherits manager -> employee -> intern)
|
||||
-- Carol — manager (inherits employee -> intern)
|
||||
-- Dave — employee (inherits intern)
|
||||
-- Eve — intern
|
||||
-- Frank — contractor (has prohibition on financial/hr data)
|
||||
-- Grace — hr_manager
|
||||
-- Heidi — auditor
|
||||
-- Ivan — accountant
|
||||
-- Judy — engineer@Engineering + sales_rep@Sales (multi-org)
|
||||
-- Karl — no role
|
||||
-- Leo — employee
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Authorization Decisions ==='
|
||||
\echo '================================================================'
|
||||
\echo '01 — CORE AUTHORIZATION DECISIONS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Test Alice (admin at Acme)
|
||||
\echo 'Alice (admin at Acme Corp):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Basic permission grants
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Basic permission grants ---'
|
||||
|
||||
-- Dave (employee) can read documents — has explicit permission
|
||||
SELECT morbac.t('Dave (employee) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) can write documents — context is business_hours (evaluates TRUE)
|
||||
SELECT morbac.t('Dave (employee) writes documents [business_hours context=true]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'write', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) can read reports
|
||||
SELECT morbac.t('Dave (employee) reads reports',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'reports'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) can read public data only
|
||||
SELECT morbac.t('Eve (intern) reads public_data',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'public_data'
|
||||
), TRUE);
|
||||
|
||||
-- Grace (hr_manager) can write hr_data
|
||||
SELECT morbac.t('Grace (hr_manager) writes hr_data',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000007'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'write', 'hr_data'
|
||||
), TRUE);
|
||||
|
||||
-- Heidi (auditor) can read financial_data
|
||||
SELECT morbac.t('Heidi (auditor) reads financial_data',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000008'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- Ivan (accountant) can write financial_data
|
||||
SELECT morbac.t('Ivan (accountant) writes financial_data',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000009'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'write', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Default deny — no rule exists for the combination
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Default deny (no permission rule) ---'
|
||||
|
||||
-- Dave (employee) cannot delete documents — no delete permission for employee
|
||||
SELECT morbac.t('Dave (employee) deletes documents [no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'delete', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Eve (intern) cannot read documents — intern only has public_data permission
|
||||
SELECT morbac.t('Eve (intern) reads documents [intern has no docs permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Dave (employee) cannot read financial_data — no rule for employee -> financial_data
|
||||
SELECT morbac.t('Dave (employee) reads financial_data [no permission before view hierarchy]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- Grace (hr_manager) cannot read audit_logs — no rule for hr_manager -> audit_logs
|
||||
SELECT morbac.t('Grace (hr_manager) reads audit_logs [no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000007'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'audit_logs'
|
||||
), FALSE);
|
||||
|
||||
-- Dave (employee) cannot approve documents — no approve permission for employee
|
||||
SELECT morbac.t('Dave (employee) approves documents [no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Default deny — user has no role at all
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Default deny (user has no role) ---'
|
||||
|
||||
-- Karl has no role anywhere — all actions denied
|
||||
SELECT morbac.t('Karl (no role) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Completely unknown user UUID — EXPECT FALSE
|
||||
SELECT morbac.t('Unknown user reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Prohibition overrides permission (prohibition precedence)
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. Prohibition overrides permission ---'
|
||||
|
||||
-- Frank (contractor) reads documents — permission granted, no prohibition
|
||||
SELECT morbac.t('Frank (contractor) reads documents [has permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Frank (contractor) reads financial_data — PROHIBITED
|
||||
SELECT morbac.t('Frank (contractor) reads financial_data [prohibited]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- Frank (contractor) reads hr_data — prohibited
|
||||
SELECT morbac.t('Frank (contractor) reads hr_data [prohibited]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'hr_data'
|
||||
), FALSE);
|
||||
|
||||
-- Add a permission for contractor on financial_data, then confirm prohibition still wins
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000009', -- contractor
|
||||
'read', 'financial_data',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- Prohibition must always win over permission
|
||||
SELECT morbac.t('Frank (contractor) reads financial_data after adding permission [prohibition wins]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- Clean up the extra rule
|
||||
DELETE FROM morbac.rules
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000009'
|
||||
AND activity = 'read' AND view = 'financial_data' AND modality = 'permission';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: Context filtering — rule only applies when context is TRUE
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Context filtering ---'
|
||||
|
||||
-- Add a rule that only applies after_hours (context evaluates FALSE)
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004', -- employee
|
||||
'export', 'documents',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'after_hours'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- Context evaluates FALSE so the rule does not apply
|
||||
SELECT morbac.t('Dave (employee) exports documents [after_hours context=false]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'export', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Replace with business_hours context (evaluates TRUE)
|
||||
UPDATE morbac.rules
|
||||
SET context_id = (SELECT id FROM morbac.contexts WHERE name = 'business_hours')
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000004'
|
||||
AND activity = 'export' AND view = 'documents';
|
||||
|
||||
-- Context evaluates TRUE
|
||||
SELECT morbac.t('Dave (employee) exports documents [business_hours context=true]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'export', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Clean up the extra rule
|
||||
DELETE FROM morbac.rules
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000004'
|
||||
AND activity = 'export' AND view = 'documents';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: Wrong organization — user has no role in the target org
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. Wrong organization ---'
|
||||
|
||||
-- Dave (employee at GlobalTech) has no role in Engineering Dept
|
||||
SELECT morbac.t('Dave (GlobalTech employee) reads Engineering documents [no role in Engineering]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Judy (engineer at Engineering) has no role in GlobalTech
|
||||
SELECT morbac.t('Judy (Engineering engineer) reads GlobalTech financial_data [no role in GlobalTech]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Multi-organization user — access scoped to each org independently
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 7. Multi-org user (Judy) ---'
|
||||
|
||||
-- Judy is engineer at Engineering — can read documents there
|
||||
SELECT morbac.t('Judy (engineer) reads Engineering documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Judy is sales_rep at Sales — can write contracts there
|
||||
SELECT morbac.t('Judy (sales_rep) writes Sales contracts',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000003'::uuid,
|
||||
'write', 'contracts'
|
||||
), TRUE);
|
||||
|
||||
-- Judy's engineering permissions do NOT carry to Sales
|
||||
SELECT morbac.t('Judy writes documents at Sales [engineer perms dont carry over]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000003'::uuid,
|
||||
'write', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 8: Role hierarchy inheritance
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 8. Role hierarchy inheritance ---'
|
||||
|
||||
-- Carol (manager) inherits employee permissions — can read documents (employee perm)
|
||||
SELECT morbac.t('Carol (manager, inherits employee) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Carol (manager) has own permission — can approve documents
|
||||
SELECT morbac.t('Carol (manager) approves documents [own permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Bob (director) inherits manager -> employee chain — can read documents
|
||||
SELECT morbac.t('Bob (director, inherits manager+employee) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000002'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Bob (director) inherits manager — can approve documents
|
||||
SELECT morbac.t('Bob (director, inherits manager) approves documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000002'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Alice (CEO) inherits the entire hierarchy — can do everything below
|
||||
SELECT morbac.t('Alice (CEO, inherits all) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
SELECT morbac.t('Alice (CEO, inherits all) approves documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
SELECT morbac.t('Alice (CEO, inherits all) deletes documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'delete', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) cannot approve — intern has no approve permission
|
||||
SELECT morbac.t('Eve (intern) approves documents [intern has no approve permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 9: is_allowed_nocache produces same result as is_allowed
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 9. is_allowed vs is_allowed_nocache consistency ---'
|
||||
|
||||
SELECT morbac.t('Dave reads documents: cached = nocache',
|
||||
morbac.is_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
) IS NOT DISTINCT FROM morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
SELECT
|
||||
'delete documents' as action,
|
||||
SELECT morbac.t('Dave deletes documents: cached = nocache',
|
||||
morbac.is_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'delete',
|
||||
'documents'
|
||||
) as allowed;
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'delete', 'documents'
|
||||
) IS NOT DISTINCT FROM morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'delete', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Test Bob (employee at Acme)
|
||||
\echo ''
|
||||
\echo 'Bob (employee at Acme Corp):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
SELECT morbac.t('Frank reads financial_data (prohibited): cached = nocache',
|
||||
morbac.is_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
SELECT
|
||||
'delete documents' as action,
|
||||
morbac.is_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'delete',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
-- Test Charlie (contractor at Acme)
|
||||
\echo ''
|
||||
\echo 'Charlie (contractor at Acme Corp):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
SELECT
|
||||
'write documents' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'write',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
-- Test PROHIBITION precedence
|
||||
\echo ''
|
||||
\echo 'Testing PROHIBITION PRECEDENCE:'
|
||||
\echo 'Charlie (contractor) trying to read sensitive_data (should be DENIED by prohibition):'
|
||||
SELECT
|
||||
'read sensitive_data' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'sensitive_data'
|
||||
) as allowed;
|
||||
|
||||
-- Test Charlie at Beta Inc (multi-org user)
|
||||
\echo ''
|
||||
\echo 'Charlie as staff at Beta Inc:'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
-- Test Diana (manager at Beta)
|
||||
\echo ''
|
||||
\echo 'Diana (manager at Beta Inc):'
|
||||
SELECT
|
||||
'write documents' as action,
|
||||
morbac.is_allowed(
|
||||
'dddddddd-0000-0000-0000-000000000004'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||
'write',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Role Hierarchy Inheritance ==='
|
||||
|
||||
-- Assign user Eve only viewer role
|
||||
\echo 'Creating user Eve with only viewer role at Acme:'
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('eeeeeeee-0000-0000-0000-000000000005', 'ffffffff-ffff-ffff-ffff-ffffffffffff', '11111111-1111-1111-1111-111111111111');
|
||||
|
||||
-- Add permission for viewer role
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality) VALUES
|
||||
('Acme Corp', 'viewer', 'read', 'documents', 'permission');
|
||||
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
\echo ''
|
||||
\echo 'Eve (has viewer role, which employee inherits from):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'eeeeeeee-0000-0000-0000-000000000005'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
\echo ''
|
||||
\echo 'Bob (employee) should inherit permissions from viewer role:'
|
||||
\echo 'Effective roles for Bob (should include employee and viewer via hierarchy):'
|
||||
SELECT r.name, er.depth
|
||||
FROM morbac.get_effective_roles(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
) er
|
||||
JOIN morbac.roles r ON er.role_id = r.id
|
||||
ORDER BY er.depth;
|
||||
|
||||
\echo ''
|
||||
\echo 'Alice (admin) should inherit from both employee and viewer:'
|
||||
SELECT r.name, er.depth
|
||||
FROM morbac.get_effective_roles(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
) er
|
||||
JOIN morbac.roles r ON er.role_id = r.id
|
||||
ORDER BY er.depth;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Organization Hierarchy ==='
|
||||
|
||||
\echo 'Ancestors of Acme Subsidiary:'
|
||||
SELECT o.name, oa.depth
|
||||
FROM morbac.get_org_ancestors('33333333-3333-3333-3333-333333333333'::uuid) oa
|
||||
JOIN morbac.orgs o ON oa.org_id = o.id
|
||||
ORDER BY oa.depth;
|
||||
|
||||
\echo ''
|
||||
\echo 'Descendants of Acme Corp (should include subsidiary):'
|
||||
SELECT o.name, od.depth
|
||||
FROM morbac.get_org_descendants('11111111-1111-1111-1111-111111111111'::uuid) od
|
||||
JOIN morbac.orgs o ON od.org_id = o.id
|
||||
ORDER BY od.depth;
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
) IS NOT DISTINCT FROM morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Authorization Decision Tests Completed ==='
|
||||
|
||||
+353
-29
@@ -1,41 +1,365 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Activity and View Hierarchies Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers activity and view hierarchy tests including:
|
||||
-- - Activity hierarchy (e.g., write implies read)
|
||||
-- - View hierarchy (e.g., sensitive_data is a type of documents)
|
||||
-- - Effective activities and views
|
||||
-- - Permission inheritance through hierarchies
|
||||
-- =============================================================================
|
||||
-- Hierarchy Tests
|
||||
-- =============================================================================
|
||||
-- Tests all four hierarchy types:
|
||||
-- 1. Role hierarchy — senior roles inherit permissions of junior roles (transitive)
|
||||
-- 2. Activity hierarchy — requesting a senior activity also matches junior-activity rules
|
||||
-- 3. View hierarchy — requesting a senior view also matches junior-view rules
|
||||
-- 4. Org hierarchy — get_org_ancestors / get_org_descendants traversal
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- Hierarchy semantics in this system:
|
||||
-- Activity: (senior='write', junior='read') means get_effective_activities('write')
|
||||
-- returns {write, read}, so a rule for 'read' ALSO covers 'write' requests.
|
||||
-- View: (senior='financial_data', junior='documents') means
|
||||
-- get_effective_views('financial_data') returns {financial_data, documents},
|
||||
-- so a rule for 'documents' ALSO covers 'financial_data' requests.
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql, 01_authorization.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Activity and View Hierarchies ==='
|
||||
\echo '================================================================'
|
||||
\echo '02 — HIERARCHIES'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Define activity hierarchy: write implies read
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Role hierarchy — get_effective_roles and get_inherited_roles
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Role hierarchy introspection ---'
|
||||
|
||||
-- Eve (intern) direct assignment only — 1 effective role
|
||||
SELECT morbac.t_eq('Eve has 1 effective role (intern only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 1);
|
||||
|
||||
-- Dave (employee) has employee + intern via hierarchy — 2 effective roles
|
||||
SELECT morbac.t_eq('Dave has 2 effective roles (employee, intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 2);
|
||||
|
||||
-- Carol (manager) has manager + employee + intern via hierarchy — 3 effective roles
|
||||
SELECT morbac.t_eq('Carol has 3 effective roles (manager, employee, intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 3);
|
||||
|
||||
-- Alice (CEO) has ceo, director, manager, employee, intern — 5 effective roles
|
||||
SELECT morbac.t_eq('Alice has 5 effective roles (ceo through intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_roles(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 5);
|
||||
|
||||
-- get_inherited_roles for manager — manager itself + employee + intern = 3
|
||||
SELECT morbac.t_eq('get_inherited_roles(manager) returns 3 roles (manager, employee, intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_inherited_roles(
|
||||
'20000000-0001-0000-0000-000000000003'::uuid
|
||||
))::bigint, 3);
|
||||
|
||||
-- get_inherited_roles for ceo — entire chain = 5
|
||||
SELECT morbac.t_eq('get_inherited_roles(ceo) returns 5 roles (ceo through intern)',
|
||||
(SELECT COUNT(*) FROM morbac.get_inherited_roles(
|
||||
'20000000-0001-0000-0000-000000000001'::uuid
|
||||
))::bigint, 5);
|
||||
|
||||
-- Manager is included in its own inherited roles (at depth 0)
|
||||
SELECT morbac.t('manager role is in its own inherited roles at depth 0',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_inherited_roles('20000000-0001-0000-0000-000000000003'::uuid)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000003' AND depth = 0
|
||||
), TRUE);
|
||||
|
||||
-- Intern is included in manager's inherited roles (at depth 2)
|
||||
SELECT morbac.t('intern role appears in manager inherited roles',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_inherited_roles('20000000-0001-0000-0000-000000000003'::uuid)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000005'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Role hierarchy — authorization via inheritance (transitive)
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Role hierarchy — authorization via inheritance ---'
|
||||
|
||||
-- Carol (manager) has employee permission (read documents) via 1-level inheritance
|
||||
SELECT morbac.t('Carol (manager, 1-level inherit) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Bob (director) has employee permission (read documents) via 2-level inheritance
|
||||
SELECT morbac.t('Bob (director, 2-level inherit) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000002'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Alice (CEO) has employee permission (read documents) via 3-level inheritance
|
||||
SELECT morbac.t('Alice (CEO, 3-level inherit) reads documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Alice (CEO) has intern permission (read public_data) via 4-level inheritance
|
||||
SELECT morbac.t('Alice (CEO, 4-level inherit) reads public_data',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'public_data'
|
||||
), TRUE);
|
||||
|
||||
-- Carol (manager) has manager's own perm (approve documents)
|
||||
SELECT morbac.t('Carol (manager) approves documents (own perm)',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) cannot approve — no role above intern has approve perm
|
||||
SELECT morbac.t('Eve (intern) approves documents [inheritance only goes up in seniority]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- tech_lead in Engineering inherits engineer permissions
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('30000000-0000-0000-0000-000000000013', '20000000-0002-0000-0000-000000000001', '10000000-0000-0000-0000-000000000002');
|
||||
|
||||
-- tech_lead inherits engineer's read permission
|
||||
SELECT morbac.t('Tech lead (inherits engineer) reads Engineering documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000013'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- tech_lead has own approve permission
|
||||
SELECT morbac.t('Tech lead approves Engineering documents (own perm)',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000013'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Activity hierarchy
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Activity hierarchy setup ---'
|
||||
|
||||
-- Build activity chain: manage -> delete -> write -> read
|
||||
-- (Senior activity requesting also covers rules for its junior activities)
|
||||
INSERT INTO morbac.activity_hierarchy (senior_activity, junior_activity) VALUES
|
||||
('write', 'read');
|
||||
('manage', 'delete'),
|
||||
('delete', 'write'),
|
||||
('write', 'read'),
|
||||
('export', 'read');
|
||||
|
||||
-- Define view hierarchy: sensitive_data is a type of documents
|
||||
-- get_effective_activities introspection
|
||||
SELECT morbac.t_eq('get_effective_activities(read) returns 1 (read only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_activities('read'))::bigint, 1);
|
||||
|
||||
SELECT morbac.t_eq('get_effective_activities(write) returns 2 (write, read)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_activities('write'))::bigint, 2);
|
||||
|
||||
SELECT morbac.t_eq('get_effective_activities(delete) returns 3 (delete, write, read)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_activities('delete'))::bigint, 3);
|
||||
|
||||
SELECT morbac.t_eq('get_effective_activities(manage) returns 4 (manage, delete, write, read)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_activities('manage'))::bigint, 4);
|
||||
|
||||
SELECT morbac.t_eq('get_effective_activities(export) returns 2 (export, read)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_activities('export'))::bigint, 2);
|
||||
|
||||
-- Activity hierarchy authorization tests
|
||||
-- Rule: employee has 'read documents' permission.
|
||||
-- With hierarchy (write->read), requesting 'write' also matches the 'read' rule.
|
||||
|
||||
-- Dave (employee) requests 'write' — matches 'read' rule via write->read hierarchy
|
||||
SELECT morbac.t('Dave (employee, has read perm) writes docs via activity hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'write', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) requests 'delete' — delete->write->read chain, 'read' rule matches
|
||||
SELECT morbac.t('Dave (employee, has read perm) deletes docs via delete->write->read hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'delete', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) requests 'export' — export->read, 'read' rule matches
|
||||
SELECT morbac.t('Dave (employee, has read perm) exports docs via export->read hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'export', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) has only 'read public_data' — requesting 'write public_data' also matches
|
||||
SELECT morbac.t('Eve (intern, has read perm) writes public_data via activity hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'write', 'public_data'
|
||||
), TRUE);
|
||||
|
||||
-- 'audit' is not in the hierarchy — no junior, no senior — no match for employee
|
||||
SELECT morbac.t('Dave (employee) audits documents [audit not in hierarchy, no permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'audit', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: View hierarchy
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. View hierarchy setup ---'
|
||||
|
||||
-- financial_data and hr_data are specific sub-types of documents
|
||||
INSERT INTO morbac.view_hierarchy (senior_view, junior_view) VALUES
|
||||
('sensitive_data', 'documents');
|
||||
('financial_data', 'documents'),
|
||||
('hr_data', 'documents');
|
||||
|
||||
\echo 'Activities implied by write:'
|
||||
SELECT * FROM morbac.get_effective_activities('write');
|
||||
-- get_effective_views introspection
|
||||
SELECT morbac.t_eq('get_effective_views(documents) returns 1 (documents only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_views('documents'))::bigint, 1);
|
||||
|
||||
\echo 'Views for sensitive_data (should include documents):'
|
||||
SELECT * FROM morbac.get_effective_views('sensitive_data');
|
||||
SELECT morbac.t_eq('get_effective_views(financial_data) returns 2 (financial_data, documents)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_views('financial_data'))::bigint, 2);
|
||||
|
||||
\echo 'Bob has permission for read+documents, so write+documents should work (activity hierarchy):'
|
||||
SELECT
|
||||
'write documents (via activity hierarchy)' as action,
|
||||
morbac.is_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'write',
|
||||
'documents'
|
||||
) as allowed;
|
||||
SELECT morbac.t_eq('get_effective_views(hr_data) returns 2 (hr_data, documents)',
|
||||
(SELECT COUNT(*) FROM morbac.get_effective_views('hr_data'))::bigint, 2);
|
||||
|
||||
-- financial_data is included in get_effective_views('financial_data')
|
||||
SELECT morbac.t('financial_data is in effective views of itself',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_effective_views('financial_data')
|
||||
WHERE view = 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- documents is also included (as junior)
|
||||
SELECT morbac.t('documents is in effective views of financial_data',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_effective_views('financial_data')
|
||||
WHERE view = 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- employee has 'read documents' permission
|
||||
-- requesting 'financial_data' -> get_effective_views('financial_data') = {financial_data, documents}
|
||||
-- the 'documents' rule matches -> access granted
|
||||
|
||||
-- Dave (employee) reads financial_data — matches employee's 'read documents' rule via view hierarchy
|
||||
SELECT morbac.t('Dave (employee, has read documents) reads financial_data via view hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) reads hr_data — matches 'read documents' via view hierarchy
|
||||
SELECT morbac.t('Dave (employee, has read documents) reads hr_data via view hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'hr_data'
|
||||
), TRUE);
|
||||
|
||||
-- Frank (contractor) reads documents — permitted
|
||||
SELECT morbac.t('Frank (contractor) reads documents [has permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Frank (contractor) reads financial_data — matches contractor's 'read documents' rule via view hierarchy
|
||||
-- BUT contractor has a PROHIBITION on financial_data — prohibition wins
|
||||
SELECT morbac.t('Frank (contractor) reads financial_data [prohibition overrides view-hierarchy match]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: Org hierarchy navigation
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Org hierarchy navigation ---'
|
||||
|
||||
-- Engineering Dept has 2 ancestors: itself (depth=0) and GlobalTech HQ (depth=1)
|
||||
SELECT morbac.t_eq('Engineering Dept has 2 ancestors (self + GlobalTech HQ)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_ancestors(
|
||||
'10000000-0000-0000-0000-000000000002'::uuid
|
||||
))::bigint, 2);
|
||||
|
||||
-- Sales Dept also has 2 ancestors
|
||||
SELECT morbac.t_eq('Sales Dept has 2 ancestors (self + GlobalTech HQ)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_ancestors(
|
||||
'10000000-0000-0000-0000-000000000003'::uuid
|
||||
))::bigint, 2);
|
||||
|
||||
-- GlobalTech HQ is the root — only 1 ancestor (itself)
|
||||
SELECT morbac.t_eq('GlobalTech HQ (root) has 1 ancestor (itself only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_ancestors(
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 1);
|
||||
|
||||
-- GlobalTech HQ has 3 descendants: itself + Engineering + Sales
|
||||
SELECT morbac.t_eq('GlobalTech HQ has 3 descendants (self + Engineering + Sales)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_descendants(
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint, 3);
|
||||
|
||||
-- Engineering (leaf node) has only 1 descendant: itself
|
||||
SELECT morbac.t_eq('Engineering (leaf) has 1 descendant (itself only)',
|
||||
(SELECT COUNT(*) FROM morbac.get_org_descendants(
|
||||
'10000000-0000-0000-0000-000000000002'::uuid
|
||||
))::bigint, 1);
|
||||
|
||||
-- GlobalTech HQ is its own ancestor at depth 0
|
||||
SELECT morbac.t('GlobalTech HQ is its own ancestor at depth 0',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_org_ancestors('10000000-0000-0000-0000-000000000001'::uuid)
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001' AND depth = 0
|
||||
), TRUE);
|
||||
|
||||
-- Engineering's parent (GlobalTech HQ) appears at depth 1
|
||||
SELECT morbac.t('GlobalTech HQ appears at depth 1 in Engineering ancestors',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_org_ancestors('10000000-0000-0000-0000-000000000002'::uuid)
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001' AND depth = 1
|
||||
), TRUE);
|
||||
|
||||
-- Verify org hierarchy does NOT automatically propagate permissions
|
||||
-- (permissions are org-scoped; cross-org access requires explicit cross_org_rules)
|
||||
SELECT morbac.t('Dave (GlobalTech employee) reads Engineering docs [no cross-org rule]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Activity and View Hierarchies Tests Completed ==='
|
||||
\echo '=== Hierarchy Tests Completed ==='
|
||||
|
||||
+346
-27
@@ -1,36 +1,355 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Delegation Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers delegation functionality including:
|
||||
-- - Temporary role delegation
|
||||
-- - Delegated permission inheritance
|
||||
-- - Comprehensive role reporting (direct + delegated)
|
||||
-- - Time-limited delegation (valid_from/valid_until)
|
||||
-- =============================================================================
|
||||
-- Delegation Tests
|
||||
-- =============================================================================
|
||||
-- Tests the role delegation feature:
|
||||
-- - Active delegation grants the delegate the delegator's role permissions
|
||||
-- - Expired delegations are inactive
|
||||
-- - Future delegations are not yet active
|
||||
-- - Revoked delegations are inactive
|
||||
-- - Delegator must hold the role (delegation from source is validated)
|
||||
-- - Negative role assignments override delegations
|
||||
-- - Delegated roles participate in role hierarchy
|
||||
-- - get_comprehensive_roles reports delegation source
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- Scenario:
|
||||
-- Carol (manager) delegates her manager role to Leo (employee) for 1 day
|
||||
-- -> Leo should temporarily have manager-level access
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 02_hierarchies.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Delegation ==='
|
||||
\echo '================================================================'
|
||||
\echo '03 — DELEGATION'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Alice delegates admin role to user Frank temporarily
|
||||
INSERT INTO morbac.delegations (delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until) VALUES
|
||||
('aaaaaaaa-0000-0000-0000-000000000001', 'ffffffff-0000-0000-0000-000000000006', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', now(), now() + interval '1 day');
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Baseline — Leo (employee) before any delegation
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Baseline: Leo before delegation ---'
|
||||
|
||||
\echo 'Frank (via delegation from Alice) should have admin permissions:'
|
||||
SELECT
|
||||
'delete documents (delegated)' as action,
|
||||
morbac.is_allowed(
|
||||
'ffffffff-0000-0000-0000-000000000006'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'delete',
|
||||
'documents'
|
||||
) as allowed;
|
||||
-- Leo has only employee permissions
|
||||
SELECT morbac.t('Leo (employee) reads documents before delegation',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
\echo 'Frank effective roles (should include delegated admin):'
|
||||
SELECT r.name, cr.source
|
||||
FROM morbac.get_comprehensive_roles('ffffffff-0000-0000-0000-000000000006'::uuid, '11111111-1111-1111-1111-111111111111'::uuid) cr
|
||||
JOIN morbac.roles r ON cr.role_id = r.id;
|
||||
-- Leo cannot approve documents — that requires manager role
|
||||
SELECT morbac.t('Leo (employee) approves documents before delegation [no manager perm]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Leo cannot delete documents (no employee delete perm, activity hierarchy grants delete via read rule,
|
||||
-- but 'delete' implies only that if there's a delete->write->read chain; in 02 we set write->read but
|
||||
-- the chain delete->write was also set. So Leo gets delete via documents perm. Let's check:
|
||||
-- Actually with activity hierarchy (delete->write->read), the 'read documents' rule covers delete.
|
||||
SELECT morbac.t('Leo (employee) deletes documents before delegation [via activity hierarchy]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'delete', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Leo cannot approve documents (approve is not in activity hierarchy for documents)
|
||||
-- This is the key baseline check: approve requires manager-level permission
|
||||
SELECT morbac.t('Leo (employee) does not have approve permission before delegation',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Active delegation — Carol delegates manager role to Leo
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Active delegation: Carol -> Leo (manager role, 1 day) ---'
|
||||
|
||||
INSERT INTO morbac.delegations
|
||||
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
|
||||
VALUES (
|
||||
'de000001-0000-0000-0000-000000000001',
|
||||
'30000000-0000-0000-0000-000000000003', -- Carol (delegator, holds manager)
|
||||
'30000000-0000-0000-0000-000000000012', -- Leo (delegatee)
|
||||
'20000000-0001-0000-0000-000000000003', -- manager role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
now() - interval '1 hour',
|
||||
now() + interval '1 day'
|
||||
);
|
||||
|
||||
-- Leo now has delegated manager role — can approve documents
|
||||
SELECT morbac.t('Leo (delegated manager) approves documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Leo can delete documents (manager permission)
|
||||
SELECT morbac.t('Leo (delegated manager) deletes documents',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'delete', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Leo (delegated manager) inherits manager's inherited permissions:
|
||||
-- manager->employee->intern chain; employee has read documents -> view hierarchy covers financial_data
|
||||
SELECT morbac.t('Leo (delegated manager) reads financial_data via manager+view hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- get_comprehensive_roles should show manager role with delegation source
|
||||
SELECT morbac.t('Leo has delegated manager role in comprehensive roles',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000003'
|
||||
AND source LIKE 'delegation%'
|
||||
), TRUE);
|
||||
|
||||
-- Employee role appears as direct in comprehensive roles
|
||||
SELECT morbac.t('Leo has employee role as direct in comprehensive roles',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000004'
|
||||
AND source = 'direct'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Expired delegation
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Expired delegation ---'
|
||||
|
||||
INSERT INTO morbac.delegations
|
||||
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
|
||||
VALUES (
|
||||
'de000001-0000-0000-0000-000000000002',
|
||||
'30000000-0000-0000-0000-000000000008', -- Heidi (auditor)
|
||||
'30000000-0000-0000-0000-000000000004', -- Dave
|
||||
'20000000-0001-0000-0000-000000000006', -- auditor role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
now() - interval '7 days',
|
||||
now() - interval '1 second' -- expired 1 second ago
|
||||
);
|
||||
|
||||
-- Dave should NOT get auditor permissions from expired delegation
|
||||
SELECT morbac.t('Dave via expired auditor delegation reads audit_logs [delegation expired]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'audit_logs'
|
||||
), FALSE);
|
||||
|
||||
-- The expired delegation should NOT appear in comprehensive roles
|
||||
SELECT morbac.t('Expired auditor delegation not in Dave comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000006'
|
||||
AND source LIKE 'delegation%'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Future delegation (not yet active)
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. Future delegation (not yet active) ---'
|
||||
|
||||
INSERT INTO morbac.delegations
|
||||
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
|
||||
VALUES (
|
||||
'de000001-0000-0000-0000-000000000003',
|
||||
'30000000-0000-0000-0000-000000000007', -- Grace (hr_manager)
|
||||
'30000000-0000-0000-0000-000000000004', -- Dave
|
||||
'20000000-0001-0000-0000-000000000008', -- hr_manager role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
now() + interval '1 day', -- starts tomorrow
|
||||
now() + interval '8 days'
|
||||
);
|
||||
|
||||
-- Dave should NOT have hr_manager in his comprehensive roles (delegation not yet active)
|
||||
-- Note: employee already has write hr_data via view hierarchy (hr_data->documents);
|
||||
-- so we test comprehensive roles rather than is_allowed to verify the delegation is inactive.
|
||||
SELECT morbac.t('Future hr_manager delegation not yet in Dave comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000008' -- hr_manager
|
||||
AND source LIKE 'delegation%'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: Revoked delegation
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Revoked delegation ---'
|
||||
|
||||
INSERT INTO morbac.delegations
|
||||
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until, revoked)
|
||||
VALUES (
|
||||
'de000001-0000-0000-0000-000000000004',
|
||||
'30000000-0000-0000-0000-000000000009', -- Ivan (accountant)
|
||||
'30000000-0000-0000-0000-000000000004', -- Dave
|
||||
'20000000-0001-0000-0000-000000000007', -- accountant role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
now() - interval '1 hour',
|
||||
now() + interval '1 day',
|
||||
TRUE -- revoked
|
||||
);
|
||||
|
||||
-- Dave should NOT get accountant from revoked delegation — verify revoked flag is set
|
||||
-- Note: employee already has write financial_data via view hierarchy;
|
||||
-- so we verify the delegation is actually revoked in the DB.
|
||||
SELECT morbac.t('Revoked accountant delegation has revoked=TRUE in DB',
|
||||
(SELECT revoked FROM morbac.delegations
|
||||
WHERE id = 'de000001-0000-0000-0000-000000000004'),
|
||||
TRUE);
|
||||
|
||||
-- The revoked delegation should NOT appear in comprehensive roles
|
||||
SELECT morbac.t('Revoked accountant delegation not in Dave comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000007'
|
||||
AND source LIKE 'delegation%'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: Delegation blocked by negative role assignment
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. Negative role assignment overrides delegation ---'
|
||||
|
||||
-- Leo has an active delegation for manager role (from section 2)
|
||||
-- Now add a negative assignment to block that same role
|
||||
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000012', -- Leo
|
||||
'20000000-0001-0000-0000-000000000003', -- manager role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'Temporarily suspended from manager responsibilities'
|
||||
);
|
||||
|
||||
-- negative assignment blocks the delegated manager role
|
||||
SELECT morbac.t('Leo (delegated manager, but negatively assigned) approves documents [blocked]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Leo's own employee permissions still work (negative is only for manager)
|
||||
SELECT morbac.t('Leo (employee, manager negated) reads documents [employee perm unaffected]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Manager delegation no longer appears in comprehensive roles (blocked by negative)
|
||||
SELECT morbac.t('Negated manager delegation not in Leo comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000012'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000003'
|
||||
), TRUE);
|
||||
|
||||
-- Clean up negative assignment so subsequent tests aren't affected
|
||||
DELETE FROM morbac.negative_role_assignments
|
||||
WHERE user_id = '30000000-0000-0000-0000-000000000012'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000003'
|
||||
AND org_id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Delegator must hold the role (validation check)
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 7. Delegator-role validation ---'
|
||||
|
||||
-- Create a delegation where the delegator does NOT hold the role
|
||||
-- Dave (employee) tries to delegate the manager role (which he does not have)
|
||||
INSERT INTO morbac.delegations
|
||||
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
|
||||
VALUES (
|
||||
'de000001-0000-0000-0000-000000000005',
|
||||
'30000000-0000-0000-0000-000000000004', -- Dave (employee — does NOT hold manager)
|
||||
'30000000-0000-0000-0000-000000000005', -- Eve
|
||||
'20000000-0001-0000-0000-000000000003', -- manager role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
now() - interval '1 hour',
|
||||
now() + interval '1 day'
|
||||
);
|
||||
|
||||
-- get_comprehensive_roles requires delegator to hold the role
|
||||
-- Eve does NOT get manager access from invalid delegation
|
||||
SELECT morbac.t('Eve via invalid delegation (Dave does not hold manager) denied approve',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- The invalid delegation should NOT appear in Eve's comprehensive roles
|
||||
SELECT morbac.t('Invalid delegation not in Eve comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000003'
|
||||
AND source LIKE 'delegation%'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 8: Comprehensive roles source reporting
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 8. get_comprehensive_roles source reporting ---'
|
||||
|
||||
-- Alice (CEO, direct) — source should be 'direct'
|
||||
SELECT morbac.t('Alice CEO role has source=direct in comprehensive roles',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000001'
|
||||
AND source = 'direct'
|
||||
), TRUE);
|
||||
|
||||
-- Alice has no delegation-sourced roles
|
||||
SELECT morbac.t('Alice has no delegation-sourced roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE source LIKE 'delegation%'
|
||||
), TRUE);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Delegation Tests Completed ==='
|
||||
|
||||
+231
-36
@@ -1,54 +1,249 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Constraint Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers various constraints including:
|
||||
-- - Separation of Duty (SoD) conflicts
|
||||
-- - Negative role assignments
|
||||
-- - Role cardinality constraints (min/max users per role)
|
||||
-- =============================================================================
|
||||
-- Constraint Tests
|
||||
-- =============================================================================
|
||||
-- Tests business constraints:
|
||||
-- 1. Separation of Duty (SoD) — mutually exclusive roles
|
||||
-- 2. Negative role assignments — explicit blocking of a role
|
||||
-- 3. Role cardinality constraints — min/max users per role
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- Scenario:
|
||||
-- - auditor and accountant are mutually exclusive (no one can hold both)
|
||||
-- - Heidi (auditor) cannot be assigned accountant role
|
||||
-- - Ivan (accountant) cannot be assigned auditor role
|
||||
-- - Frank (contractor) is explicitly blocked from the employee role
|
||||
-- - compliance_officer role has a min 1, max 2 cardinality constraint
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 03_delegation.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Separation of Duty ==='
|
||||
\echo '================================================================'
|
||||
\echo '04 — CONSTRAINTS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Define SoD conflict: auditor and accountant roles are mutually exclusive
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'auditor', 'Auditor role'),
|
||||
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'accountant', 'Accountant role');
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Separation of Duty — define conflict
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Separation of Duty setup ---'
|
||||
|
||||
INSERT INTO morbac.sod_conflicts (role_a_id, role_b_id, org_id, description) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'Auditors and accountants are mutually exclusive');
|
||||
INSERT INTO morbac.sod_conflicts (role_a_id, role_b_id, org_id, description)
|
||||
VALUES (
|
||||
'20000000-0001-0000-0000-000000000006', -- auditor
|
||||
'20000000-0001-0000-0000-000000000007', -- accountant
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'Auditors and accountants are mutually exclusive to ensure financial independence'
|
||||
);
|
||||
|
||||
-- Assign user to auditor role
|
||||
-- Verify the conflict was created
|
||||
SELECT morbac.t_eq('SoD conflict between auditor and accountant created',
|
||||
(SELECT COUNT(*) FROM morbac.sod_conflicts
|
||||
WHERE role_a_id = '20000000-0001-0000-0000-000000000006'
|
||||
AND role_b_id = '20000000-0001-0000-0000-000000000007')::bigint,
|
||||
1);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: SoD violation detection
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. SoD violation detection ---'
|
||||
|
||||
-- Heidi (auditor) — check if assigning accountant role would violate SoD
|
||||
SELECT morbac.t('Assigning accountant to Heidi (auditor) violates SoD',
|
||||
morbac.check_sod_violation(
|
||||
'30000000-0000-0000-0000-000000000008'::uuid, -- Heidi
|
||||
'20000000-0001-0000-0000-000000000007'::uuid, -- accountant role
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), TRUE);
|
||||
|
||||
-- Ivan (accountant) — check if assigning auditor role would violate SoD
|
||||
SELECT morbac.t('Assigning auditor to Ivan (accountant) violates SoD [symmetric]',
|
||||
morbac.check_sod_violation(
|
||||
'30000000-0000-0000-0000-000000000009'::uuid, -- Ivan
|
||||
'20000000-0001-0000-0000-000000000006'::uuid, -- auditor role
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), TRUE);
|
||||
|
||||
-- Dave (employee) — check if assigning accountant would violate SoD
|
||||
-- Dave is not an auditor — no conflict
|
||||
SELECT morbac.t('Assigning accountant to Dave (not an auditor) does not violate SoD',
|
||||
morbac.check_sod_violation(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid, -- Dave
|
||||
'20000000-0001-0000-0000-000000000007'::uuid, -- accountant role
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), FALSE);
|
||||
|
||||
-- Heidi (auditor) — assigning a non-conflicting role (manager) is fine
|
||||
SELECT morbac.t('Assigning manager to Heidi (auditor) does not violate SoD',
|
||||
morbac.check_sod_violation(
|
||||
'30000000-0000-0000-0000-000000000008'::uuid, -- Heidi
|
||||
'20000000-0001-0000-0000-000000000003'::uuid, -- manager role
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), FALSE);
|
||||
|
||||
-- User in a different org has no SoD conflict with GlobalTech roles
|
||||
-- Judy is in Engineering/Sales, not GlobalTech
|
||||
SELECT morbac.t('Assigning accountant to Judy (no GlobalTech roles) does not violate SoD',
|
||||
morbac.check_sod_violation(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid, -- Judy
|
||||
'20000000-0001-0000-0000-000000000007'::uuid, -- accountant role
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Negative role assignments
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Negative role assignments ---'
|
||||
|
||||
-- Baseline: Frank (contractor) has read documents permission
|
||||
SELECT morbac.t('Frank (contractor) reads documents [baseline before negative assignment]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Add employee role directly to Frank (unusual but valid)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('99999999-0000-0000-0000-000000000009', '11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111');
|
||||
('30000000-0000-0000-0000-000000000006', '20000000-0001-0000-0000-000000000004', '10000000-0000-0000-0000-000000000001');
|
||||
|
||||
\echo 'Check SoD violation if we try to assign accountant role to auditor:'
|
||||
SELECT morbac.check_sod_violation(
|
||||
'99999999-0000-0000-0000-000000000009'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
) as would_violate_sod;
|
||||
-- Frank now has both contractor and employee roles; employee grants write documents
|
||||
SELECT morbac.t('Frank (now also employee) writes documents [employee permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'write', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Add negative assignment blocking Frank from the employee role
|
||||
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000006', -- Frank
|
||||
'20000000-0001-0000-0000-000000000004', -- employee role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'Frank is a contractor and must not gain employee-level access'
|
||||
);
|
||||
|
||||
-- Frank's employee role is negated — verify via get_comprehensive_roles
|
||||
-- employee role must not appear (negated by negative assignment)
|
||||
SELECT morbac.t('Frank: employee role excluded by negative assignment',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000004'
|
||||
), TRUE);
|
||||
|
||||
-- Frank's contractor role still appears (only employee is negated)
|
||||
SELECT morbac.t('Frank: contractor role still present after employee negated',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000006'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000009'
|
||||
), TRUE);
|
||||
|
||||
-- Negative assignment on a role the user never had is harmless
|
||||
-- Karl has no role — adding negative assignment for manager is a no-op
|
||||
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||
'20000000-0001-0000-0000-000000000003', -- manager role (Karl never had this)
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'Preemptive block'
|
||||
);
|
||||
|
||||
-- Karl still has no access (negative assignment on non-held role is harmless)
|
||||
SELECT morbac.t('Karl (no role, preemptive negative assignment) reads documents [still no access]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Clean up Frank's extra employee assignment and negative assignment for later tests
|
||||
DELETE FROM morbac.user_roles
|
||||
WHERE user_id = '30000000-0000-0000-0000-000000000006'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000004'
|
||||
AND org_id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
DELETE FROM morbac.negative_role_assignments
|
||||
WHERE user_id = '30000000-0000-0000-0000-000000000006'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000004';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Role cardinality constraints
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '=== Negative Role Assignment ==='
|
||||
\echo '--- 4. Role cardinality constraints ---'
|
||||
|
||||
-- Explicitly prohibit contractor from ever being admin
|
||||
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason) VALUES
|
||||
('cccccccc-0000-0000-0000-000000000003', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', 'Contractors cannot be administrators');
|
||||
-- Set a cardinality constraint: compliance_officer role — min 1, max 2
|
||||
INSERT INTO morbac.role_cardinality (role_id, min_users, max_users, description)
|
||||
VALUES (
|
||||
'20000000-0001-0000-0000-000000000010', -- compliance_officer
|
||||
1, 2,
|
||||
'Compliance officer role: at least 1, at most 2'
|
||||
);
|
||||
|
||||
\echo 'Charlie cannot have admin role even if explicitly assigned (negative assignment takes precedence).'
|
||||
-- Currently 0 users have compliance_officer — adding one should be fine (0 < max=2)
|
||||
SELECT morbac.t_null('Adding first compliance_officer (0 users, max=2) — no violation',
|
||||
morbac.check_cardinality_violation(
|
||||
'20000000-0001-0000-0000-000000000010'::uuid,
|
||||
TRUE -- adding
|
||||
));
|
||||
|
||||
\echo ''
|
||||
\echo '=== Cardinality Constraints ==='
|
||||
-- Assign two users as compliance officer
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('30000000-0000-0000-0000-000000000004', '20000000-0001-0000-0000-000000000010', '10000000-0000-0000-0000-000000000001'),
|
||||
('30000000-0000-0000-0000-000000000005', '20000000-0001-0000-0000-000000000010', '10000000-0000-0000-0000-000000000001');
|
||||
|
||||
-- Set max 2 users for admin role
|
||||
INSERT INTO morbac.role_cardinality (role_id, min_users, max_users, description) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, 2, 'Admin role limited to 2 users minimum 1');
|
||||
-- Now 2 users — at max. Trying to add a 3rd should violate
|
||||
SELECT morbac.t_not_null('Adding 3rd compliance_officer (2 users, max=2) — violation returned',
|
||||
morbac.check_cardinality_violation(
|
||||
'20000000-0001-0000-0000-000000000010'::uuid,
|
||||
TRUE -- adding
|
||||
));
|
||||
|
||||
\echo 'Check cardinality when adding user (1 admin exists, max is 2, should be OK):'
|
||||
SELECT morbac.check_cardinality_violation('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, TRUE) as violation;
|
||||
-- Removing one — 2 users, min=1 — removing leaves 1 which is ≥ min=1, should be fine
|
||||
SELECT morbac.t_null('Removing from 2 compliance_officers (min=1) — no violation (still above min)',
|
||||
morbac.check_cardinality_violation(
|
||||
'20000000-0001-0000-0000-000000000010'::uuid,
|
||||
FALSE -- removing
|
||||
));
|
||||
|
||||
-- Remove one user
|
||||
DELETE FROM morbac.user_roles
|
||||
WHERE user_id = '30000000-0000-0000-0000-000000000005'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000010';
|
||||
|
||||
-- 1 user remaining = min. Removing the last one would violate min=1
|
||||
SELECT morbac.t_not_null('Removing last compliance_officer (1 user, min=1) — violation returned',
|
||||
morbac.check_cardinality_violation(
|
||||
'20000000-0001-0000-0000-000000000010'::uuid,
|
||||
FALSE -- removing
|
||||
));
|
||||
|
||||
-- Adding again after being at 1 — 1 user, max=2 — ok
|
||||
SELECT morbac.t_null('Adding when at 1 compliance_officer (max=2) — no violation',
|
||||
morbac.check_cardinality_violation(
|
||||
'20000000-0001-0000-0000-000000000010'::uuid,
|
||||
TRUE -- adding
|
||||
));
|
||||
|
||||
-- Role with no cardinality constraint — no violation for any operation
|
||||
SELECT morbac.t_null('Checking cardinality for employee role (no constraint) — no violation',
|
||||
morbac.check_cardinality_violation(
|
||||
'20000000-0001-0000-0000-000000000004'::uuid, -- employee
|
||||
TRUE
|
||||
));
|
||||
|
||||
-- Clean up extra compliance_officer assignments
|
||||
DELETE FROM morbac.user_roles
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000010'
|
||||
AND user_id = '30000000-0000-0000-0000-000000000004';
|
||||
|
||||
\echo ''
|
||||
\echo '=== Constraint Tests Completed ==='
|
||||
|
||||
+314
-67
@@ -1,82 +1,329 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Temporal Constraint Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers temporal constraints including:
|
||||
-- - Rules with valid_from timestamps (future activation)
|
||||
-- - Rules with valid_until timestamps (expiration)
|
||||
-- - Rules with both valid_from and valid_until (time windows)
|
||||
-- - Authorization decisions respecting temporal bounds
|
||||
-- =============================================================================
|
||||
-- Temporal Constraint Tests
|
||||
-- =============================================================================
|
||||
-- Tests time-bounded rules using valid_from and valid_until on the rules table.
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- Cases:
|
||||
-- 1. is_rule_valid() helper function tested directly
|
||||
-- 2. is_active flag set by trigger on insert/update
|
||||
-- 3. Expired rule (valid_until in the past) -> denied
|
||||
-- 4. Future rule (valid_from in the future) -> denied
|
||||
-- 5. Active time window (valid_from past, valid_until future) -> allowed
|
||||
-- 6. Multiple rules for same combination — only active ones count
|
||||
-- 7. Expired prohibition: no longer blocks access after it expires
|
||||
-- 8. Temporal rules interact correctly with role hierarchy
|
||||
--
|
||||
-- Note: Direct rule inserts are used (bypassing Policy DSL) to control timestamps.
|
||||
-- Uses a dedicated view 'temp_view' for isolation so existing rules aren't disturbed.
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 04_constraints.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Temporal Constraints on Rules ==='
|
||||
\echo '================================================================'
|
||||
\echo '05 — TEMPORAL CONSTRAINTS'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Create a time-limited rule (expires in the past)
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until)
|
||||
VALUES (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', -- employee role
|
||||
'write',
|
||||
'temp_documents',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
'2020-01-01 00:00:00+00' -- Expired rule
|
||||
);
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Setup: dedicated view for temporal tests
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO morbac.views (name, description) VALUES
|
||||
('temp_view', 'Scratch view for temporal tests');
|
||||
|
||||
-- Create a future rule (not yet active)
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from)
|
||||
VALUES (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'write',
|
||||
'future_documents',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
'2030-01-01 00:00:00+00' -- Future rule
|
||||
);
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: is_rule_valid() helper function
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. is_rule_valid() helper ---'
|
||||
|
||||
-- Create an active time-bounded rule
|
||||
-- No bounds: always valid
|
||||
SELECT morbac.t('is_rule_valid(NULL, NULL) — always valid',
|
||||
morbac.is_rule_valid(NULL::timestamptz, NULL::timestamptz), TRUE);
|
||||
|
||||
-- Past valid_from, no valid_until: currently active
|
||||
SELECT morbac.t('is_rule_valid(past, NULL) — started in past, no end',
|
||||
morbac.is_rule_valid('2000-01-01'::timestamptz, NULL), TRUE);
|
||||
|
||||
-- Future valid_from: not yet active
|
||||
SELECT morbac.t('is_rule_valid(future, NULL) — not yet started',
|
||||
morbac.is_rule_valid('2099-01-01'::timestamptz, NULL), FALSE);
|
||||
|
||||
-- Past valid_until: expired
|
||||
SELECT morbac.t('is_rule_valid(NULL, past) — already expired',
|
||||
morbac.is_rule_valid(NULL, '2000-01-01'::timestamptz), FALSE);
|
||||
|
||||
-- Future valid_until, no valid_from: currently active
|
||||
SELECT morbac.t('is_rule_valid(NULL, future) — no start, future end',
|
||||
morbac.is_rule_valid(NULL, '2099-01-01'::timestamptz), TRUE);
|
||||
|
||||
-- Active window: past start, future end
|
||||
SELECT morbac.t('is_rule_valid(past, future) — within active window',
|
||||
morbac.is_rule_valid('2000-01-01'::timestamptz, '2099-01-01'::timestamptz), TRUE);
|
||||
|
||||
-- Fully past window (both start and end in the past)
|
||||
SELECT morbac.t('is_rule_valid(past_start, past_end) — entirely expired',
|
||||
morbac.is_rule_valid('2000-01-01'::timestamptz, '2001-01-01'::timestamptz), FALSE);
|
||||
|
||||
-- Fully future window (both start and end in the future)
|
||||
SELECT morbac.t('is_rule_valid(future_start, future_end) — entirely in the future',
|
||||
morbac.is_rule_valid('2090-01-01'::timestamptz, '2099-01-01'::timestamptz), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: is_active flag set by trigger on insert
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. is_active trigger on insert ---'
|
||||
|
||||
-- Insert a currently active rule
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
|
||||
VALUES (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'write',
|
||||
'active_documents',
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004', -- employee
|
||||
'audit', 'temp_view',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
'2020-01-01 00:00:00+00', -- Started in the past
|
||||
'2030-12-31 23:59:59+00' -- Ends in the future
|
||||
now() - interval '1 day',
|
||||
now() + interval '1 day'
|
||||
);
|
||||
|
||||
\echo 'Alice (employee) access to temporal-constrained documents:'
|
||||
SELECT
|
||||
'write to temp_documents (expired rule)' as action,
|
||||
morbac.is_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'write',
|
||||
'temp_documents'
|
||||
) as allowed
|
||||
UNION ALL
|
||||
SELECT
|
||||
'write to future_documents (not yet active)' as action,
|
||||
morbac.is_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'write',
|
||||
'future_documents'
|
||||
) as allowed
|
||||
UNION ALL
|
||||
SELECT
|
||||
'write to active_documents (currently valid)' as action,
|
||||
morbac.is_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'write',
|
||||
'active_documents'
|
||||
) as allowed;
|
||||
-- is_active should be TRUE for the active rule
|
||||
SELECT morbac.t('is_active = TRUE for rule with active time window',
|
||||
(SELECT is_active FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view'),
|
||||
TRUE);
|
||||
|
||||
-- Update to expired window, trigger should recalculate is_active
|
||||
UPDATE morbac.rules
|
||||
SET valid_until = now() - interval '1 second'
|
||||
WHERE activity = 'audit' AND view = 'temp_view';
|
||||
|
||||
-- is_active should be FALSE after expiry update
|
||||
SELECT morbac.t('is_active = FALSE after valid_until set to past',
|
||||
(SELECT is_active FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view'),
|
||||
FALSE);
|
||||
|
||||
-- Clean up
|
||||
DELETE FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Expired rule — valid_until in the past
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Expired rule (valid_until in the past) ---'
|
||||
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004', -- employee
|
||||
'export', 'temp_view',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
'2020-01-01 00:00:00+00' -- expired years ago
|
||||
);
|
||||
|
||||
-- Rule expired, no valid permission
|
||||
SELECT morbac.t('Dave (employee) exports temp_view via expired rule [denied]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'export', 'temp_view'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Future rule — valid_from in the future
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. Future rule (valid_from in the future) ---'
|
||||
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004', -- employee
|
||||
'manage', 'temp_view',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
'2099-01-01 00:00:00+00' -- won't start for decades
|
||||
);
|
||||
|
||||
-- Rule not yet active
|
||||
SELECT morbac.t('Dave (employee) manages temp_view via future rule [not yet active]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'manage', 'temp_view'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: Active time window (valid_from past, valid_until future)
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Active time window ---'
|
||||
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004', -- employee
|
||||
'read', 'temp_view',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
'2020-01-01 00:00:00+00',
|
||||
'2099-12-31 23:59:59+00'
|
||||
);
|
||||
|
||||
-- Rule is within its validity window
|
||||
SELECT morbac.t('Dave (employee) reads temp_view via active window rule',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'temp_view'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: Multiple rules for same combination, only active ones count
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. Multiple rules for same combination, only active ones count ---'
|
||||
|
||||
-- At this point, employee has expired export rule (section 3) and active read rule (section 5).
|
||||
-- Since export->read in activity hierarchy, the active read rule grants export access.
|
||||
SELECT morbac.t('Dave exports temp_view (expired export + active read via export->read hierarchy)',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'export', 'temp_view'
|
||||
), TRUE);
|
||||
|
||||
-- Activate the expired export rule (unique constraint prevents a second rule with same modality)
|
||||
UPDATE morbac.rules
|
||||
SET valid_from = now() - interval '1 hour',
|
||||
valid_until = now() + interval '1 hour'
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000004'
|
||||
AND activity = 'export' AND view = 'temp_view' AND modality = 'permission';
|
||||
|
||||
-- Now export rule is active directly (not just via read hierarchy)
|
||||
SELECT morbac.t('Dave exports temp_view (export rule now active)',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'export', 'temp_view'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Temporal prohibition — expired prohibition no longer blocks
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 7. Expired prohibition no longer blocks ---'
|
||||
|
||||
-- Add an active permission for intern -> read temp_view
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000005', -- intern
|
||||
'read', 'temp_view',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- Add an EXPIRED prohibition for intern -> read temp_view
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000005', -- intern
|
||||
'read', 'temp_view',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'prohibition',
|
||||
'2020-01-01 00:00:00+00' -- prohibition expired years ago
|
||||
);
|
||||
|
||||
-- Prohibition expired, permission stands
|
||||
SELECT morbac.t('Eve (intern) reads temp_view [active permission, expired prohibition]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'temp_view'
|
||||
), TRUE);
|
||||
|
||||
-- Now replace the prohibition with an active one
|
||||
UPDATE morbac.rules
|
||||
SET valid_until = now() + interval '1 day'
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000005'
|
||||
AND activity = 'read' AND view = 'temp_view' AND modality = 'prohibition';
|
||||
|
||||
-- Active prohibition now blocks
|
||||
SELECT morbac.t('Eve (intern) reads temp_view [active prohibition blocks]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'temp_view'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 8: Temporal rule with role hierarchy
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 8. Temporal rule with role hierarchy ---'
|
||||
|
||||
-- Expire the intern read temp_view prohibition (activated in section 7) so it does not
|
||||
-- block Carol (manager) and Alice (CEO) who inherit from intern via role hierarchy.
|
||||
UPDATE morbac.rules
|
||||
SET valid_until = now() - interval '1 second'
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000005'
|
||||
AND activity = 'read' AND view = 'temp_view' AND modality = 'prohibition';
|
||||
|
||||
-- Add an active 'approve temp_view' permission for manager.
|
||||
-- 'approve' is NOT in the activity hierarchy (manage->delete->write->read, export->read),
|
||||
-- so employee's 'read temp_view' rule does NOT cover 'approve temp_view'.
|
||||
-- This ensures the temporal test cleanly verifies expiry without hierarchy interference.
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000003', -- manager
|
||||
'approve', 'temp_view',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
now() - interval '1 hour',
|
||||
now() + interval '1 hour'
|
||||
);
|
||||
|
||||
-- Carol (manager) can approve temp_view directly
|
||||
SELECT morbac.t('Carol (manager) approves temp_view [temporal rule, active]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'temp_view'
|
||||
), TRUE);
|
||||
|
||||
-- Alice (CEO) inherits from manager — should also get the temporal permission
|
||||
SELECT morbac.t('Alice (CEO, inherits manager) approves temp_view [temporal rule, active]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'temp_view'
|
||||
), TRUE);
|
||||
|
||||
-- Expire the manager approve rule
|
||||
UPDATE morbac.rules
|
||||
SET valid_until = now() - interval '1 second'
|
||||
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000003'
|
||||
AND activity = 'approve' AND view = 'temp_view';
|
||||
|
||||
-- Rule expired for both manager and CEO
|
||||
SELECT morbac.t('Carol (manager) approves temp_view after rule expiry [denied]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'temp_view'
|
||||
), FALSE);
|
||||
|
||||
SELECT morbac.t('Alice (CEO, inherits manager) approves temp_view after rule expiry [denied]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'approve', 'temp_view'
|
||||
), FALSE);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Temporal Constraint Tests Completed ==='
|
||||
|
||||
+291
-22
@@ -1,30 +1,299 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Cross-Organizational Rules Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers cross-organizational authorization including:
|
||||
-- - Cross-organization rules (access between different orgs)
|
||||
-- - Inter-organizational collaboration scenarios
|
||||
-- - Cross-org permission validation
|
||||
-- =============================================================================
|
||||
-- Cross-Organization Rules Tests
|
||||
-- =============================================================================
|
||||
-- Tests inter-organizational access via the cross_org_rules table.
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- Scenarios:
|
||||
-- 1. No cross-org rule: users in org A cannot access org B resources
|
||||
-- 2. Cross-org permission: specific role in source org can access target org
|
||||
-- 3. User must hold role in source org (not just any role)
|
||||
-- 4. Cross-org prohibition: blocks access even if there's a regular permission
|
||||
-- 5. Cross-org rules combine with activity hierarchy
|
||||
-- 6. Cross-org rules combine with view hierarchy
|
||||
-- 7. Temporal cross-org rules (valid_from / valid_until)
|
||||
--
|
||||
-- Company context:
|
||||
-- Judy (sales_rep at Sales) is allowed to read GlobalTech reports via cross-org rule
|
||||
-- Nina (eng_auditor at Engineering) has a temporal cross-org rule to read GlobalTech audit_logs
|
||||
-- Diana (sales_manager at Sales) has a cross-org prohibition on writing Engineering docs
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 05_temporal.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Cross-Organization Rules ==='
|
||||
\echo '================================================================'
|
||||
\echo '06 — CROSS-ORGANIZATIONAL RULES'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Allow Beta managers to read Acme documents (inter-org collaboration)
|
||||
INSERT INTO morbac.cross_org_rules (source_org_id, target_org_id, role_id, activity, view, context_id, modality) VALUES
|
||||
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'dddddddd-dddd-dddd-dddd-dddddddddddd', 'read', 'documents', (SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Setup: additional roles and users for cross-org scenarios
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
\echo 'Diana (manager at Beta) can read Acme documents via cross-org rule:'
|
||||
SELECT
|
||||
'read Acme documents from Beta' as action,
|
||||
morbac.is_allowed(
|
||||
'dddddddd-0000-0000-0000-000000000004'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
-- Nina: eng_auditor role in Engineering Dept
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('20000000-0002-0000-0000-000000000003', '10000000-0000-0000-0000-000000000002', 'eng_auditor', 'Engineering Auditor');
|
||||
|
||||
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();
|
||||
|
||||
-- Verify nina's role was set up correctly
|
||||
SELECT morbac.t('Nina has eng_auditor role at Engineering',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000014'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'eng_auditor'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: No cross-org rule — access between orgs is denied by default
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. No cross-org rule: access denied by default ---'
|
||||
|
||||
-- Dave (employee at GlobalTech) has no role in Engineering and no cross-org rule
|
||||
SELECT morbac.t('Dave (GlobalTech employee) reads Engineering documents [no cross-org rule]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Judy (Engineering engineer) accesses GlobalTech financial_data (no cross-org rule yet)
|
||||
SELECT morbac.t('Judy (Engineering engineer) reads GlobalTech financial_data [no cross-org rule]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: Cross-org permission — Sales sales_rep reads GlobalTech reports
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. Cross-org permission ---'
|
||||
|
||||
-- Judy is sales_rep at Sales. Add a cross-org rule:
|
||||
-- Sales sales_rep can read GlobalTech reports
|
||||
INSERT INTO morbac.cross_org_rules
|
||||
(source_org_id, target_org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000003', -- source: Sales Dept
|
||||
'10000000-0000-0000-0000-000000000001', -- target: GlobalTech HQ
|
||||
'20000000-0003-0000-0000-000000000002', -- sales_rep role
|
||||
'read', 'reports',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- Cross-org rule grants access
|
||||
SELECT morbac.t('Judy (Sales sales_rep) reads GlobalTech reports via cross-org rule',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'reports'
|
||||
), TRUE);
|
||||
|
||||
-- Judy at Sales can still read Sales docs normally (local rule, no cross-org needed)
|
||||
SELECT morbac.t('Judy (Sales sales_rep) reads Sales documents [local rule]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000003'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Judy does NOT have permission to read GlobalTech documents (cross-org rule only covers reports)
|
||||
SELECT morbac.t('Judy (Sales sales_rep) reads GlobalTech documents [no rule for documents]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: User must hold role in source org
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Role must be held in source org ---'
|
||||
|
||||
-- Karl has no role anywhere — cannot use the Sales->GlobalTech cross-org rule
|
||||
SELECT morbac.t('Karl (no role) reads GlobalTech reports via cross-org rule [no role in source]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'reports'
|
||||
), FALSE);
|
||||
|
||||
-- Karl also has no access to GlobalTech documents
|
||||
SELECT morbac.t('Karl (no role) reads GlobalTech documents [no access anywhere]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Cross-org prohibition — blocks access even with regular permission
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. Cross-org prohibition ---'
|
||||
|
||||
-- Engineering engineer can normally read Engineering documents (local perm)
|
||||
SELECT morbac.t('Judy (Engineering engineer) reads Engineering documents [local perm]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- Add Diana (sales_manager at Sales) for cross-org prohibition test
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('30000000-0000-0000-0000-000000000015', '20000000-0003-0000-0000-000000000001', '10000000-0000-0000-0000-000000000003');
|
||||
|
||||
-- Add cross-org prohibition: Sales sales_manager prohibited from writing Engineering documents
|
||||
INSERT INTO morbac.cross_org_rules
|
||||
(source_org_id, target_org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000003', -- source: Sales Dept
|
||||
'10000000-0000-0000-0000-000000000002', -- target: Engineering Dept
|
||||
'20000000-0003-0000-0000-000000000001', -- sales_manager role
|
||||
'write', 'documents',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'prohibition'
|
||||
);
|
||||
|
||||
-- Also give a cross-org permission that would otherwise allow the write
|
||||
INSERT INTO morbac.cross_org_rules
|
||||
(source_org_id, target_org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000003', -- source: Sales Dept
|
||||
'10000000-0000-0000-0000-000000000002', -- target: Engineering Dept
|
||||
'20000000-0003-0000-0000-000000000001', -- sales_manager role
|
||||
'write', 'documents',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- Cross-org prohibition takes precedence over permission
|
||||
SELECT morbac.t('Diana (Sales sales_manager) writes Engineering documents [cross-org prohibition wins]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000015'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'write', 'documents'
|
||||
), FALSE);
|
||||
|
||||
-- Diana can read Engineering documents (no prohibition for read)
|
||||
INSERT INTO morbac.cross_org_rules
|
||||
(source_org_id, target_org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000003', -- source: Sales Dept
|
||||
'10000000-0000-0000-0000-000000000002', -- target: Engineering Dept
|
||||
'20000000-0003-0000-0000-000000000001', -- sales_manager role
|
||||
'read', 'documents',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- No prohibition for read, permission applies
|
||||
SELECT morbac.t('Diana (Sales sales_manager) reads Engineering documents [cross-org permission]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000015'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'read', 'documents'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: Cross-org + activity hierarchy
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Cross-org with activity hierarchy ---'
|
||||
|
||||
-- The sales_rep cross-org rule grants 'read reports' in GlobalTech.
|
||||
-- With activity hierarchy (write->read), requesting 'write reports' also matches
|
||||
-- the 'read' rule (get_effective_activities('write') = {write, read}).
|
||||
SELECT morbac.t('Judy (Sales sales_rep) writes GlobalTech reports via cross-org + activity hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'write', 'reports'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: Cross-org + view hierarchy
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. Cross-org with view hierarchy ---'
|
||||
|
||||
-- Add a cross-org rule for 'read documents' (not financial_data directly)
|
||||
-- With view hierarchy (financial_data->documents), 'read documents' cross-org rule
|
||||
-- also covers 'read financial_data' requests
|
||||
INSERT INTO morbac.cross_org_rules
|
||||
(source_org_id, target_org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000003', -- source: Sales Dept
|
||||
'10000000-0000-0000-0000-000000000001', -- target: GlobalTech HQ
|
||||
'20000000-0003-0000-0000-000000000002', -- sales_rep role
|
||||
'read', 'documents',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- get_effective_views('financial_data') = {financial_data, documents}
|
||||
-- so 'read documents' cross-org rule covers 'read financial_data' request
|
||||
SELECT morbac.t('Judy (Sales sales_rep) reads GlobalTech financial_data via cross-org + view hierarchy',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'financial_data'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Temporal cross-org rule
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 7. Temporal cross-org rule ---'
|
||||
|
||||
-- Add a time-limited cross-org rule for eng_auditor to read GlobalTech audit_logs
|
||||
INSERT INTO morbac.cross_org_rules
|
||||
(source_org_id, target_org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000002', -- source: Engineering
|
||||
'10000000-0000-0000-0000-000000000001', -- target: GlobalTech HQ
|
||||
'20000000-0002-0000-0000-000000000003', -- eng_auditor role
|
||||
'read', 'audit_logs',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission',
|
||||
now() - interval '1 hour',
|
||||
now() + interval '1 day'
|
||||
);
|
||||
|
||||
-- Nina (Engineering eng_auditor) reads GlobalTech audit_logs within window
|
||||
SELECT morbac.t('Nina (Engineering eng_auditor) reads GlobalTech audit_logs [temporal cross-org, active]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000014'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'audit_logs'
|
||||
), TRUE);
|
||||
|
||||
-- Expire the cross-org rule
|
||||
UPDATE morbac.cross_org_rules
|
||||
SET valid_until = now() - interval '1 second'
|
||||
WHERE source_org_id = '10000000-0000-0000-0000-000000000002'
|
||||
AND target_org_id = '10000000-0000-0000-0000-000000000001'
|
||||
AND role_id = '20000000-0002-0000-0000-000000000003'
|
||||
AND activity = 'read' AND view = 'audit_logs';
|
||||
|
||||
-- Temporal rule expired
|
||||
SELECT morbac.t('Nina (Engineering eng_auditor) reads GlobalTech audit_logs [temporal cross-org, expired]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000014'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'audit_logs'
|
||||
), FALSE);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Cross-Organization Rules Tests Completed ==='
|
||||
|
||||
+260
-34
@@ -1,54 +1,280 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Audit Logging Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers audit logging functionality including:
|
||||
-- - Enabling audit on tables
|
||||
-- - Auditing INSERT operations
|
||||
-- - Auditing UPDATE operations
|
||||
-- - Auditing DELETE operations
|
||||
-- - Audit log queries and reporting
|
||||
-- - Disabling audit
|
||||
-- =============================================================================
|
||||
-- Audit Logging Tests
|
||||
-- =============================================================================
|
||||
-- Tests the audit system: enable_audit / disable_audit and the resulting
|
||||
-- audit_log entries for INSERT, UPDATE, and DELETE operations.
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- Cases:
|
||||
-- 1. Enable audit creates trigger on specified table
|
||||
-- 2. INSERT to audited table creates an audit log entry
|
||||
-- 3. DELETE from audited table creates an audit log entry
|
||||
-- 4. UPDATE to audited table creates an audit log entry with changed_fields
|
||||
-- 5. All operations for a record_id are queryable
|
||||
-- 6. Multiple tables can be audited independently
|
||||
-- 7. Disable audit removes the trigger
|
||||
-- 8. After disabling, changes are no longer logged
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 06_cross_org.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Audit Logging ==='
|
||||
\echo '================================================================'
|
||||
\echo '07 — AUDIT LOGGING'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Clear any existing audit log entries to start fresh
|
||||
TRUNCATE morbac.audit_log;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Enable audit on user_roles
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Enable audit on user_roles ---'
|
||||
|
||||
-- Enable audit on user_roles table
|
||||
SELECT morbac.enable_audit('user_roles');
|
||||
|
||||
\echo 'Audit trigger created for user_roles table'
|
||||
-- Trigger must exist on user_roles after enable
|
||||
SELECT morbac.t('Trigger exists on user_roles after enable_audit',
|
||||
(SELECT COUNT(*) > 0 FROM information_schema.triggers
|
||||
WHERE event_object_schema = 'morbac'
|
||||
AND event_object_table = 'user_roles'),
|
||||
TRUE);
|
||||
|
||||
-- Perform some auditable operations
|
||||
-- No entries before any change
|
||||
SELECT morbac.t_eq('Audit log count before operations = 0',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'user_roles')::bigint,
|
||||
0);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: INSERT is logged
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. INSERT is logged ---'
|
||||
|
||||
-- Assign a new role to Karl (user who has no role)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||
VALUES (
|
||||
'ffffffff-0000-0000-0000-000000000001'::uuid,
|
||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', -- employee role
|
||||
'11111111-1111-1111-1111-111111111111'
|
||||
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||
'20000000-0001-0000-0000-000000000005', -- intern role
|
||||
'10000000-0000-0000-0000-000000000001'
|
||||
);
|
||||
|
||||
-- Exactly 1 INSERT entry for user_roles
|
||||
SELECT morbac.t_eq('Audit log has 1 INSERT entry after INSERT into user_roles',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log
|
||||
WHERE table_name = 'user_roles' AND operation = 'INSERT')::bigint,
|
||||
1);
|
||||
|
||||
-- new_data must contain the inserted record
|
||||
SELECT morbac.t('INSERT audit entry contains user_id in new_data',
|
||||
(SELECT new_data->>'user_id' FROM morbac.audit_log
|
||||
WHERE table_name = 'user_roles' AND operation = 'INSERT'
|
||||
LIMIT 1) = '30000000-0000-0000-0000-000000000011',
|
||||
TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: DELETE is logged
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. DELETE is logged ---'
|
||||
|
||||
-- Delete a user role
|
||||
DELETE FROM morbac.user_roles
|
||||
WHERE user_id = 'ffffffff-0000-0000-0000-000000000001'::uuid
|
||||
AND role_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
||||
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000005'
|
||||
AND org_id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
\echo 'Audit log entries for user_roles operations:'
|
||||
SELECT
|
||||
operation,
|
||||
record_id,
|
||||
'N/A' as old_active,
|
||||
'N/A' as new_active,
|
||||
session_user,
|
||||
timestamp
|
||||
FROM morbac.audit_log
|
||||
WHERE table_name = 'user_roles'
|
||||
ORDER BY timestamp;
|
||||
-- Now 2 entries: INSERT + DELETE
|
||||
SELECT morbac.t_eq('Audit log has 2 entries after DELETE (INSERT + DELETE)',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'user_roles')::bigint,
|
||||
2);
|
||||
|
||||
-- DELETE entry must have old_data populated
|
||||
SELECT morbac.t('DELETE audit entry has old_data populated',
|
||||
(SELECT old_data IS NOT NULL FROM morbac.audit_log
|
||||
WHERE table_name = 'user_roles' AND operation = 'DELETE'
|
||||
LIMIT 1),
|
||||
TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: Enable audit on rules and test UPDATE logging
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. UPDATE on rules table is logged (with changed_fields) ---'
|
||||
|
||||
SELECT morbac.enable_audit('rules');
|
||||
|
||||
-- Insert a rule to audit (using explicit id for record_id tracking)
|
||||
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000009', -- contractor
|
||||
'read', 'public_data',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- 1 INSERT entry for rules
|
||||
SELECT morbac.t_eq('Audit log has 1 INSERT entry for rules',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log
|
||||
WHERE table_name = 'rules' AND operation = 'INSERT')::bigint,
|
||||
1);
|
||||
|
||||
-- Update the modality (simulate a policy change)
|
||||
UPDATE morbac.rules
|
||||
SET modality = 'prohibition'
|
||||
WHERE id = 'a0000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- 1 UPDATE entry with changed_fields
|
||||
SELECT morbac.t_eq('Audit log has 1 UPDATE entry for rules',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log
|
||||
WHERE table_name = 'rules' AND operation = 'UPDATE')::bigint,
|
||||
1);
|
||||
|
||||
-- changed_fields must be non-null for UPDATE
|
||||
SELECT morbac.t('UPDATE audit entry has changed_fields populated',
|
||||
(SELECT changed_fields IS NOT NULL FROM morbac.audit_log
|
||||
WHERE table_name = 'rules' AND operation = 'UPDATE'
|
||||
LIMIT 1),
|
||||
TRUE);
|
||||
|
||||
-- old_data and new_data both populated for UPDATE
|
||||
SELECT morbac.t('UPDATE audit entry: old modality = permission',
|
||||
(SELECT old_data->>'modality' FROM morbac.audit_log
|
||||
WHERE table_name = 'rules' AND operation = 'UPDATE'
|
||||
LIMIT 1) = 'permission',
|
||||
TRUE);
|
||||
|
||||
SELECT morbac.t('UPDATE audit entry: new modality = prohibition',
|
||||
(SELECT new_data->>'modality' FROM morbac.audit_log
|
||||
WHERE table_name = 'rules' AND operation = 'UPDATE'
|
||||
LIMIT 1) = 'prohibition',
|
||||
TRUE);
|
||||
|
||||
-- Clean up the audited rule
|
||||
DELETE FROM morbac.rules WHERE id = 'a0000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: All operations queryable by record_id
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. Query audit log by record_id ---'
|
||||
|
||||
-- The test rule had INSERT, UPDATE, DELETE — should be 3 entries
|
||||
SELECT morbac.t_eq('Audit log has 3 entries for test rule record (INSERT + UPDATE + DELETE)',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log
|
||||
WHERE table_name = 'rules'
|
||||
AND record_id = 'a0000000-0000-0000-0000-000000000001')::bigint,
|
||||
3);
|
||||
|
||||
-- All three operations present
|
||||
SELECT morbac.t('INSERT, UPDATE, DELETE all present for test rule record',
|
||||
(SELECT COUNT(DISTINCT operation) FROM morbac.audit_log
|
||||
WHERE table_name = 'rules'
|
||||
AND record_id = 'a0000000-0000-0000-0000-000000000001')::bigint = 3,
|
||||
TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: Multiple tables audited independently
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. Multiple tables audited independently ---'
|
||||
|
||||
SELECT morbac.enable_audit('delegations');
|
||||
|
||||
-- Insert a delegation to trigger audit on the delegations table
|
||||
INSERT INTO morbac.delegations
|
||||
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
|
||||
VALUES (
|
||||
'de000099-0000-0000-0000-000000000099',
|
||||
'30000000-0000-0000-0000-000000000001', -- Alice (CEO)
|
||||
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||
'20000000-0001-0000-0000-000000000001', -- ceo role
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
now(),
|
||||
now() + interval '1 hour'
|
||||
);
|
||||
|
||||
-- delegations table now has audit entries
|
||||
SELECT morbac.t('Audit log has at least 1 entry for delegations table',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'delegations') >= 1,
|
||||
TRUE);
|
||||
|
||||
-- All three tables have audit entries
|
||||
SELECT morbac.t('user_roles, rules, and delegations all have audit entries',
|
||||
(SELECT COUNT(DISTINCT table_name) FROM morbac.audit_log
|
||||
WHERE table_name IN ('user_roles', 'rules', 'delegations'))::bigint = 3,
|
||||
TRUE);
|
||||
|
||||
-- Clean up the test delegation
|
||||
DELETE FROM morbac.delegations WHERE id = 'de000099-0000-0000-0000-000000000099';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Disable audit removes trigger
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 7. Disable audit removes trigger ---'
|
||||
|
||||
\echo 'Disable audit on user_roles:'
|
||||
SELECT morbac.disable_audit('user_roles');
|
||||
|
||||
-- Audit trigger must be removed from user_roles (other system triggers may remain)
|
||||
SELECT morbac.t('No audit trigger on user_roles after disable_audit',
|
||||
(SELECT COUNT(*) = 0 FROM information_schema.triggers
|
||||
WHERE event_object_schema = 'morbac'
|
||||
AND event_object_table = 'user_roles'
|
||||
AND trigger_name LIKE '%audit%'),
|
||||
TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 8: After disabling, changes are no longer logged
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 8. Disable audit stops logging ---'
|
||||
|
||||
-- Record current count for user_roles before post-disable operations
|
||||
-- (2 entries from sections 2+3)
|
||||
DO $$
|
||||
DECLARE
|
||||
count_before BIGINT;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO count_before FROM morbac.audit_log WHERE table_name = 'user_roles';
|
||||
-- Store in a temp table for later assertion
|
||||
CREATE TEMP TABLE IF NOT EXISTS audit_count_check (cnt BIGINT);
|
||||
DELETE FROM audit_count_check;
|
||||
INSERT INTO audit_count_check VALUES (count_before);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Perform another user_roles change — should NOT be logged
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000011',
|
||||
'20000000-0001-0000-0000-000000000005', -- intern
|
||||
'10000000-0000-0000-0000-000000000001'
|
||||
);
|
||||
|
||||
DELETE FROM morbac.user_roles
|
||||
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000005';
|
||||
|
||||
-- Count after operations must equal count before (no new entries)
|
||||
SELECT morbac.t('user_roles audit count unchanged after post-disable operations',
|
||||
(SELECT COUNT(*) FROM morbac.audit_log WHERE table_name = 'user_roles')::bigint
|
||||
= (SELECT cnt FROM audit_count_check),
|
||||
TRUE);
|
||||
|
||||
DROP TABLE IF EXISTS audit_count_check;
|
||||
|
||||
-- Disable the remaining audit triggers
|
||||
SELECT morbac.disable_audit('rules');
|
||||
SELECT morbac.disable_audit('delegations');
|
||||
|
||||
-- All audit triggers removed
|
||||
SELECT morbac.t('No audit triggers remain after all disable_audit calls',
|
||||
(SELECT COUNT(*) = 0 FROM information_schema.triggers
|
||||
WHERE event_object_schema = 'morbac'
|
||||
AND trigger_name LIKE '%audit%'),
|
||||
TRUE);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Audit Logging Tests Completed ==='
|
||||
|
||||
+333
-28
@@ -1,37 +1,342 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Administration Rules Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers administration rules including:
|
||||
-- - Administrative activities (create_rule, etc.)
|
||||
-- - Administrative permission checks (is_admin_allowed)
|
||||
-- - Role-based administrative access control
|
||||
-- - Administrative authorization decisions
|
||||
-- =============================================================================
|
||||
-- Administration Rules Tests
|
||||
-- =============================================================================
|
||||
-- Tests the admin meta-policy system: who can manage policies, roles, and users.
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- 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 '=== Administration Rules ==='
|
||||
\echo '================================================================'
|
||||
\echo '08 — ADMINISTRATION RULES'
|
||||
\echo '================================================================'
|
||||
|
||||
-- Only admin role can create rules
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, context_id, modality) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'create_rule', 'rules', (SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: Default deny — no admin rule means no admin access
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. Default deny: no admin rule ---'
|
||||
|
||||
\echo 'Alice (admin) can create rules:'
|
||||
SELECT morbac.is_admin_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'create_rule',
|
||||
'rules'
|
||||
) as can_create_rules;
|
||||
-- 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);
|
||||
|
||||
\echo 'Bob (employee) cannot create rules:'
|
||||
SELECT morbac.is_admin_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'create_rule',
|
||||
'rules'
|
||||
) as can_create_rules;
|
||||
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 ==='
|
||||
|
||||
+455
-75
@@ -1,91 +1,471 @@
|
||||
-- ============================================================================
|
||||
-- MORBAC PostgreSQL Extension - Utility Functions and Context Tests
|
||||
-- ============================================================================
|
||||
-- This test file covers utility functions and context evaluation including:
|
||||
-- - Obligations (pending_obligations)
|
||||
-- - Recommendations (pending_recommendations)
|
||||
-- - Context evaluation (eval_context)
|
||||
-- - User role queries (user_roles_in_org, user_has_role)
|
||||
-- - Policy compilation and idempotency
|
||||
-- - Adding new policy entries
|
||||
-- =============================================================================
|
||||
-- Utilities, Derived Roles, RLS, and Policy DSL Tests
|
||||
-- =============================================================================
|
||||
-- Tests miscellaneous utility functions and advanced features:
|
||||
--
|
||||
-- Prerequisites: Assumes 00_setup.sql has been run to set up base data
|
||||
-- ============================================================================
|
||||
-- 1. pending_obligations — returns obligation rules for a user
|
||||
-- 2. pending_recommendations — returns recommendation rules for a user
|
||||
-- 3. Obligations/recommendations do NOT affect is_allowed()
|
||||
-- 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 helper: rls_check() with session variables
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 08_admin.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '=== Obligations and Recommendations ==='
|
||||
\echo '================================================================'
|
||||
\echo '09 — UTILITIES, DERIVED ROLES, RLS, POLICY DSL'
|
||||
\echo '================================================================'
|
||||
|
||||
\echo 'Obligations for Bob (employee at Acme):'
|
||||
SELECT * FROM morbac.pending_obligations(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 1: pending_obligations
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 1. pending_obligations ---'
|
||||
|
||||
-- Dave (employee) has obligation: 'read reports' (set up in 00_setup via Policy DSL)
|
||||
-- The context is 'always' so it evaluates TRUE
|
||||
SELECT morbac.t('Dave (employee) has at least 1 pending obligation',
|
||||
(SELECT COUNT(*) FROM morbac.pending_obligations(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)) >= 1,
|
||||
TRUE);
|
||||
|
||||
-- Dave's obligation is for 'read reports'
|
||||
SELECT morbac.t('Dave has pending obligation: read reports',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.pending_obligations(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE activity = 'read' AND view = 'reports'
|
||||
), TRUE);
|
||||
|
||||
-- Karl (no role) has no obligations
|
||||
SELECT morbac.t_eq('Karl (no role) has 0 pending obligations',
|
||||
(SELECT COUNT(*) FROM morbac.pending_obligations(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint,
|
||||
0);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 2: pending_recommendations
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 2. pending_recommendations ---'
|
||||
|
||||
-- Dave (employee) has recommendation: 'read reports' during end_of_quarter (evaluates TRUE)
|
||||
SELECT morbac.t('Dave (employee) has at least 1 pending recommendation',
|
||||
(SELECT COUNT(*) FROM morbac.pending_recommendations(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)) >= 1,
|
||||
TRUE);
|
||||
|
||||
-- Eve (intern) has no recommendations
|
||||
SELECT morbac.t_eq('Eve (intern) has 0 pending recommendations',
|
||||
(SELECT COUNT(*) FROM morbac.pending_recommendations(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint,
|
||||
0);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 3: Obligations / Recommendations do NOT affect is_allowed
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 3. Obligations do not affect authorization ---'
|
||||
|
||||
-- Add a pure obligation for a view Dave has no permission rule for
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004', -- employee
|
||||
'read', 'contracts',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'obligation'
|
||||
);
|
||||
|
||||
\echo ''
|
||||
\echo 'Recommendations for Charlie (staff at Beta):'
|
||||
SELECT * FROM morbac.pending_recommendations(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid
|
||||
-- Obligation alone doesn't grant access
|
||||
SELECT morbac.t('Dave has obligation for read contracts but no permission [denied]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'contracts'
|
||||
), FALSE);
|
||||
|
||||
-- Recommendations similarly do not grant access
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000004', -- employee
|
||||
'read', 'audit_logs',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'recommendation'
|
||||
);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Utility Functions ==='
|
||||
-- Recommendation alone doesn't grant access
|
||||
SELECT morbac.t('Dave has recommendation for read audit_logs but no permission [denied]',
|
||||
morbac.is_allowed_nocache(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'read', 'audit_logs'
|
||||
), FALSE);
|
||||
|
||||
\echo 'Roles for Charlie at Acme Corp:'
|
||||
SELECT * FROM morbac.user_roles_in_org(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 4: user_has_role
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 4. user_has_role ---'
|
||||
|
||||
-- Alice has the CEO role at GlobalTech
|
||||
SELECT morbac.t('Alice has ceo role at GlobalTech',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'ceo'
|
||||
), TRUE);
|
||||
|
||||
-- Alice does NOT have employee role (user_has_role checks direct assignment, not hierarchy)
|
||||
SELECT morbac.t('Alice does NOT have employee role directly at GlobalTech',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'employee'
|
||||
), FALSE);
|
||||
|
||||
-- Dave has employee role
|
||||
SELECT morbac.t('Dave has employee role at GlobalTech',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'employee'
|
||||
), TRUE);
|
||||
|
||||
-- Dave does not have auditor role
|
||||
SELECT morbac.t('Dave does not have auditor role at GlobalTech',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'auditor'
|
||||
), FALSE);
|
||||
|
||||
-- Karl has no role
|
||||
SELECT morbac.t('Karl does not have employee role at GlobalTech [no role]',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'employee'
|
||||
), FALSE);
|
||||
|
||||
-- Judy has engineer at Engineering but not at GlobalTech
|
||||
SELECT morbac.t('Judy does not have engineer role at GlobalTech [wrong org]',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'engineer'
|
||||
), FALSE);
|
||||
|
||||
SELECT morbac.t('Judy has engineer role at Engineering [correct org]',
|
||||
morbac.user_has_role(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'engineer'
|
||||
), TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 5: user_roles_in_org
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 5. user_roles_in_org ---'
|
||||
|
||||
-- Alice has exactly 1 role at GlobalTech (ceo)
|
||||
SELECT morbac.t_eq('Alice has exactly 1 role at GlobalTech (ceo)',
|
||||
(SELECT COUNT(*) FROM morbac.user_roles_in_org(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint,
|
||||
1);
|
||||
|
||||
SELECT morbac.t('Alice role at GlobalTech is ceo',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.user_roles_in_org(
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_name = 'ceo'
|
||||
), TRUE);
|
||||
|
||||
-- Judy has 1 role at Engineering (engineer)
|
||||
SELECT morbac.t_eq('Judy has 1 role at Engineering',
|
||||
(SELECT COUNT(*) FROM morbac.user_roles_in_org(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000002'::uuid
|
||||
))::bigint,
|
||||
1);
|
||||
|
||||
-- Judy has 1 role at Sales (sales_rep)
|
||||
SELECT morbac.t_eq('Judy has 1 role at Sales',
|
||||
(SELECT COUNT(*) FROM morbac.user_roles_in_org(
|
||||
'30000000-0000-0000-0000-000000000010'::uuid,
|
||||
'10000000-0000-0000-0000-000000000003'::uuid
|
||||
))::bigint,
|
||||
1);
|
||||
|
||||
-- Karl has no roles at GlobalTech
|
||||
SELECT morbac.t_eq('Karl has 0 roles at GlobalTech',
|
||||
(SELECT COUNT(*) FROM morbac.user_roles_in_org(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
))::bigint,
|
||||
0);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 6: eval_context
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo '--- 6. eval_context ---'
|
||||
|
||||
-- 'always' context always returns TRUE
|
||||
SELECT morbac.t('eval_context(always) = TRUE',
|
||||
morbac.eval_context((SELECT id FROM morbac.contexts WHERE name = 'always')),
|
||||
TRUE);
|
||||
|
||||
-- 'business_hours' context returns TRUE (our test implementation)
|
||||
SELECT morbac.t('eval_context(business_hours) = TRUE',
|
||||
morbac.eval_context((SELECT id FROM morbac.contexts WHERE name = 'business_hours')),
|
||||
TRUE);
|
||||
|
||||
-- 'after_hours' context returns FALSE (our test implementation)
|
||||
SELECT morbac.t('eval_context(after_hours) = FALSE',
|
||||
morbac.eval_context((SELECT id FROM morbac.contexts WHERE name = 'after_hours')),
|
||||
FALSE);
|
||||
|
||||
-- 'end_of_quarter' context returns TRUE (our test implementation)
|
||||
SELECT morbac.t('eval_context(end_of_quarter) = TRUE',
|
||||
morbac.eval_context((SELECT id FROM morbac.contexts WHERE name = 'end_of_quarter')),
|
||||
TRUE);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 7: Policy DSL — idempotency
|
||||
-- ---------------------------------------------------------------------------
|
||||
\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 ---'
|
||||
|
||||
-- Create a derived role: 'senior_employee' — dynamically granted to Dave (only)
|
||||
INSERT INTO morbac.roles (id, org_id, name, description)
|
||||
VALUES (
|
||||
'20000000-0001-0000-0000-000000000011',
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'senior_employee',
|
||||
'Senior employee — granted dynamically based on tenure'
|
||||
);
|
||||
|
||||
\echo ''
|
||||
\echo 'Roles for Charlie at Beta Inc:'
|
||||
SELECT * FROM morbac.user_roles_in_org(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid
|
||||
-- Evaluator function: returns TRUE only for Dave at GlobalTech
|
||||
CREATE OR REPLACE FUNCTION morbac.eval_senior_employee(p_user_id UUID, p_org_id UUID)
|
||||
RETURNS BOOLEAN LANGUAGE plpgsql STABLE AS $$
|
||||
BEGIN
|
||||
-- In a real system, this would query app.employees for tenure >= 2 years.
|
||||
-- For testing, we hardcode Dave's UUID.
|
||||
RETURN p_user_id = '30000000-0000-0000-0000-000000000004'::UUID
|
||||
AND p_org_id = '10000000-0000-0000-0000-000000000001'::UUID;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO morbac.derived_roles (role_id, condition_evaluator, description)
|
||||
VALUES (
|
||||
'20000000-0001-0000-0000-000000000011',
|
||||
'morbac.eval_senior_employee'::regproc,
|
||||
'Senior employee role: granted to employees with 2+ years tenure'
|
||||
);
|
||||
|
||||
-- Grant senior_employee permission to read financial_data directly
|
||||
-- (separate from the view hierarchy path, to test derived role specifically)
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0001-0000-0000-000000000011', -- senior_employee role
|
||||
'read', 'financial_data',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||
'permission'
|
||||
);
|
||||
|
||||
-- Dave should have senior_employee via derived role in get_comprehensive_roles
|
||||
SELECT morbac.t('Dave has senior_employee derived role in comprehensive roles',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000011'
|
||||
AND source = 'derived'
|
||||
), TRUE);
|
||||
|
||||
-- Eve (intern) does NOT satisfy the evaluator — no derived role
|
||||
SELECT morbac.t('Eve has no derived roles in comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000005'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE source = 'derived'
|
||||
), TRUE);
|
||||
|
||||
-- Karl (no role) does not get the derived role
|
||||
SELECT morbac.t('Karl has no derived roles in comprehensive roles',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE source = 'derived'
|
||||
), TRUE);
|
||||
|
||||
-- Negative assignment overrides derived role
|
||||
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason)
|
||||
VALUES (
|
||||
'30000000-0000-0000-0000-000000000004', -- Dave
|
||||
'20000000-0001-0000-0000-000000000011', -- senior_employee
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'Derived role explicitly blocked for testing'
|
||||
);
|
||||
|
||||
-- Derived role is blocked by negative assignment
|
||||
SELECT morbac.t('Dave: senior_employee derived role absent after negative assignment',
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000011'
|
||||
), TRUE);
|
||||
|
||||
-- Dave's direct employee role is still present (negative only blocks senior_employee)
|
||||
SELECT morbac.t('Dave: employee role still present after senior_employee negated',
|
||||
EXISTS(
|
||||
SELECT 1 FROM morbac.get_comprehensive_roles(
|
||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||
'10000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
WHERE role_id = '20000000-0001-0000-0000-000000000004'
|
||||
AND source = 'direct'
|
||||
), TRUE);
|
||||
|
||||
-- Clean up negative assignment
|
||||
DELETE FROM morbac.negative_role_assignments
|
||||
WHERE user_id = '30000000-0000-0000-0000-000000000004'
|
||||
AND role_id = '20000000-0001-0000-0000-000000000011';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Section 10: RLS helper functions
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo ''
|
||||
\echo 'Does Alice have admin role at Acme Corp?'
|
||||
SELECT morbac.user_has_role(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'admin'
|
||||
) as has_admin_role;
|
||||
\echo '--- 10. RLS helper: current_user_id, current_org_id, rls_check ---'
|
||||
|
||||
-- Without session variables set, current_user_id and current_org_id return NULL
|
||||
SELECT morbac.t_null('current_user_id() returns NULL without session var',
|
||||
morbac.current_user_id()::text);
|
||||
|
||||
SELECT morbac.t_null('current_org_id() returns NULL without session var',
|
||||
morbac.current_org_id()::text);
|
||||
|
||||
-- rls_check without session context returns FALSE (no user/org)
|
||||
SELECT morbac.t('rls_check without session context returns FALSE',
|
||||
morbac.rls_check('read', 'documents'),
|
||||
FALSE);
|
||||
|
||||
-- Set session variables (Dave at GlobalTech)
|
||||
-- rls_check reads morbac.user_id and morbac.org_id session variables
|
||||
SET morbac.user_id = '30000000-0000-0000-0000-000000000004';
|
||||
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- rls_check uses morbac.user_id / morbac.org_id session vars
|
||||
SELECT morbac.t('rls_check(read, documents) as Dave at GlobalTech',
|
||||
morbac.rls_check('read', 'documents'),
|
||||
TRUE);
|
||||
|
||||
-- rls_check for activity Dave cannot do
|
||||
SELECT morbac.t('rls_check(approve, documents) as Dave at GlobalTech [no permission]',
|
||||
morbac.rls_check('approve', 'documents'),
|
||||
FALSE);
|
||||
|
||||
-- Switch to Karl (no role)
|
||||
SET morbac.user_id = '30000000-0000-0000-0000-000000000011';
|
||||
|
||||
SELECT morbac.t('rls_check(read, documents) as Karl (no role) [denied]',
|
||||
morbac.rls_check('read', 'documents'),
|
||||
FALSE);
|
||||
|
||||
-- Reset session to avoid polluting other queries
|
||||
RESET morbac.user_id;
|
||||
RESET morbac.org_id;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Context Evaluation ==='
|
||||
|
||||
\echo 'Evaluating always context:'
|
||||
SELECT morbac.eval_context(
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
) as result;
|
||||
|
||||
\echo ''
|
||||
\echo 'Evaluating business_hours context:'
|
||||
SELECT morbac.eval_context(
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'business_hours')
|
||||
) as result;
|
||||
|
||||
\echo ''
|
||||
\echo 'Evaluating weekend context:'
|
||||
SELECT morbac.eval_context(
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'weekend')
|
||||
) as result;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Policy Re-compilation (Idempotency Test) ==='
|
||||
|
||||
\echo 'Compiling policy again (should show 0 new rules, idempotent):'
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
\echo ''
|
||||
\echo '=== Adding New Policy Entry ==='
|
||||
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||
('Acme Corp', 'admin', 'approve', 'documents', 'permission', 'always');
|
||||
|
||||
\echo 'Compiling new policy entry:'
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
\echo ''
|
||||
\echo '=== Utility Functions and Context Tests Completed ==='
|
||||
\echo '=== Utilities, Derived Roles, RLS, and Policy DSL Tests Completed ==='
|
||||
|
||||
+49
-22
@@ -2,9 +2,15 @@
|
||||
|
||||
# test.sh - Run tests against the PostgreSQL extension using psql
|
||||
# Usage: ./test.sh [database_name] [postgres_user]
|
||||
# Note: This script expects the extension to be already installed via 'make install'
|
||||
#
|
||||
# Assertions in SQL test files must output rows whose value starts with:
|
||||
# CHECK PASS: <description> — assertion passed
|
||||
# CHECK FAIL: <description> — assertion failed
|
||||
#
|
||||
# Any SQL ERROR also counts as a failure.
|
||||
# The script exits with code 1 if any file has failures or SQL errors.
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
set -e
|
||||
|
||||
DB="${1:-morbac_test}"
|
||||
PG_USER="${2:-$USER}"
|
||||
@@ -18,43 +24,64 @@ if [ ! -d "$TEST_DIR" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! psql -U "$PG_USER" -d "$DB" -c '\q'; then
|
||||
if ! psql -U "$PG_USER" -d "$DB" -c '\q' 2>/dev/null; then
|
||||
echo "Error: cannot connect to database: $DB" >&2
|
||||
echo "Ensure the database exists and is accessible" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running tests on database $DB"
|
||||
echo ""
|
||||
|
||||
total=0
|
||||
passed=0
|
||||
failed=0
|
||||
total_files=0
|
||||
passed_files=0
|
||||
failed_files=0
|
||||
total_pass=0
|
||||
total_fail=0
|
||||
|
||||
set +e # Temporarily disable exit on error to capture test failures
|
||||
set +e # Capture failures without aborting
|
||||
|
||||
for test_file in "$TEST_DIR"/[0-9][0-9]_*.sql; do
|
||||
if [ -f "$test_file" ]; then
|
||||
test_name=$(basename "$test_file" .sql)
|
||||
echo " - $test_name"
|
||||
((total++))
|
||||
[ -f "$test_file" ] || continue
|
||||
|
||||
output=$(psql -U "$PG_USER" -d "$DB" -f "$test_file" 2>&1)
|
||||
exit_code=$?
|
||||
echo "$output"
|
||||
test_name=$(basename "$test_file" .sql)
|
||||
((total_files++))
|
||||
|
||||
if [ $exit_code -eq 0 ] && ! echo "$output" | grep -qE "ERROR|FAILED"; then
|
||||
((passed++))
|
||||
else
|
||||
((failed++))
|
||||
echo " FAILED"
|
||||
output=$(psql -U "$PG_USER" -d "$DB" -f "$test_file" 2>&1)
|
||||
exit_code=$?
|
||||
|
||||
# Count assertions
|
||||
file_pass=$(echo "$output" | grep -c "CHECK PASS:" || true)
|
||||
file_fail=$(echo "$output" | grep -c "CHECK FAIL:" || true)
|
||||
file_errors=$(echo "$output" | grep -c "^psql:.*ERROR:" || true)
|
||||
|
||||
total_pass=$((total_pass + file_pass))
|
||||
total_fail=$((total_fail + file_fail))
|
||||
|
||||
if [ $exit_code -ne 0 ] || [ "$file_fail" -gt 0 ] || [ "$file_errors" -gt 0 ]; then
|
||||
((failed_files++))
|
||||
echo " [FAIL] $test_name — ${file_pass} passed, ${file_fail} failed, ${file_errors} SQL error(s)"
|
||||
# Print only failing lines for a focused view
|
||||
if [ "$file_errors" -gt 0 ]; then
|
||||
echo "$output" | grep "^psql:.*ERROR:" | sed 's/^/ /'
|
||||
fi
|
||||
if [ "$file_fail" -gt 0 ]; then
|
||||
echo "$output" | grep "CHECK FAIL:" | sed 's/^/ /'
|
||||
fi
|
||||
else
|
||||
((passed_files++))
|
||||
echo " [PASS] $test_name — ${file_pass} assertion(s) passed"
|
||||
fi
|
||||
done
|
||||
|
||||
set -e # Re-enable exit on error
|
||||
set -e
|
||||
|
||||
echo "Tests: $total total, $passed passed, $failed failed"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Files : $total_files total | $passed_files passed | $failed_files failed"
|
||||
echo "Checks : $((total_pass + total_fail)) total | $total_pass passed | $total_fail failed"
|
||||
echo "========================================"
|
||||
|
||||
if [ $failed -gt 0 ]; then
|
||||
if [ $failed_files -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user