Warden

Decision Model

How Warden merges decisions from RBAC, ABAC, ReBAC, and PBAC evaluators.

Decision Types

Every evaluator returns one of three decisions:

DecisionMeaning
AllowThe evaluator grants access
DenyThe evaluator explicitly denies access
NoOpinionThe 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

RBACABAC/PBACReBACResult
AllowNoOpinionNoOpinionAllowed

The user has the right role/permission. No policies or relation tuples apply.

Example 2: RBAC allows, ABAC denies

RBACABAC/PBACReBACResult
AllowDenyNoOpinionDenied

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

RBACABAC/PBACReBACResult
NoOpinionNoOpinionAllowAllowed

No role assignment exists, but a relation tuple grants access through the relationship graph.

Example 4: Nothing matches

RBACABAC/PBACReBACResult
NoOpinionNoOpinionNoOpinionDenied

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 Obligations to CheckResult.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).

On this page