Policies & Conditions
ABAC and PBAC policies with conditional access rules, time-bound windows, and obligations.
Policies
A policy is an ABAC rule that matches requests by subject, action, and resource, then applies conditions to decide whether to allow or deny. PBAC extends ABAC with time-bound windows (NotBefore / NotAfter) and obligations (named side-effect signals).
Every config example on this page comes in two flavours: Go (programmatic — call the store directly) and DSL (declarative — .warden source applied via warden apply or dsl.ApplyFS). They produce identical state.
Policy Fields
| Field | Type | Description |
|---|---|---|
ID | id.PolicyID | TypeID (wpol_...) — auto-assigned on Create when unset |
Name | string | Display name |
Description | string | Human-readable description |
Effect | Effect | allow or deny |
Priority | int | Lower number = higher priority |
IsActive | bool | Manual on/off toggle — only active policies are evaluated |
NotBefore | *time.Time | PBAC: policy is inactive before this instant (optional) |
NotAfter | *time.Time | PBAC: policy is inactive after this instant (optional) |
Obligations | []string | PBAC: named side-effect actions emitted on match |
Version | int | Auto-incremented on update |
Subjects | []SubjectMatch | Subject matchers |
Actions | []string | Action matchers (glob patterns) |
Resources | []string | Resource matchers (glob patterns) |
Conditions | []Condition | All conditions must be true |
Metadata | map[string]any | Custom key-value data |
Create a Policy
import "github.com/xraph/warden/policy"
// IDs and CreatedAt/UpdatedAt timestamps are auto-assigned by the
// store on Create — set them only when you need a specific value.
p := &policy.Policy{
Name: "internal-network-only",
Description: "Allow access only from internal IP ranges",
Effect: policy.EffectAllow,
Priority: 10,
IsActive: true,
Version: 1,
Subjects: []policy.SubjectMatch{{Kind: "user"}},
Actions: []string{"read", "write"},
Resources: []string{"document:*"},
Conditions: []policy.Condition{
{
Field: "ip_address",
Operator: policy.OpIPInCIDR,
Value: "10.0.0.0/8",
},
},
}
err := store.CreatePolicy(ctx, p)warden config 1
tenant t1
policy "internal-network-only" {
description = "Allow access only from internal IP ranges"
effect = allow
priority = 10
active = true
actions = ["read", "write"]
resources = ["document:*"]
when {
ip_address ip_in_cidr "10.0.0.0/8"
}
}Conditions
Conditions are evaluated against the Context map in a CheckRequest. By default all conditions in a when block are AND-ed; use all_of / any_of to override.
Condition Fields
| Field | Type | Description |
|---|---|---|
ID | id.ConditionID | TypeID (wcnd_...) — auto-assigned on Create when unset |
Field | string | Dotted path into the request (e.g. subject.attributes.dept) |
Operator | Operator | Comparison operator |
Value | any | Expected value |
Negate | bool | Flips the result |
Operators Reference
The same operators surface in both flavours — Go uses the policy.Op* constants, the DSL spells them as keywords:
| Category | Go constant | DSL keyword |
|---|---|---|
| String | OpEq / OpNeq | == / != |
OpContains | contains | |
OpStartsWith / OpEndsWith | starts_with / ends_with | |
OpRegex | =~ | |
| Collection | OpIn / OpNotIn | in / not in |
| Numeric | OpGT / OpLT / OpGTE / OpLTE | > / < / >= / <= |
| Network | OpIPInCIDR | ip_in_cidr |
| Time | OpTimeAfter / OpTimeBefore | time_after / time_before |
| Presence | OpExists / OpNotExists | exists / not exists |
Policy Matching
Subject Matchers
Subjects: []policy.SubjectMatch{{Kind: "user"}} // All users
Subjects: []policy.SubjectMatch{{Kind: "user", ID: "u-42"}} // Specific user
Subjects: []policy.SubjectMatch{{Kind: "api_key"}} // All API keys
// Empty Subjects slice means: match any subject.Action Matchers
Actions: []string{"read"} // Specific action
Actions: []string{"read", "write"} // Multiple actions
Actions: []string{"*"} // All actionsResource Matchers
Resources: []string{"document:*"} // All documents
Resources: []string{"document:doc-1"} // Specific document
Resources: []string{"*"} // All resourcesExamples
Deny After Business Hours
&policy.Policy{
Name: "business-hours-only",
Effect: policy.EffectDeny,
Actions: []string{"write", "delete"},
Resources: []string{"*"},
IsActive: true,
Conditions: []policy.Condition{
{Field: "time", Operator: policy.OpTimeAfter, Value: "18:00"},
},
}policy "business-hours-only" {
effect = deny
active = true
actions = ["write", "delete"]
resources = ["*"]
when {
time time_after "18:00"
}
}Allow Only From Trusted IPs
&policy.Policy{
Name: "vpn-required-for-admin",
Effect: policy.EffectDeny,
Subjects: []policy.SubjectMatch{{Kind: "user"}},
Actions: []string{"*"},
Resources: []string{"admin:*"},
IsActive: true,
Conditions: []policy.Condition{
// Negated: deny when NOT in 10.0.0.0/8.
{Field: "ip_address", Operator: policy.OpIPInCIDR, Value: "10.0.0.0/8", Negate: true},
},
}policy "vpn-required-for-admin" {
effect = deny
active = true
actions = ["*"]
resources = ["admin:*"]
when {
ip_address ip_in_cidr "10.0.0.0/8" negate
}
}Department-Based Access
&policy.Policy{
Name: "engineering-only",
Effect: policy.EffectAllow,
Subjects: []policy.SubjectMatch{{Kind: "user"}},
Actions: []string{"read", "write"},
Resources: []string{"code:*"},
IsActive: true,
Conditions: []policy.Condition{
{Field: "subject.attributes.department", Operator: policy.OpEq, Value: "engineering"},
},
}policy "engineering-only" {
effect = allow
active = true
actions = ["read", "write"]
resources = ["code:*"]
when {
subject.attributes.department == "engineering"
}
}PBAC — Time-Bound Policies & Obligations
PBAC adds two orthogonal capabilities on top of plain ABAC. Same policy.Policy type, same evaluator, same storage table — three new fields.
Time-Bound Policies
NotBefore and NotAfter define a half-open effective window. Outside it, the engine skips the policy as if IsActive=false. Either bound may be nil ("no limit on that side").
Policy.EffectiveAt(t time.Time) bool is the single source of truth:
func (p *Policy) EffectiveAt(t time.Time) bool {
if !p.IsActive {
return false
}
if p.NotBefore != nil && t.Before(*p.NotBefore) {
return false
}
if p.NotAfter != nil && t.After(*p.NotAfter) {
return false
}
return true
}Incident Freeze
Deny all deploys until 2026-06-01.
notAfter := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
&policy.Policy{
Name: "incident-freeze",
Effect: policy.EffectDeny,
Priority: 1,
IsActive: true,
NotAfter: ¬After,
Actions: []string{"deploy:*"},
}policy "incident-freeze" {
effect = deny
priority = 1
active = true
not_after = "2026-06-01T00:00:00Z"
actions = ["deploy:*"]
}Scheduled Grant
Allow data exports during Q2 2026 only.
notBefore := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
notAfter := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
&policy.Policy{
Name: "q2-export-window",
Effect: policy.EffectAllow,
IsActive: true,
NotBefore: ¬Before,
NotAfter: ¬After,
Actions: []string{"export"},
Resources: []string{"dataset:*"},
}policy "q2-export-window" {
effect = allow
active = true
not_before = "2026-04-01T00:00:00Z"
not_after = "2026-07-01T00:00:00Z"
actions = ["export"]
resources = ["dataset:*"]
}The default evaluator uses time.Now(). Tests inject a fixed clock:
fixed := func() time.Time { return time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) }
eng, _ := warden.NewEngine(
warden.WithStore(store),
warden.WithEvaluator(warden.NewConditionEvaluator(fixed)),
)Obligations
Obligations are named side-effect actions a policy emits when it matches. They flow through to CheckResult.Obligations (deduplicated across every matched policy, allow OR deny) and fire the PolicyObligationFired plugin hook once per obligation.
Obligations don't change the allow/deny decision — they're signals to the calling system.
&policy.Policy{
Name: "after-hours-mfa",
Effect: policy.EffectAllow,
IsActive: true,
Actions: []string{"write", "delete"},
Obligations: []string{"require-mfa", "audit-log"},
Conditions: []policy.Condition{
{Field: "context.time", Operator: policy.OpTimeBefore, Value: "09:00:00Z"},
{Field: "context.time", Operator: policy.OpTimeAfter, Value: "17:00:00Z"},
},
}policy "after-hours-mfa" {
effect = allow
active = true
actions = ["write", "delete"]
obligations = ["require-mfa", "audit-log"]
when {
context.time time_before "09:00:00Z"
context.time time_after "17:00:00Z"
}
}After eng.Check(...):
result, _ := eng.Check(ctx, req)
for _, ob := range result.Obligations {
switch ob {
case "require-mfa":
if !req.Context["mfa_verified"].(bool) {
return errors.New("re-authentication required")
}
case "audit-log":
// Already handled by an audit plugin via PolicyObligationFired.
}
}Plugin Hook
type PolicyObligationFired interface {
OnPolicyObligationFired(
ctx context.Context,
polID id.PolicyID,
obligation string,
req, result any, // *warden.CheckRequest / *warden.CheckResult
) error
}Fired once per obligation in the deduplicated post-merge result. Errors returned from the hook are logged but never block the pipeline. Chronicle / audit / dispatcher plugins implement this to react without scanning CheckResult.Obligations themselves.
not_before / not_after accept RFC3339 timestamps (with or without nanoseconds). The resolver flags windows where not_after < not_before as a diagnostic. See the language reference for the full policy block grammar.