feat(info): cleanup obligations and recommendations features
This commit is contained in:
@@ -219,6 +219,17 @@ flowchart LR
|
|||||||
D -->|No| F[DENY]
|
D -->|No| F[DENY]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Obligations and Recommendations**
|
||||||
|
|
||||||
|
Obligations and recommendations do not affect access control, they are informational signals your application reads and acts on.
|
||||||
|
|
||||||
|
- **Obligation**: the user *must* perform this action. Use it to model required tasks, workflow steps, or compliance actions (e.g. a doctor must file a discharge report, an employee must review the security policy weekly).
|
||||||
|
- **Recommendation**: the user *should* consider this action. Use it for suggested next steps, nudges, or guidance (e.g. a manager is recommended to review team performance reports monthly).
|
||||||
|
|
||||||
|
Query them via `pending_obligations()` and `pending_recommendations()` to drive task lists, alerts, or workflow gates in your application.
|
||||||
|
|
||||||
|
**Conflict resolution**: a prohibition voids any applicable obligation. A prohibition or obligation voids any applicable recommendation. This mirrors the deontic priority order from the Multi-OrBAC paper: prohibition > obligation > recommendation.
|
||||||
|
|
||||||
**Example: Employee Document Access**
|
**Example: Employee Document Access**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
|||||||
+49
-12
@@ -3,8 +3,14 @@
|
|||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
-- Obligations and recommendations do NOT affect authorization
|
-- Obligations and recommendations do NOT affect authorization
|
||||||
-- They are queryable for informational purposes
|
-- They are queryable for informational purposes
|
||||||
|
--
|
||||||
|
-- Conflict resolution (from Multi-OrBAC paper):
|
||||||
|
-- - A prohibition voids any applicable obligation for the same (activity, view)
|
||||||
|
-- - A prohibition or obligation voids any applicable recommendation for the same (activity, view)
|
||||||
|
-- - Both functions use comprehensive roles (direct, delegated, derived, inherited)
|
||||||
|
|
||||||
-- Pending obligations for a user in an organization
|
-- Pending obligations for a user in an organization
|
||||||
|
-- Returns only obligations not voided by an applicable prohibition
|
||||||
CREATE OR REPLACE FUNCTION morbac.pending_obligations(
|
CREATE OR REPLACE FUNCTION morbac.pending_obligations(
|
||||||
p_user_id UUID,
|
p_user_id UUID,
|
||||||
p_org_id UUID
|
p_org_id UUID
|
||||||
@@ -30,22 +36,38 @@ BEGIN
|
|||||||
c.name,
|
c.name,
|
||||||
r.created_at
|
r.created_at
|
||||||
FROM morbac.rules r
|
FROM morbac.rules r
|
||||||
INNER JOIN morbac.user_roles ur ON ur.role_id = r.role_id
|
|
||||||
INNER JOIN morbac.roles ro ON ro.id = r.role_id
|
INNER JOIN morbac.roles ro ON ro.id = r.role_id
|
||||||
INNER JOIN morbac.contexts c ON c.id = r.context_id
|
INNER JOIN morbac.contexts c ON c.id = r.context_id
|
||||||
WHERE ur.user_id = p_user_id
|
WHERE r.org_id = p_org_id
|
||||||
AND r.org_id = p_org_id
|
|
||||||
AND ur.org_id = p_org_id
|
|
||||||
AND r.modality = 'obligation'
|
AND r.modality = 'obligation'
|
||||||
AND morbac.eval_context(r.context_id) = TRUE
|
AND r.role_id IN (
|
||||||
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
||||||
|
)
|
||||||
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||||
|
AND morbac.eval_context(r.context_id)
|
||||||
|
-- Prohibition voids obligation: exclude if an applicable prohibition exists
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM morbac.rules p
|
||||||
|
WHERE p.org_id = p_org_id
|
||||||
|
AND p.modality = 'prohibition'
|
||||||
|
AND p.activity IN (SELECT activity FROM morbac.get_effective_activities(r.activity))
|
||||||
|
AND p.view IN (SELECT view FROM morbac.get_effective_views(r.view))
|
||||||
|
AND p.role_id IN (
|
||||||
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
||||||
|
)
|
||||||
|
AND morbac.is_rule_valid(p.valid_from, p.valid_until)
|
||||||
|
AND morbac.eval_context(p.context_id)
|
||||||
|
)
|
||||||
ORDER BY r.created_at;
|
ORDER BY r.created_at;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
COMMENT ON FUNCTION morbac.pending_obligations(UUID, UUID) IS
|
COMMENT ON FUNCTION morbac.pending_obligations(UUID, UUID) IS
|
||||||
'Returns pending obligations for a user in an organization (informational only)';
|
'Returns pending obligations for a user in an organization (informational only). Prohibitions void applicable obligations per Multi-OrBAC conflict resolution.';
|
||||||
|
|
||||||
-- Recommendations for a user in an organization
|
-- Recommendations for a user in an organization
|
||||||
|
-- Returns only recommendations not voided by an applicable prohibition or obligation
|
||||||
CREATE OR REPLACE FUNCTION morbac.pending_recommendations(
|
CREATE OR REPLACE FUNCTION morbac.pending_recommendations(
|
||||||
p_user_id UUID,
|
p_user_id UUID,
|
||||||
p_org_id UUID
|
p_org_id UUID
|
||||||
@@ -71,20 +93,35 @@ BEGIN
|
|||||||
c.name,
|
c.name,
|
||||||
r.created_at
|
r.created_at
|
||||||
FROM morbac.rules r
|
FROM morbac.rules r
|
||||||
INNER JOIN morbac.user_roles ur ON ur.role_id = r.role_id
|
|
||||||
INNER JOIN morbac.roles ro ON ro.id = r.role_id
|
INNER JOIN morbac.roles ro ON ro.id = r.role_id
|
||||||
INNER JOIN morbac.contexts c ON c.id = r.context_id
|
INNER JOIN morbac.contexts c ON c.id = r.context_id
|
||||||
WHERE ur.user_id = p_user_id
|
WHERE r.org_id = p_org_id
|
||||||
AND r.org_id = p_org_id
|
|
||||||
AND ur.org_id = p_org_id
|
|
||||||
AND r.modality = 'recommendation'
|
AND r.modality = 'recommendation'
|
||||||
AND morbac.eval_context(r.context_id) = TRUE
|
AND r.role_id IN (
|
||||||
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
||||||
|
)
|
||||||
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||||
|
AND morbac.eval_context(r.context_id)
|
||||||
|
-- Prohibition or obligation voids recommendation
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM morbac.rules p
|
||||||
|
WHERE p.org_id = p_org_id
|
||||||
|
AND p.modality IN ('prohibition', 'obligation')
|
||||||
|
AND p.activity IN (SELECT activity FROM morbac.get_effective_activities(r.activity))
|
||||||
|
AND p.view IN (SELECT view FROM morbac.get_effective_views(r.view))
|
||||||
|
AND p.role_id IN (
|
||||||
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
||||||
|
)
|
||||||
|
AND morbac.is_rule_valid(p.valid_from, p.valid_until)
|
||||||
|
AND morbac.eval_context(p.context_id)
|
||||||
|
)
|
||||||
ORDER BY r.created_at;
|
ORDER BY r.created_at;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
COMMENT ON FUNCTION morbac.pending_recommendations(UUID, UUID) IS
|
COMMENT ON FUNCTION morbac.pending_recommendations(UUID, UUID) IS
|
||||||
'Returns recommendations for a user in an organization (informational only)';
|
'Returns recommendations for a user in an organization (informational only). Prohibitions and obligations void applicable recommendations per Multi-OrBAC conflict resolution.';
|
||||||
|
|
||||||
-- Get all roles for a user in an organization
|
-- Get all roles for a user in an organization
|
||||||
CREATE OR REPLACE FUNCTION morbac.user_roles_in_org(
|
CREATE OR REPLACE FUNCTION morbac.user_roles_in_org(
|
||||||
|
|||||||
+5
-1
@@ -267,7 +267,11 @@ INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, contex
|
|||||||
('GlobalTech HQ', 'compliance_officer', 'read', 'financial_data', 'permission', 'always'),
|
('GlobalTech HQ', 'compliance_officer', 'read', 'financial_data', 'permission', 'always'),
|
||||||
|
|
||||||
-- Recommendation: staff should review reports end of quarter
|
-- Recommendation: staff should review reports end of quarter
|
||||||
('GlobalTech HQ', 'employee', 'read', 'reports', 'recommendation','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
|
-- Engineering Dept policies
|
||||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||||
|
|||||||
+74
-1
@@ -6,6 +6,7 @@
|
|||||||
-- 1. pending_obligations — returns obligation rules for a user
|
-- 1. pending_obligations — returns obligation rules for a user
|
||||||
-- 2. pending_recommendations — returns recommendation rules for a user
|
-- 2. pending_recommendations — returns recommendation rules for a user
|
||||||
-- 3. Obligations/recommendations do NOT affect is_allowed()
|
-- 3. Obligations/recommendations do NOT affect is_allowed()
|
||||||
|
-- 3b. Conflict resolution: prohibition voids obligation; prohibition/obligation voids recommendation
|
||||||
-- 4. user_has_role — checks if user holds a named role
|
-- 4. user_has_role — checks if user holds a named role
|
||||||
-- 5. user_roles_in_org — lists all roles for user in org
|
-- 5. user_roles_in_org — lists all roles for user in org
|
||||||
-- 6. eval_context — evaluates context predicates directly
|
-- 6. eval_context — evaluates context predicates directly
|
||||||
@@ -61,7 +62,7 @@ SELECT morbac.t_eq('Karl (no role) has 0 pending obligations',
|
|||||||
\echo ''
|
\echo ''
|
||||||
\echo '--- 2. pending_recommendations ---'
|
\echo '--- 2. pending_recommendations ---'
|
||||||
|
|
||||||
-- Dave (employee) has recommendation: 'read reports' during end_of_quarter (evaluates TRUE)
|
-- Dave (employee) has recommendation for 'read public_data' (no conflicting obligation/prohibition)
|
||||||
SELECT morbac.t('Dave (employee) has at least 1 pending recommendation',
|
SELECT morbac.t('Dave (employee) has at least 1 pending recommendation',
|
||||||
(SELECT COUNT(*) FROM morbac.pending_recommendations(
|
(SELECT COUNT(*) FROM morbac.pending_recommendations(
|
||||||
'30000000-0000-0000-0000-000000000004'::uuid,
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
@@ -69,6 +70,26 @@ SELECT morbac.t('Dave (employee) has at least 1 pending recommendation',
|
|||||||
)) >= 1,
|
)) >= 1,
|
||||||
TRUE);
|
TRUE);
|
||||||
|
|
||||||
|
-- 'read public_data' recommendation appears (no obligation or prohibition conflicts)
|
||||||
|
SELECT morbac.t('Dave has pending recommendation: read public_data',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.pending_recommendations(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid
|
||||||
|
)
|
||||||
|
WHERE activity = 'read' AND view = 'public_data'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- 'read reports' recommendation is voided by the existing obligation (conflict resolution)
|
||||||
|
SELECT morbac.t('Dave: read reports recommendation is voided by obligation',
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1 FROM morbac.pending_recommendations(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid
|
||||||
|
)
|
||||||
|
WHERE activity = 'read' AND view = 'reports'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
-- Eve (intern) has no recommendations
|
-- Eve (intern) has no recommendations
|
||||||
SELECT morbac.t_eq('Eve (intern) has 0 pending recommendations',
|
SELECT morbac.t_eq('Eve (intern) has 0 pending recommendations',
|
||||||
(SELECT COUNT(*) FROM morbac.pending_recommendations(
|
(SELECT COUNT(*) FROM morbac.pending_recommendations(
|
||||||
@@ -119,6 +140,58 @@ SELECT morbac.t('Dave has recommendation for read audit_logs but no permission [
|
|||||||
'read', 'audit_logs'
|
'read', 'audit_logs'
|
||||||
), FALSE);
|
), FALSE);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 3b: Conflict resolution — prohibition voids obligation/recommendation
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 3b. Conflict resolution: prohibition voids obligation and recommendation ---'
|
||||||
|
|
||||||
|
-- Add a prohibition for employee on read contracts (Dave already has an obligation for this)
|
||||||
|
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'),
|
||||||
|
'prohibition'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Prohibition voids the obligation: read contracts obligation no longer pending
|
||||||
|
SELECT morbac.t('Dave: read contracts obligation is voided by prohibition',
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1 FROM morbac.pending_obligations(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid
|
||||||
|
)
|
||||||
|
WHERE activity = 'read' AND view = 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- Prohibition also voids the recommendation for read audit_logs
|
||||||
|
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'),
|
||||||
|
'prohibition'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave: read audit_logs recommendation is voided by prohibition',
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1 FROM morbac.pending_recommendations(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid
|
||||||
|
)
|
||||||
|
WHERE activity = 'read' AND view = 'audit_logs'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- Clean up: remove the temporary prohibitions added for this test
|
||||||
|
DELETE FROM morbac.rules
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||||
|
AND role_id = '20000000-0001-0000-0000-000000000004'
|
||||||
|
AND modality = 'prohibition'
|
||||||
|
AND view IN ('contracts', 'audit_logs');
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- Section 4: user_has_role
|
-- Section 4: user_has_role
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user