291 lines
11 KiB
SQL
291 lines
11 KiB
SQL
-- =============================================================================
|
|
-- System Access Tests
|
|
-- =============================================================================
|
|
-- morbac system tables are protected by RLS.
|
|
-- is_allowed() and its internals are SECURITY DEFINER to avoid recursion.
|
|
-- System view names are config-driven (system_view.* keys, default: orgs/roles/rules/...).
|
|
--
|
|
-- Scenarios:
|
|
-- 1. Default deny: no rule = no access to system views via is_allowed()
|
|
-- 2. Grant permissions via regular rules, verify access
|
|
-- 3. Prohibition overrides permission (standard engine behavior)
|
|
-- 4. Role hierarchy applies: senior role inherits permissions
|
|
-- 5. assign_role() / revoke_role() — SoD/cardinality enforcement, RLS guards the INSERT/DELETE
|
|
-- 6. RLS on morbac tables: session user cannot read/write without rules
|
|
-- 7. Rules are org-scoped
|
|
--
|
|
-- Prerequisites: 00_setup.sql -> 07_audit.sql
|
|
-- =============================================================================
|
|
|
|
\echo ''
|
|
\echo '================================================================'
|
|
\echo '08 — SYSTEM ACCESS'
|
|
\echo '================================================================'
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Section 1: Default deny
|
|
-- ---------------------------------------------------------------------------
|
|
\echo ''
|
|
\echo '--- 1. Default deny: no rule = no access to system views ---'
|
|
|
|
SELECT morbac.t('Carol (manager) create rules [default deny]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'rules'
|
|
), FALSE);
|
|
|
|
SELECT morbac.t('Grace (hr_manager) create user_roles [default deny]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000007'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'user_roles'
|
|
), FALSE);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Section 2: Grant permissions via regular rules
|
|
-- ---------------------------------------------------------------------------
|
|
\echo ''
|
|
\echo '--- 2. Grant permissions ---'
|
|
|
|
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
|
SELECT o.id, r.id, v.activity, v.view, c.id, v.modality::morbac.modality
|
|
FROM (VALUES
|
|
('GlobalTech HQ', 'manager', 'create', 'rules', 'always', 'permission'),
|
|
('GlobalTech HQ', 'manager', 'delete', 'rules', 'always', 'permission'),
|
|
('GlobalTech HQ', 'hr_manager', 'read', 'user_roles', 'always', 'permission'),
|
|
('GlobalTech HQ', 'hr_manager', 'create', 'user_roles', 'always', 'permission'),
|
|
('GlobalTech HQ', 'hr_manager', 'delete', 'user_roles', 'always', 'permission'),
|
|
('GlobalTech HQ', 'director', 'create', 'roles', 'always', 'permission'),
|
|
('GlobalTech HQ', 'director', 'read', 'roles', 'always', 'permission'),
|
|
('GlobalTech HQ', 'director', 'update', 'roles', 'always', 'permission'),
|
|
('GlobalTech HQ', 'director', 'delete', 'roles', 'always', 'permission')
|
|
) AS v(org_name, role_name, activity, view, context_name, modality)
|
|
JOIN morbac.orgs o ON o.name = v.org_name
|
|
JOIN morbac.roles r ON r.org_id = o.id AND r.name = v.role_name
|
|
JOIN morbac.contexts c ON c.name = v.context_name;
|
|
|
|
\echo ''
|
|
\echo '--- 3. Verify permissions ---'
|
|
|
|
SELECT morbac.t('Carol (manager) create rules',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'rules'
|
|
), TRUE);
|
|
|
|
SELECT morbac.t('Carol (manager) delete rules',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'delete', 'rules'
|
|
), TRUE);
|
|
|
|
SELECT morbac.t('Carol (manager) update rules [no rule]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'update', 'rules'
|
|
), FALSE);
|
|
|
|
SELECT morbac.t('Grace (hr_manager) create user_roles',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000007'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'user_roles'
|
|
), TRUE);
|
|
|
|
SELECT morbac.t('Dave (employee) create rules [no rule]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'rules'
|
|
), FALSE);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Section 4: Role hierarchy applies
|
|
-- ---------------------------------------------------------------------------
|
|
\echo ''
|
|
\echo '--- 4. Role hierarchy applies ---'
|
|
|
|
SELECT morbac.t('Bob (director, inherits manager) create rules',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000002'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'rules'
|
|
), TRUE);
|
|
|
|
SELECT morbac.t('Bob (director) create roles [own rule]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000002'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'roles'
|
|
), TRUE);
|
|
|
|
SELECT morbac.t('Alice (CEO) create roles [inherits director]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000001'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'roles'
|
|
), TRUE);
|
|
|
|
SELECT morbac.t('Eve (intern) create rules [nothing in hierarchy]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000005'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'rules'
|
|
), FALSE);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Section 5: Prohibition overrides permission
|
|
-- ---------------------------------------------------------------------------
|
|
\echo ''
|
|
\echo '--- 5. Prohibition overrides permission ---'
|
|
|
|
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
|
|
SELECT o.id, r.id, 'create', 'rules', c.id, 'prohibition'
|
|
FROM morbac.orgs o
|
|
JOIN morbac.roles r ON r.org_id = o.id AND r.name = 'manager'
|
|
JOIN morbac.contexts c ON c.name = 'always'
|
|
WHERE o.name = 'GlobalTech HQ';
|
|
|
|
SELECT morbac.t('Carol (manager, prohibited) create rules [prohibition wins]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'create', 'rules'
|
|
), FALSE);
|
|
|
|
SELECT morbac.t('Carol (prohibition only on create) delete rules [still allowed]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
'delete', 'rules'
|
|
), TRUE);
|
|
|
|
DELETE FROM morbac.rules
|
|
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
|
AND role_id = '20000000-0001-0000-0000-000000000003'
|
|
AND activity = 'create' AND view = 'rules'
|
|
AND modality = 'prohibition';
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Section 6: assign_role / revoke_role
|
|
-- ---------------------------------------------------------------------------
|
|
\echo ''
|
|
\echo '--- 6. assign_role / revoke_role ---'
|
|
|
|
-- As DB owner (bypasses RLS), assign Karl as intern to verify constraint logic
|
|
SELECT morbac.assign_role(
|
|
'30000000-0000-0000-0000-000000000011'::uuid, -- Karl
|
|
'20000000-0001-0000-0000-000000000005'::uuid, -- intern
|
|
'10000000-0000-0000-0000-000000000001'::uuid
|
|
);
|
|
|
|
SELECT morbac.t('Karl assigned intern role via assign_role()',
|
|
EXISTS(
|
|
SELECT 1 FROM morbac.user_roles
|
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
|
AND role_id = '20000000-0001-0000-0000-000000000005'
|
|
), TRUE);
|
|
|
|
SELECT morbac.revoke_role(
|
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
|
'20000000-0001-0000-0000-000000000005'::uuid,
|
|
'10000000-0000-0000-0000-000000000001'::uuid
|
|
);
|
|
|
|
SELECT morbac.t('Karl intern role revoked via revoke_role()',
|
|
NOT EXISTS(
|
|
SELECT 1 FROM morbac.user_roles
|
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
|
AND role_id = '20000000-0001-0000-0000-000000000005'
|
|
), TRUE);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Section 7: RLS on morbac tables
|
|
-- ---------------------------------------------------------------------------
|
|
\echo ''
|
|
\echo '--- 7. RLS on morbac tables ---'
|
|
|
|
-- Create a non-superuser role so RLS policies are enforced (superusers bypass RLS by default)
|
|
DROP ROLE IF EXISTS morbac_rls_tester;
|
|
CREATE ROLE morbac_rls_tester;
|
|
GRANT USAGE ON SCHEMA morbac TO morbac_rls_tester;
|
|
GRANT SELECT, INSERT, DELETE ON morbac.user_roles TO morbac_rls_tester;
|
|
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO morbac_rls_tester;
|
|
|
|
SET SESSION AUTHORIZATION morbac_rls_tester;
|
|
|
|
-- Grace (hr_manager) has create/delete on user_roles — RLS should allow
|
|
SET morbac.user_id = '30000000-0000-0000-0000-000000000007';
|
|
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
|
|
|
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
|
VALUES (
|
|
'30000000-0000-0000-0000-000000000011',
|
|
'20000000-0001-0000-0000-000000000005',
|
|
'10000000-0000-0000-0000-000000000001'
|
|
);
|
|
|
|
SELECT morbac.t('Grace (hr_manager) inserted user_role via RLS',
|
|
EXISTS(
|
|
SELECT 1 FROM morbac.user_roles
|
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
|
AND role_id = '20000000-0001-0000-0000-000000000005'
|
|
), TRUE);
|
|
|
|
DELETE FROM morbac.user_roles
|
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
|
AND role_id = '20000000-0001-0000-0000-000000000005'
|
|
AND org_id = '10000000-0000-0000-0000-000000000001';
|
|
|
|
-- Dave (employee) has no rules for user_roles — RLS should block
|
|
SET morbac.user_id = '30000000-0000-0000-0000-000000000004';
|
|
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
|
|
|
|
DO $$
|
|
BEGIN
|
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
|
VALUES (
|
|
'30000000-0000-0000-0000-000000000011',
|
|
'20000000-0001-0000-0000-000000000005',
|
|
'10000000-0000-0000-0000-000000000001'
|
|
);
|
|
RAISE NOTICE 'CHECK FAIL: Dave (employee) inserted user_role [should have been blocked by RLS]';
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RAISE NOTICE 'CHECK PASS: Dave (employee) blocked from inserting user_role by RLS';
|
|
END;
|
|
$$;
|
|
|
|
RESET SESSION AUTHORIZATION;
|
|
RESET morbac.user_id;
|
|
RESET morbac.org_id;
|
|
|
|
DROP OWNED BY morbac_rls_tester;
|
|
DROP ROLE IF EXISTS morbac_rls_tester;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Section 8: Rules are org-scoped
|
|
-- ---------------------------------------------------------------------------
|
|
\echo ''
|
|
\echo '--- 8. Rules are org-scoped ---'
|
|
|
|
SELECT morbac.t('Carol (GlobalTech manager) create Engineering rules [wrong org]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
|
'10000000-0000-0000-0000-000000000002'::uuid,
|
|
'create', 'rules'
|
|
), FALSE);
|
|
|
|
SELECT morbac.t('Alice (GlobalTech CEO) create Engineering roles [org-scoped]',
|
|
morbac.is_allowed(
|
|
'30000000-0000-0000-0000-000000000001'::uuid,
|
|
'10000000-0000-0000-0000-000000000002'::uuid,
|
|
'create', 'roles'
|
|
), FALSE);
|
|
|
|
\echo ''
|
|
\echo '=== System Access Tests Completed ==='
|