Warden

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

FieldTypeDescription
IDid.PolicyIDTypeID (wpol_...) — auto-assigned on Create when unset
NamestringDisplay name
DescriptionstringHuman-readable description
EffectEffectallow or deny
PriorityintLower number = higher priority
IsActiveboolManual on/off toggle — only active policies are evaluated
NotBefore*time.TimePBAC: policy is inactive before this instant (optional)
NotAfter*time.TimePBAC: policy is inactive after this instant (optional)
Obligations[]stringPBAC: named side-effect actions emitted on match
VersionintAuto-incremented on update
Subjects[]SubjectMatchSubject matchers
Actions[]stringAction matchers (glob patterns)
Resources[]stringResource matchers (glob patterns)
Conditions[]ConditionAll conditions must be true
Metadatamap[string]anyCustom 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

FieldTypeDescription
IDid.ConditionIDTypeID (wcnd_...) — auto-assigned on Create when unset
FieldstringDotted path into the request (e.g. subject.attributes.dept)
OperatorOperatorComparison operator
ValueanyExpected value
NegateboolFlips the result

Operators Reference

The same operators surface in both flavours — Go uses the policy.Op* constants, the DSL spells them as keywords:

CategoryGo constantDSL keyword
StringOpEq / OpNeq== / !=
OpContainscontains
OpStartsWith / OpEndsWithstarts_with / ends_with
OpRegex=~
CollectionOpIn / OpNotInin / not in
NumericOpGT / OpLT / OpGTE / OpLTE> / < / >= / <=
NetworkOpIPInCIDRip_in_cidr
TimeOpTimeAfter / OpTimeBeforetime_after / time_before
PresenceOpExists / OpNotExistsexists / 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 actions

Resource Matchers

Resources: []string{"document:*"}      // All documents
Resources: []string{"document:doc-1"}  // Specific document
Resources: []string{"*"}               // All resources

Examples

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: &notAfter,
    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: &notBefore,
    NotAfter:  &notAfter,
    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.

On this page