feat(rules): add priority field
This commit is contained in:
+53
-63
@@ -6,11 +6,14 @@
|
|||||||
-- Semantics (from Multi-OrBAC paper):
|
-- Semantics (from Multi-OrBAC paper):
|
||||||
-- - Access is allowed if and only if:
|
-- - Access is allowed if and only if:
|
||||||
-- 1. At least one applicable permission exists
|
-- 1. At least one applicable permission exists
|
||||||
-- 2. AND no applicable prohibition exists
|
-- 2. AND no applicable prohibition exists (or permission has strictly higher priority)
|
||||||
-- - Prohibitions have precedence over permissions
|
|
||||||
-- - Contexts must be evaluated
|
|
||||||
-- - Default deny (no permission = deny)
|
-- - Default deny (no permission = deny)
|
||||||
-- - Obligations and recommendations do NOT affect authorization
|
-- - Obligations and recommendations do NOT affect authorization
|
||||||
|
--
|
||||||
|
-- Priority resolution:
|
||||||
|
-- - Each rule has an optional integer priority (NULL = 0, lowest)
|
||||||
|
-- - When both a prohibition and a permission apply, the higher-priority rule wins
|
||||||
|
-- - Tie goes to prohibition (modality precedence from the paper)
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache(
|
CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache(
|
||||||
p_user_id UUID,
|
p_user_id UUID,
|
||||||
@@ -24,111 +27,98 @@ STABLE
|
|||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_rule RECORD;
|
v_rule RECORD;
|
||||||
|
v_max_prohibition_priority INTEGER := NULL;
|
||||||
|
v_max_permission_priority INTEGER := NULL;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- STEP 1: Check for prohibitions first (prohibition precedence)
|
-- STEP 1: Local prohibitions — find the highest-priority applicable one
|
||||||
-- If any applicable prohibition exists, deny immediately
|
|
||||||
-- Checks:
|
|
||||||
-- - Direct rules for (activity, view) combinations
|
|
||||||
-- - Activity hierarchy (senior activities imply junior)
|
|
||||||
-- - View hierarchy (senior views imply junior)
|
|
||||||
-- - Comprehensive roles (direct, delegated, derived, inherited)
|
|
||||||
-- - Temporal validity (if rule has time constraints)
|
|
||||||
|
|
||||||
FOR v_rule IN
|
FOR v_rule IN
|
||||||
SELECT r.context_id
|
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
||||||
FROM morbac.rules r
|
FROM morbac.rules r
|
||||||
WHERE r.org_id = p_org_id
|
WHERE r.org_id = p_org_id
|
||||||
AND r.modality = 'prohibition'
|
AND r.modality = 'prohibition'
|
||||||
-- Match activity or any senior activity in hierarchy
|
AND r.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||||
AND r.activity IN (
|
AND r.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||||
SELECT activity FROM morbac.get_effective_activities(p_activity)
|
AND r.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id))
|
||||||
)
|
|
||||||
-- Match view or any senior view in hierarchy
|
|
||||||
AND r.view IN (
|
|
||||||
SELECT view FROM morbac.get_effective_views(p_view)
|
|
||||||
)
|
|
||||||
-- Match comprehensive roles (direct, delegated, derived, inherited)
|
|
||||||
AND r.role_id IN (
|
|
||||||
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
|
||||||
)
|
|
||||||
-- Check temporal validity
|
|
||||||
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||||
|
ORDER BY COALESCE(r.priority, 0) DESC
|
||||||
LOOP
|
LOOP
|
||||||
-- Evaluate context
|
|
||||||
IF morbac.eval_context(v_rule.context_id) THEN
|
IF morbac.eval_context(v_rule.context_id) THEN
|
||||||
-- Prohibition found - immediate deny (prohibition precedence)
|
v_max_prohibition_priority := v_rule.prio;
|
||||||
RETURN FALSE;
|
EXIT; -- Highest-priority prohibition found; lower ones can't change the outcome
|
||||||
END IF;
|
END IF;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
-- STEP 2: Check cross-organizational prohibitions
|
-- STEP 2: Cross-org prohibitions — update max if a higher priority is found
|
||||||
FOR v_rule IN
|
FOR v_rule IN
|
||||||
SELECT cr.context_id
|
SELECT cr.context_id, COALESCE(cr.priority, 0) AS prio
|
||||||
FROM morbac.cross_org_rules cr
|
FROM morbac.cross_org_rules cr
|
||||||
WHERE cr.target_org_id = p_org_id
|
WHERE cr.target_org_id = p_org_id
|
||||||
AND cr.modality = 'prohibition'
|
AND cr.modality = 'prohibition'
|
||||||
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||||
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||||
-- User has role in source org
|
AND cr.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id))
|
||||||
AND cr.role_id IN (
|
|
||||||
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id)
|
|
||||||
)
|
|
||||||
-- Check temporal validity
|
|
||||||
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
||||||
|
ORDER BY COALESCE(cr.priority, 0) DESC
|
||||||
LOOP
|
LOOP
|
||||||
IF morbac.eval_context(v_rule.context_id) THEN
|
IF morbac.eval_context(v_rule.context_id) THEN
|
||||||
RETURN FALSE;
|
IF v_max_prohibition_priority IS NULL OR v_rule.prio > v_max_prohibition_priority THEN
|
||||||
|
v_max_prohibition_priority := v_rule.prio;
|
||||||
|
END IF;
|
||||||
|
EXIT;
|
||||||
END IF;
|
END IF;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
-- STEP 3: No prohibitions found, check for permissions
|
-- STEP 3: Local permissions — find the highest-priority applicable one
|
||||||
FOR v_rule IN
|
FOR v_rule IN
|
||||||
SELECT r.context_id
|
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
||||||
FROM morbac.rules r
|
FROM morbac.rules r
|
||||||
WHERE r.org_id = p_org_id
|
WHERE r.org_id = p_org_id
|
||||||
AND r.modality = 'permission'
|
AND r.modality = 'permission'
|
||||||
-- Match activity or any senior activity in hierarchy
|
AND r.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||||
AND r.activity IN (
|
AND r.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||||
SELECT activity FROM morbac.get_effective_activities(p_activity)
|
AND r.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id))
|
||||||
)
|
|
||||||
-- Match view or any senior view in hierarchy
|
|
||||||
AND r.view IN (
|
|
||||||
SELECT view FROM morbac.get_effective_views(p_view)
|
|
||||||
)
|
|
||||||
-- Match comprehensive roles
|
|
||||||
AND r.role_id IN (
|
|
||||||
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
|
||||||
)
|
|
||||||
-- Check temporal validity
|
|
||||||
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||||
|
ORDER BY COALESCE(r.priority, 0) DESC
|
||||||
LOOP
|
LOOP
|
||||||
-- Evaluate context
|
|
||||||
IF morbac.eval_context(v_rule.context_id) THEN
|
IF morbac.eval_context(v_rule.context_id) THEN
|
||||||
-- Permission found and no prohibition - allow
|
v_max_permission_priority := v_rule.prio;
|
||||||
RETURN TRUE;
|
EXIT;
|
||||||
END IF;
|
END IF;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
-- STEP 4: Check cross-organizational permissions
|
-- STEP 4: Cross-org permissions — update max if higher found
|
||||||
FOR v_rule IN
|
FOR v_rule IN
|
||||||
SELECT cr.context_id
|
SELECT cr.context_id, COALESCE(cr.priority, 0) AS prio
|
||||||
FROM morbac.cross_org_rules cr
|
FROM morbac.cross_org_rules cr
|
||||||
WHERE cr.target_org_id = p_org_id
|
WHERE cr.target_org_id = p_org_id
|
||||||
AND cr.modality = 'permission'
|
AND cr.modality = 'permission'
|
||||||
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||||
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||||
AND cr.role_id IN (
|
AND cr.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id))
|
||||||
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id)
|
|
||||||
)
|
|
||||||
-- Check temporal validity
|
|
||||||
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
||||||
|
ORDER BY COALESCE(cr.priority, 0) DESC
|
||||||
LOOP
|
LOOP
|
||||||
IF morbac.eval_context(v_rule.context_id) THEN
|
IF morbac.eval_context(v_rule.context_id) THEN
|
||||||
RETURN TRUE;
|
IF v_max_permission_priority IS NULL OR v_rule.prio > v_max_permission_priority THEN
|
||||||
|
v_max_permission_priority := v_rule.prio;
|
||||||
|
END IF;
|
||||||
|
EXIT;
|
||||||
END IF;
|
END IF;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
-- No permission found - deny (default deny)
|
-- STEP 5: Priority resolution
|
||||||
|
-- No prohibition at all: allow if any permission was found
|
||||||
|
IF v_max_prohibition_priority IS NULL THEN
|
||||||
|
RETURN v_max_permission_priority IS NOT NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Prohibition exists: a permission with strictly higher priority overrides it
|
||||||
|
IF v_max_permission_priority IS NOT NULL
|
||||||
|
AND v_max_permission_priority > v_max_prohibition_priority THEN
|
||||||
|
RETURN TRUE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Prohibition wins (no permission, equal priority, or lower-priority permission)
|
||||||
RETURN FALSE;
|
RETURN FALSE;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ CREATE TABLE morbac.cross_org_rules (
|
|||||||
view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,
|
view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,
|
||||||
context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE,
|
context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE,
|
||||||
modality morbac.modality NOT NULL,
|
modality morbac.modality NOT NULL,
|
||||||
|
priority INTEGER,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
valid_from TIMESTAMPTZ,
|
valid_from TIMESTAMPTZ,
|
||||||
valid_until TIMESTAMPTZ,
|
valid_until TIMESTAMPTZ,
|
||||||
|
|||||||
+4
-2
@@ -45,7 +45,7 @@ BEGIN
|
|||||||
)
|
)
|
||||||
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||||
AND morbac.eval_context(r.context_id)
|
AND morbac.eval_context(r.context_id)
|
||||||
-- Prohibition voids obligation: exclude if an applicable prohibition exists
|
-- Prohibition voids obligation unless the obligation has strictly higher priority
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM morbac.rules p
|
FROM morbac.rules p
|
||||||
@@ -58,6 +58,7 @@ BEGIN
|
|||||||
)
|
)
|
||||||
AND morbac.is_rule_valid(p.valid_from, p.valid_until)
|
AND morbac.is_rule_valid(p.valid_from, p.valid_until)
|
||||||
AND morbac.eval_context(p.context_id)
|
AND morbac.eval_context(p.context_id)
|
||||||
|
AND COALESCE(p.priority, 0) >= COALESCE(r.priority, 0)
|
||||||
)
|
)
|
||||||
ORDER BY r.created_at;
|
ORDER BY r.created_at;
|
||||||
END;
|
END;
|
||||||
@@ -102,7 +103,7 @@ BEGIN
|
|||||||
)
|
)
|
||||||
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||||
AND morbac.eval_context(r.context_id)
|
AND morbac.eval_context(r.context_id)
|
||||||
-- Prohibition or obligation voids recommendation
|
-- Prohibition or obligation voids recommendation unless recommendation has strictly higher priority
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM morbac.rules p
|
FROM morbac.rules p
|
||||||
@@ -115,6 +116,7 @@ BEGIN
|
|||||||
)
|
)
|
||||||
AND morbac.is_rule_valid(p.valid_from, p.valid_until)
|
AND morbac.is_rule_valid(p.valid_from, p.valid_until)
|
||||||
AND morbac.eval_context(p.context_id)
|
AND morbac.eval_context(p.context_id)
|
||||||
|
AND COALESCE(p.priority, 0) >= COALESCE(r.priority, 0)
|
||||||
)
|
)
|
||||||
ORDER BY r.created_at;
|
ORDER BY r.created_at;
|
||||||
END;
|
END;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ CREATE TABLE morbac.rules (
|
|||||||
view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,
|
view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,
|
||||||
context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE,
|
context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE,
|
||||||
modality morbac.modality NOT NULL,
|
modality morbac.modality NOT NULL,
|
||||||
|
priority INTEGER,
|
||||||
valid_from TIMESTAMPTZ,
|
valid_from TIMESTAMPTZ,
|
||||||
valid_until TIMESTAMPTZ,
|
valid_until TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
@@ -60,6 +61,7 @@ COMMENT ON COLUMN morbac.rules.activity IS 'Activity (abstract action)';
|
|||||||
COMMENT ON COLUMN morbac.rules.view IS 'View (abstract object category)';
|
COMMENT ON COLUMN morbac.rules.view IS 'View (abstract object category)';
|
||||||
COMMENT ON COLUMN morbac.rules.context_id IS 'Context condition';
|
COMMENT ON COLUMN morbac.rules.context_id IS 'Context condition';
|
||||||
COMMENT ON COLUMN morbac.rules.modality IS 'Deontic modality: permission, prohibition, obligation, recommendation';
|
COMMENT ON COLUMN morbac.rules.modality IS 'Deontic modality: permission, prohibition, obligation, recommendation';
|
||||||
|
COMMENT ON COLUMN morbac.rules.priority IS 'Optional: Rule priority (higher wins). NULL = 0. A permission with higher priority than a prohibition overrides it.';
|
||||||
COMMENT ON COLUMN morbac.rules.valid_from IS 'Optional: Rule valid from this timestamp';
|
COMMENT ON COLUMN morbac.rules.valid_from IS 'Optional: Rule valid from this timestamp';
|
||||||
COMMENT ON COLUMN morbac.rules.valid_until IS 'Optional: Rule valid until this timestamp';
|
COMMENT ON COLUMN morbac.rules.valid_until IS 'Optional: Rule valid until this timestamp';
|
||||||
|
|
||||||
|
|||||||
@@ -414,5 +414,81 @@ SELECT morbac.t('Frank reads financial_data (prohibited): cached = nocache',
|
|||||||
'read', 'financial_data'
|
'read', 'financial_data'
|
||||||
), TRUE);
|
), TRUE);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section N: Priority-based conflict resolution
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- Priority: permission overrides prohibition when strictly higher ---'
|
||||||
|
|
||||||
|
-- Dave (employee) already has permission to read documents.
|
||||||
|
-- Add a prohibition with priority=5 and a permission with priority=10 on 'read contracts'.
|
||||||
|
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, priority)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000001',
|
||||||
|
'10000000-0000-0000-0000-000000000001',
|
||||||
|
'20000000-0001-0000-0000-000000000004', -- employee
|
||||||
|
'read', 'contracts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition', 5
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality, priority)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000002',
|
||||||
|
'10000000-0000-0000-0000-000000000001',
|
||||||
|
'20000000-0001-0000-0000-000000000004', -- employee
|
||||||
|
'read', 'contracts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission', 10
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Permission (priority=10) beats prohibition (priority=5) -> ALLOW
|
||||||
|
SELECT morbac.t('Dave reads contracts: permission priority=10 overrides prohibition priority=5 [allowed]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- Flip priorities: prohibition=10, permission=5 -> prohibition wins
|
||||||
|
UPDATE morbac.rules SET priority = 10 WHERE id = 'e0000000-0000-0000-0000-000000000001';
|
||||||
|
UPDATE morbac.rules SET priority = 5 WHERE id = 'e0000000-0000-0000-0000-000000000002';
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave reads contracts: prohibition priority=10 beats permission priority=5 [denied]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
-- Equal priorities: prohibition wins (modality tiebreaker)
|
||||||
|
UPDATE morbac.rules SET priority = 5 WHERE id = 'e0000000-0000-0000-0000-000000000001';
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave reads contracts: equal priority — prohibition wins by modality precedence [denied]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
-- No priority on either (NULL = 0): prohibition still wins
|
||||||
|
UPDATE morbac.rules SET priority = NULL WHERE id IN (
|
||||||
|
'e0000000-0000-0000-0000-000000000001',
|
||||||
|
'e0000000-0000-0000-0000-000000000002'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave reads contracts: no priority set — prohibition wins by default [denied]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
-- Clean up
|
||||||
|
DELETE FROM morbac.rules WHERE id IN (
|
||||||
|
'e0000000-0000-0000-0000-000000000001',
|
||||||
|
'e0000000-0000-0000-0000-000000000002'
|
||||||
|
);
|
||||||
|
|
||||||
\echo ''
|
\echo ''
|
||||||
\echo '=== Authorization Decision Tests Completed ==='
|
\echo '=== Authorization Decision Tests Completed ==='
|
||||||
|
|||||||
Reference in New Issue
Block a user