349 lines
18 KiB
PL/PgSQL
349 lines
18 KiB
PL/PgSQL
-- =============================================================================
|
||
-- Test Setup — GlobalTech Inc. Company Scenario
|
||
-- =============================================================================
|
||
-- This file establishes the full company structure used across all test files:
|
||
--
|
||
-- GlobalTech HQ (root)
|
||
-- ├── Engineering Dept (child)
|
||
-- └── Sales Dept (child)
|
||
--
|
||
-- 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 slate
|
||
DROP EXTENSION IF EXISTS pgmorbac CASCADE;
|
||
DROP SCHEMA IF EXISTS morbac CASCADE;
|
||
CREATE EXTENSION pgmorbac;
|
||
|
||
\echo '=== Setup: Verify schema created ==='
|
||
SELECT tablename FROM pg_tables WHERE schemaname = 'morbac' ORDER BY tablename;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- ORGANIZATIONS
|
||
-- ---------------------------------------------------------------------------
|
||
\echo ''
|
||
\echo '=== Setup: Organizations ==='
|
||
|
||
INSERT INTO morbac.orgs (id, name) VALUES
|
||
('10000000-0000-0000-0000-000000000001', 'GlobalTech HQ'),
|
||
('10000000-0000-0000-0000-000000000002', 'Engineering Dept'),
|
||
('10000000-0000-0000-0000-000000000003', 'Sales Dept');
|
||
|
||
-- 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, '—') 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 ==='
|
||
|
||
-- GlobalTech HQ roles
|
||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||
('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');
|
||
|
||
-- Engineering Dept roles
|
||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||
('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');
|
||
|
||
-- 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: Role Hierarchy (GlobalTech HQ) ==='
|
||
|
||
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');
|
||
|
||
-- 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
|
||
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 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', '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: Contexts ==='
|
||
|
||
-- 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 (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);
|
||
|
||
SELECT name, description FROM morbac.contexts ORDER BY name;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- POLICIES (via Policy DSL — resolved by compile_policy)
|
||
-- ---------------------------------------------------------------------------
|
||
\echo ''
|
||
\echo '=== Setup: Policies ==='
|
||
|
||
-- GlobalTech HQ policies
|
||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||
-- Intern: read public data only
|
||
('GlobalTech HQ', 'intern', 'read', 'public_data', 'permission', 'always'),
|
||
|
||
-- Employee: read documents and reports; write documents only during business hours
|
||
('GlobalTech HQ', 'employee', 'read', 'documents', 'permission', 'always'),
|
||
('GlobalTech HQ', 'employee', 'write', 'documents', 'permission', 'business_hours'),
|
||
('GlobalTech HQ', 'employee', 'read', 'reports', 'permission', 'always'),
|
||
('GlobalTech HQ', 'employee', 'read', 'public_data', 'permission', 'always'),
|
||
|
||
-- Obligation: employees must review reports weekly
|
||
('GlobalTech HQ', 'employee', 'read', 'reports', 'obligation', 'always'),
|
||
|
||
-- Manager: approve and delete documents; read financial data
|
||
('GlobalTech HQ', 'manager', 'approve','documents', 'permission', 'always'),
|
||
('GlobalTech HQ', 'manager', 'delete', 'documents', 'permission', 'always'),
|
||
('GlobalTech HQ', 'manager', 'read', 'financial_data', 'permission', 'always'),
|
||
|
||
-- HR Manager: full access to HR data
|
||
('GlobalTech HQ', 'hr_manager', 'read', 'hr_data', 'permission', 'always'),
|
||
('GlobalTech HQ', 'hr_manager', 'write', 'hr_data', 'permission', 'always'),
|
||
('GlobalTech HQ', 'hr_manager', 'delete', 'hr_data', 'permission', 'always'),
|
||
|
||
-- Auditor: read audit logs, financial data, and documents
|
||
('GlobalTech HQ', 'auditor', 'read', 'audit_logs', 'permission', 'always'),
|
||
('GlobalTech HQ', 'auditor', 'read', 'financial_data', 'permission', 'always'),
|
||
('GlobalTech HQ', 'auditor', 'read', 'documents', 'permission', 'always'),
|
||
|
||
-- Accountant: read and write financial data
|
||
('GlobalTech HQ', 'accountant', 'read', 'financial_data', 'permission', 'always'),
|
||
('GlobalTech HQ', 'accountant', 'write', 'financial_data', 'permission', 'always'),
|
||
|
||
-- Contractor: read documents; PROHIBITED from financial and HR data
|
||
('GlobalTech HQ', 'contractor', 'read', 'documents', 'permission', 'always'),
|
||
('GlobalTech HQ', 'contractor', 'read', 'financial_data', 'prohibition', 'always'),
|
||
('GlobalTech HQ', 'contractor', 'read', 'hr_data', 'prohibition', 'always'),
|
||
|
||
-- Compliance Officer: read audit logs and financial data
|
||
('GlobalTech HQ', 'compliance_officer', 'read', 'audit_logs', 'permission', 'always'),
|
||
('GlobalTech HQ', 'compliance_officer', 'read', 'financial_data', 'permission', 'always'),
|
||
|
||
-- Recommendation: staff should review reports end of quarter
|
||
-- Note: voided by the obligation above (conflict resolution: obligation > recommendation)
|
||
('GlobalTech HQ', 'employee', 'read', 'reports', 'recommendation','end_of_quarter'),
|
||
|
||
-- Recommendation: staff should stay informed on public data (no conflicting obligation)
|
||
('GlobalTech HQ', 'employee', 'read', 'public_data', 'recommendation','always');
|
||
|
||
-- Engineering Dept policies
|
||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||
('Engineering Dept', 'engineer', 'read', 'documents', 'permission', 'always'),
|
||
('Engineering Dept', 'engineer', 'write', 'documents', 'permission', 'always'),
|
||
('Engineering Dept', 'tech_lead', 'approve', 'documents', 'permission', 'always');
|
||
|
||
-- Sales Dept policies
|
||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||
('Sales Dept', 'sales_rep', 'read', 'documents', 'permission', 'always'),
|
||
('Sales Dept', 'sales_rep', 'write', 'contracts', 'permission', 'always'),
|
||
('Sales Dept', 'sales_manager', 'read', 'reports', 'permission', 'always'),
|
||
('Sales Dept', 'sales_manager', 'approve', 'contracts', 'permission', 'always');
|
||
|
||
-- Compile all policies into rules
|
||
\echo ''
|
||
\echo '=== Setup: Compiling Policies ==='
|
||
SELECT * FROM morbac.compile_policy();
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- 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)'
|