Conflict Detection
Problem
As a policy grows, rules can inadvertently shadow each other. You want to detect unreachable rules at startup or in a CI step rather than discovering them as silent authorization bugs.
Solution
Use ward.detectConflicts() to find duplicate or shadowed predicate-free rules. The result is lazily computed and cached, so calling it repeatedly is cheap after the first call.
import { createWard } from '@vielzeug/ward';
const ward = createWard([
{ role: 'editor', resource: 'posts', action: 'update', effect: 'allow' },
// Duplicate: same (role, resource, action) — this rule can never fire
{ role: 'editor', resource: 'posts', action: 'update', effect: 'deny' },
// Shadowed: the wildcard rule covers everything the next rule could match
{ role: '*', resource: 'posts', action: '*', effect: 'allow', priority: 10 },
{ role: 'viewer', resource: 'posts', action: 'read', effect: 'deny' },
]);
const conflicts = ward.detectConflicts();
// [
// { kind: 'duplicate', ruleIndex: 1, shadowedByIndex: 0, rule: …, shadowedBy: … },
// { kind: 'shadowed', ruleIndex: 3, shadowedByIndex: 2, rule: …, shadowedBy: … },
// ]
conflicts.forEach(({ kind, ruleIndex, shadowedByIndex }) => {
console.warn(`[ward] ${kind}: Rule[${ruleIndex}] unreachable (shadowed by Rule[${shadowedByIndex}])`);
});Detect at creation time with onConflict
For early-fail detection, pass onConflict (or set strict: true) to createWard:
const ward = createWard(
[
{ role: 'editor', resource: 'posts', action: 'update', effect: 'allow' },
{ role: 'editor', resource: 'posts', action: 'update', effect: 'deny' },
],
{
// Called synchronously for each conflict at creation time:
onConflict: ({ kind, ruleIndex, shadowedByIndex }) => {
console.error(`Policy conflict: Rule[${ruleIndex}] ${kind} by Rule[${shadowedByIndex}]`);
},
},
);To throw on the first conflict (e.g. in a test environment):
const ward = createWard(rules, { strict: true }); // throws if any conflict existsCap analysis cost for large policies
detectConflicts is O(n²). For large auto-generated policies, cap the number of conflicts returned:
const ward = createWard(rules, { maxConflicts: 50 });
const conflicts = ward.detectConflicts(); // at most 50 entriesWhat is excluded from conflict detection?
Rules with a when predicate are excluded from both 'duplicate' and 'shadowed' detection. Their applicability can only be determined at runtime based on contextual data, so they cannot be statically flagged as unreachable.
const ward = createWard([
// NOT flagged — the predicate may fail and cause the rule to not match
{ role: 'editor', resource: 'posts', action: 'update', effect: 'allow', when: owns('authorId') },
{ role: 'editor', resource: 'posts', action: 'update', effect: 'deny', when: owns('authorId') },
]);
ward.detectConflicts(); // []Pitfalls
detectConflictsonly checks static shadowing — it does not modelpriorityinteractions beyond identifying which rule has higher priority.'shadowed'requires the shadowing rule to have nowhenpredicate. A predicate-bearing rule with higher priority may still fail at runtime, so it is not treated as a static shadow.- For very large dynamically generated policies (thousands of rules), prefer
maxConflictsto avoid O(n²) cost on every startup.