Warden
1.x
Docs/Warden/Authorization Models
Open

Reading9 min
Updated31 Jul 2026
Sourcev1/concepts/authorization-models.mdx

Warden supports four authorization models that can be used independently or combined in a single Check() call:

  • RBAC — Role-Based Access Control (roles + permissions + assignments)

  • ABAC — Attribute-Based Access Control (policies with conditional matchers)

  • ReBAC — Relationship-Based Access Control (Zanzibar-style relation tuples)

  • PBAC — Policy-Based Access Control (ABAC plus time-bound windows and obligations)

Every config example on this page comes in two flavours: Go (call the store directly) and DSL (write .warden source and apply via warden apply or dsl.ApplyFS). They produce identical state.

RBAC — Role-Based Access Control01

RBAC is the most common authorization model. Users are assigned roles, and roles have permissions.

User → Role → Permission → (Resource, Action)

Entities:

  • Role — A named group of permissions (e.g., "admin", "editor", "viewer")

  • Permission — A (resource, action) pair (e.g., "document:read", "user:delete")

  • Assignment — Links a subject to a role, optionally scoped to a resource

When to use: When you have well-defined roles with predictable permission sets. Most applications start here.

go
// Create role with permissions. IDs are auto-assigned by the store
// on Create; the in-place mutation means role.ID is populated when
// the call returns, ready for downstream uses.
r := &role.Role{Name: "Editor", Slug: "editor"}
p := &permission.Permission{Name: "document:write", Resource: "document", Action: "write"}
store.CreateRole(ctx, r)
store.CreatePermission(ctx, p)
store.AttachPermission(ctx, r.ID, permission.Ref{Name: "document:write"})

// Assign role to user (assignments are runtime-only; not in the DSL).
store.CreateAssignment(ctx, &assignment.Assignment{
    RoleID:      r.ID,
    SubjectKind: "user",
    SubjectID:   "user-42",
})

Role Hierarchy

Roles can have parents. A child role inherits all permissions from its parent:

admin (all permissions)
  └── editor (document:write, document:read)
        └── viewer (document:read)
go
admin  := &role.Role{Name: "Admin",  Slug: "admin"}
editor := &role.Role{Name: "Editor", Slug: "editor", ParentSlug: "admin"}
viewer := &role.Role{Name: "Viewer", Slug: "viewer", ParentSlug: "editor"}

Resource-Scoped Assignments

Assignments can be scoped to specific resources (runtime-only — no DSL form):

// User is editor ONLY for project-123
store.CreateAssignment(ctx, &assignment.Assignment{
    RoleID:       editorRole.ID,
    SubjectKind:  "user",
    SubjectID:    "user-42",
    ResourceType: "project",
    ResourceID:   "project-123",
})

ABAC — Attribute-Based Access Control02

ABAC evaluates policies with conditions based on attributes of the request context.

Policy + Conditions → (subjects, actions, resources) → allow/deny

Entities:

  • Policy — A rule with effect (allow/deny), subject/action/resource matchers, and conditions

  • Condition — A field/operator/value comparison (e.g., ip_address ip_in_cidr 10.0.0.0/8)

When to use: When authorization depends on dynamic attributes like time of day, IP address, department, or custom metadata.

go
p := &policy.Policy{
    Name:      "deny-outside-office-hours",
    Effect:    policy.EffectDeny,
    IsActive:  true,
    Priority:  100,
    Actions:   []string{"write"},
    Resources: []string{"document:*"},
    Conditions: []policy.Condition{{
        Field:    "time",
        Operator: policy.OpTimeAfter,
        Value:    "18:00",
    }},
}

Supported Operators

