feat(rules): add preemptive warning when creating conflicting rules

This commit is contained in:
2026-03-23 00:50:00 +01:00
parent 09082ea0d3
commit fca7e2053f
5 changed files with 256 additions and 5 deletions
+29
View File
@@ -498,6 +498,33 @@ SELECT morbac.check_cardinality_violation(admin_role_id, org_id);
-- Returns: error message if would violate constraint, NULL if valid
```
### Rule Conflict Detection
When inserting or updating a rule, pgmorbac automatically warns if the new rule conflicts with an existing one due to modality precedence. Conflicts are non-blocking (the insert succeeds) but a `WARNING` is emitted so you can catch unintentional policy contradictions.
A conflict is flagged when two rules share the same `(org, role, activity, view, context)` tuple and their modalities create a dead rule:
| New modality | Existing modality | Issue |
|---|---|---|
| `permission` | `prohibition` | permission will always be overridden |
| `obligation` | `prohibition` | obligation will always be voided |
| `recommendation` | `prohibition` or `obligation` | recommendation will always be voided |
| `prohibition` | any | the existing rule(s) become dead or voided |
Hierarchy-based overlaps (e.g. a permission on `manage` and a prohibition on `write`, where `write` is a sub-activity of `manage`) are intentional design and not flagged.
You can also call `detect_rule_conflicts()` explicitly before inserting:
```sql
-- Check for conflicts before inserting
SELECT * FROM morbac.detect_rule_conflicts(
org_id, role_id, 'read', 'documents',
(SELECT id FROM morbac.contexts WHERE name = 'always'),
'permission'
);
-- Returns empty if no conflict, or rows describing each conflicting rule
```
### Derived Roles
Roles computed dynamically from application data.
@@ -698,6 +725,8 @@ SELECT * FROM morbac.get_org_scope(org_uuid, 'subtree', 2);
**`check_cardinality_violation(role_id, org_id)`**: Returns error message if constraint violated, NULL if valid.
**`detect_rule_conflicts(org_id, role_id, activity, view, context_id, modality, exclude_id?)`**: Returns rows describing rules that directly conflict with the given tuple due to modality precedence. `exclude_id` is used on UPDATE to skip the rule being modified. Also fires automatically as a non-blocking trigger warning on every INSERT or UPDATE to `morbac.rules`.
### Administration Functions
**`is_admin_allowed(user_id, org_id, capability)`**: Check administrative permissions. Capabilities: `manage_policies`, `manage_roles`, `manage_users`.