Authorization Models
Understanding RBAC, ABAC, ReBAC, and PBAC and when to use each.
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 Control
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.
// 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",
})warden config 1
tenant t1
permission "document:write" {
resource = "document"
action = "write"
}
role editor {
name = "Editor"
grants = ["document:write"]
}Assignments aren't expressible in the DSL — they describe runtime data (who has which role), not configuration. Apply the DSL once for setup, then create assignments via
store.CreateAssignmentwhenever subjects are provisioned.
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)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"}warden config 1
tenant t1
role admin { name = "Admin" }
role editor : admin { name = "Editor" }
role viewer : editor { name = "Viewer" }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 Control
ABAC evaluates policies with conditions based on attributes of the request context.
Policy + Conditions → (subjects, actions, resources) → allow/denyEntities:
- 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.
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",
}},
}warden config 1
tenant t1
policy "deny-outside-office-hours" {
effect = deny
active = true
priority = 100
actions = ["write"]
resources = ["document:*"]
when {
time time_after "18:00"
}
}Supported Operators
| Operator | Description | Example |
|---|---|---|
== / != | Equals / not equals | subject.attributes.department == "engineering" |
in / not in | In / not in list | subject.attributes.country in ["US", "CA", "UK"] |
contains | String contains | subject.email contains "@company.com" |
starts_with / ends_with | Prefix / suffix | resource.path starts_with "/api/" |
> / < / >= / <= | Numeric comparison | subject.attributes.risk_score > 80 |
ip_in_cidr | IP in CIDR range | context.ip ip_in_cidr "10.0.0.0/8" |
time_after / time_before | Time comparison | context.time time_after "09:00" |
=~ | Regex match | resource.path =~ "^/api/v[0-9]+" |
exists / not exists | Field presence | subject.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 Control
ReBAC (inspired by Google Zanzibar) models authorization as a graph of relationships between objects.
subject#relation@object → transitive graph traversalEntities:
- 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).
// "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",
})warden config 1
tenant t1
resource document {
relation viewer: user
permission read = viewer
}
resource project {
relation editor: team#member
permission write = editor
}
// Initial tuples — usually high-volume tuples are written at runtime
// via store.CreateRelation, not in source.
relation document:doc-123 viewer = user:user-42
relation project:project-alpha editor = team:team-eng#memberTransitive Relations
ReBAC automatically follows transitive paths:
user:42 ──member──▶ team:eng ──editor──▶ project:alphaA check for "can user:42 edit project:alpha?" traverses the graph and finds the path through team:eng.
PBAC — Policy-Based Access Control
PBAC builds on ABAC. Policies use the same policy.Policy type, evaluator, and storage shape — PBAC adds two orthogonal capabilities:
- Time-bound policies via
NotBefore/NotAfter. Outside the window the policy is treated asIsActive=false. Either bound is optional. - Obligations — named side-effect actions emitted in
CheckResult.Obligationswhenever 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.
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: ¬After, // policy expires at 2026-06-01
Actions: []string{"deploy:*"},
Obligations: []string{"notify-oncall", "audit-log"},
}warden config 1
tenant t1
policy "incident-freeze" {
effect = deny
priority = 1
active = true
not_after = "2026-06-01T00:00:00Z"
actions = ["deploy:*"]
obligations = ["notify-oncall", "audit-log"]
}After a Check():
CheckResult.Obligationscontains the deduplicated list of obligations from every matched policy (allow OR deny).- Each obligation also fires the
PolicyObligationFiredplugin 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 Models
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 Models
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
Nare visible fromNand 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.