OperatorDescriptionExample
== / !=Equals / not equalssubject.attributes.department == "engineering"
in / not inIn / not in listsubject.attributes.country in ["US", "CA", "UK"]
containsString containssubject.email contains "@company.com"
starts_with / ends_withPrefix / suffixresource.path starts_with "/api/"
> / < / >= / <=Numeric comparisonsubject.attributes.risk_score > 80
ip_in_cidrIP in CIDR rangecontext.ip ip_in_cidr "10.0.0.0/8"
time_after / time_beforeTime comparisoncontext.time time_after "09:00"
=~Regex matchresource.path =~ "^/api/v[0-9]+"
exists / not existsField presencesubject.attributes.mfa_verified exists

The Go side uses policy.Op* constants; the DSL spells them as keywords. See Policies & Conditions for the full table.

ReBAC — Relationship-Based Access Control03

ReBAC (inspired by Google Zanzibar) models authorization as a graph of relationships between objects.

subject#relation@object → transitive graph traversal

Entities:

  • Resource Type — Defines valid relations and permissions for an object type

  • Relation Tuple — A (object_type, object_id, relation, subject_type, subject_id) record

When to use: When authorization depends on relationships between objects (document sharing, org membership, folder hierarchy).

go
// "user:42 is a viewer of document:123"
store.CreateRelation(ctx, &relation.Tuple{
    ObjectType:  "document",
    ObjectID:    "doc-123",
    Relation:    "viewer",
    SubjectType: "user",
    SubjectID:   "user-42",
})

// "team:engineering is an editor of project:alpha"
store.CreateRelation(ctx, &relation.Tuple{
    ObjectType:  "project",
    ObjectID:    "project-alpha",
    Relation:    "editor",
    SubjectType: "team",
    SubjectID:   "team-eng",
})

Transitive Relations

ReBAC automatically follows transitive paths:

user:42 ──member──▶ team:eng ──editor──▶ project:alpha

A check for "can user:42 edit project:alpha?" traverses the graph and finds the path through team:eng.

PBAC — Policy-Based Access Control04

PBAC builds on ABAC. Policies use the same policy.Policy type, evaluator, and storage shape — PBAC adds two orthogonal capabilities:

  1. Time-bound policies via NotBefore / NotAfter. Outside the window the policy is treated as IsActive=false. Either bound is optional.

  2. Obligations — named side-effect actions emitted in CheckResult.Obligations whenever the policy matches. Common values: audit-log, require-mfa, notify-security.

When to use: when policies need to activate for a scheduled window (incident freezes, Q2 access grants) or when matching needs to trigger downstream actions without changing the allow/deny outcome.

go
notAfter := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

p := &policy.Policy{
    Name:        "incident-freeze",
    Effect:      policy.EffectDeny,
    Priority:    1,
    IsActive:    true,
    NotAfter:    &notAfter, // policy expires at 2026-06-01
    Actions:     []string{"deploy:*"},
    Obligations: []string{"notify-oncall", "audit-log"},
}

After a Check():

  • CheckResult.Obligations contains the deduplicated list of obligations from every matched policy (allow OR deny).

  • Each obligation also fires the PolicyObligationFired plugin hook so audit / Chronicle / dispatcher plugins can react without scanning the result.

See Policies & Conditions for the full reference and the .warden Language Reference for the DSL syntax.

Combining Models05

Warden evaluates all enabled models on every Check() call and merges results:

1. Explicit DENY from any model  → DENIED
2. ALLOW from any model          → ALLOWED
3. No match                      → DENIED (default deny)

ABAC/PBAC deny policies act as guardrails on top of RBAC and ReBAC grants. Obligations from every matched policy flow through to the final result regardless of which decision wins — they're side-effect signals, not part of the merge calculus.

Scoping the Models06

Every model evaluation is scoped by (tenant_id, namespace_path):

  • Tenant — a hard wall. No model ever crosses tenants. See Multi-Tenancy.

  • Namespace — a hierarchy inside a tenant. Roles, permissions, policies, and resource types declared at namespace N are visible from N and every descendant. Relation tuples don't cascade — they sit at exactly one namespace. See Namespaces.

Single-team deployments can ignore both — entities at empty tenant_id and empty namespace_path are the natural default.