Decision Model
How Warden merges decisions from RBAC, ABAC, ReBAC, and PBAC evaluators.
Decision Types
Every evaluator returns one of three decisions:
| Decision | Meaning |
|---|---|
| Allow | The evaluator grants access |
| Deny | The evaluator explicitly denies access |
| NoOpinion | The evaluator has no matching rules |
Merging Algorithm
When multiple evaluators return decisions, Warden merges them with this priority:
1. If ANY evaluator returns explicit DENY → DENIED
2. If ANY evaluator returns ALLOW → ALLOWED
3. If all return NoOpinion → DENIED (default deny)This is a deny-overrides strategy: explicit denies always win.
Examples
Example 1: RBAC allows, no policies
| RBAC | ABAC/PBAC | ReBAC | Result |
|---|---|---|---|
| Allow | NoOpinion | NoOpinion | Allowed |
The user has the right role/permission. No policies or relation tuples apply.
Example 2: RBAC allows, ABAC denies
| RBAC | ABAC/PBAC | ReBAC | Result |
|---|---|---|---|
| Allow | Deny | NoOpinion | Denied |
The user has the right role, but an ABAC policy (e.g., "deny after business hours") blocks access.
Example 3: No RBAC match, ReBAC allows
| RBAC | ABAC/PBAC | ReBAC | Result |
|---|---|---|---|
| NoOpinion | NoOpinion | Allow | Allowed |
No role assignment exists, but a relation tuple grants access through the relationship graph.
Example 4: Nothing matches
| RBAC | ABAC/PBAC | ReBAC | Result |
|---|---|---|---|
| NoOpinion | NoOpinion | NoOpinion | Denied |
Default deny: if no evaluator grants access, the request is denied.
CheckResult
The Check() call returns a CheckResult with full provenance:
type CheckResult struct {
Allowed bool // Decision shorthand
Decision Decision // allow / deny_explicit / deny_no_roles / deny_no_perms /
// deny_condition / deny_relation / deny_default
Reason string // Human-readable explanation
MatchedBy []MatchInfo // Every rule that matched (RBAC role IDs, ABAC policy IDs, ReBAC paths)
Obligations []string // PBAC side-effect actions: audit-log, require-mfa, ...
EvalTimeNs int64 // Evaluation latency
}
type MatchInfo struct {
Source string // "rbac" | "abac" | "rebac"
RuleID string // typeid of the matched entity
Detail string // free-form, e.g. `policy "incident-freeze" (deny)`
}MatchedBy carries every contributing rule — useful for debugging "why did this Check return X?" and for audit logs that need to record specific policies, not just decisions.
Policy Priority
When multiple ABAC/PBAC policies match, they are evaluated by priority (lower number = higher priority). An explicit deny from any policy still short-circuits the merge — priority controls order, not override semantics.
// High priority deny (evaluated first)
&policy.Policy{
Name: "incident-freeze",
Priority: 1,
Effect: policy.EffectDeny,
IsActive: true,
Actions: []string{"deploy:*"},
}
// Lower priority allow (evaluated after the deny)
&policy.Policy{
Name: "default-deploy",
Priority: 100,
Effect: policy.EffectAllow,
IsActive: true,
Actions: []string{"deploy:*"},
}warden config 1
tenant t1
// High priority deny (evaluated first)
policy "incident-freeze" {
effect = deny
priority = 1
active = true
actions = ["deploy:*"]
}
// Lower priority allow (evaluated after the deny)
policy "default-deploy" {
effect = allow
priority = 100
active = true
actions = ["deploy:*"]
}Obligations and the merge
PBAC obligations are accumulated independently of the allow/deny merge:
- Every matched policy contributes its
ObligationstoCheckResult.Obligations. - Duplicates are removed (first-occurrence order preserved).
- Obligations from matched-but-overridden allows still surface — the engine records what would have fired even when an explicit deny wins.
So a successful read against a document with an audit-log-emitting policy plus a require-mfa-emitting policy returns Allowed=true, Obligations=["audit-log","require-mfa"]. The calling system reads both and acts on each (write to audit log, prompt for MFA).