warden

package module
v1.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: Apache-2.0 Imports: 32 Imported by: 9

README

Warden, a Go authorization engine combining RBAC, ABAC and ReBAC with a declarative config language

Warden: RBAC, ABAC and ReBAC authorization for Go

Warden is an authorization engine for Go. It answers "may this subject do this action on this resource?" by combining role-based access control (RBAC), attribute-based policies (ABAC) and Zanzibar-style relationship-based access control (ReBAC) behind one Check call, with every role, policy and relation scoped to a tenant. You can manage permissions in code, through a REST and AuthZEN API, or declare them in .warden files that a CLI lints, diffs and applies. It runs inside your process against your own database, and it ships as a Forge extension too.

CI Go Reference Release GitHub Stars Apache 2.0

Documentation · Quick start · Features · Models · Declarative config · Middleware · Forge · FAQ

Quick start

You need Go 1.26 or later.

go get github.com/xraph/warden@latest

Save this as main.go. It creates a role with one permission, gives it to a user, and checks two actions:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/xraph/warden"
	"github.com/xraph/warden/assignment"
	"github.com/xraph/warden/permission"
	"github.com/xraph/warden/role"
	"github.com/xraph/warden/store/memory"
)

func main() {
	ctx := warden.WithTenant(context.Background(), "docs-app", "acme")
	s := memory.New()

	eng, err := warden.NewEngine(warden.WithStore(s))
	if err != nil {
		log.Fatal(err)
	}

	// An editor role that can read documents. The store assigns the IDs.
	editor := &role.Role{TenantID: "acme", Name: "Editor", Slug: "editor"}
	if err := s.CreateRole(ctx, editor); err != nil {
		log.Fatal(err)
	}
	err = s.CreatePermission(ctx, &permission.Permission{
		TenantID: "acme", Name: "document:read", Resource: "document", Action: "read",
	})
	if err != nil {
		log.Fatal(err)
	}
	if err := s.AttachPermission(ctx, "acme", editor.ID, permission.Ref{Name: "document:read"}); err != nil {
		log.Fatal(err)
	}

	// Give alice the role.
	err = s.CreateAssignment(ctx, &assignment.Assignment{
		TenantID: "acme", RoleID: editor.ID, SubjectKind: "user", SubjectID: "alice",
	})
	if err != nil {
		log.Fatal(err)
	}

	result, err := eng.Check(ctx, &warden.CheckRequest{
		Subject:  warden.Subject{Kind: warden.SubjectUser, ID: "alice"},
		Action:   warden.Action{Name: "read"},
		Resource: warden.Resource{Type: "document", ID: "doc-123"},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("alice read: %v (%s)\n", result.Allowed, result.Decision)

	canDelete, err := eng.CanI(ctx, warden.SubjectUser, "alice", "delete", "document", "doc-123")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("alice delete: %v\n", canDelete)
}

Run it:

go mod tidy
go run .
# alice read: true (allow)
# alice delete: false

Check gives you the decision with a reason and what matched. If you'd rather have an error on deny, for a code path that should just stop, use Enforce, and when all you want is a boolean, CanI is the one-line form. You set the tenant with warden.WithTenant here. Under Forge you don't have to, because Warden reads it from forge.Scope (app ID and org ID). If you check with no tenant at all, you get ErrTenantRequired unless you turn RequireTenant off.

Why Warden?

Most apps start with an isAdmin column and end up needing roles per organization, a deny rule for one region, and "anyone who can edit the folder can edit the document". Warden gives you all three models over the same data, so you add the next one when you need it without moving to a different system.

You need Warden gives you
Roles and permissions Roles with inheritance, permissions attached by name, and assignments to users or other subjects, optionally scoped to one resource
Rules on attributes ABAC policies with conditions such as eq, in, contains, gt, ip_in_cidr, time_after and regex, explicit deny, and obligations
Sharing and hierarchies Relation tuples (object#relation@subject) and a graph walk with depth, fanout and visit budgets
Multi-tenant isolation A tenant in every key, and a wrong-tenant lookup that looks exactly like a missing row
Config as code The .warden language, plus a warden CLI and a language server for VS Code
An API for other services A REST management API under /v1, check endpoints, and the AuthZEN evaluation API
Answers you can audit A check log written for every decision (including errors), lifecycle hooks and a structured audit-log plugin
Speed An in-memory LRU check cache with TTL, invalidated by the hooks
Your database PostgreSQL, SQLite and MongoDB through Grove, and an in-memory store

How a check is decided

Every time you call Check, Warden runs the models in this order:

  1. RBAC resolves the subject's roles, including inherited ones, and looks for a matching permission.
  2. ReBAC walks relation tuples, unless RBAC already allowed the request (set EvaluateAllModels to run it anyway).
  3. ABAC evaluates active policies. It always runs, because an explicit deny has to be able to override an allow from the other two.

Then the results merge. An explicit deny from a policy wins. Otherwise any allow from any model grants access, and if nothing matches, you get a default deny carrying the most useful reason Warden found, which is what you'll want to log when a user asks why they were refused. Obligations from every matched policy ride along on the result whichever way the decision went.

Check fails closed. A store or evaluation error returns no result and the error, and it still writes a check-log entry with decision error. If you want to try a request without side effects, pass warden.WithCallDryRun(), which skips the cache, the check log and the hooks.

The decision model docs and the check engine page go deeper.

Declare permissions in .warden files

You don't have to create roles from Go. You can describe a tenant's model in a .warden file, keep it in your repository and review it like code:

warden config 1
tenant acme

resource document {
  relation owner:  user
  relation viewer: user

  permission read   = viewer or owner
  permission delete = owner
}

permission "document:read"   (document : read)
permission "document:delete" (document : delete)

role viewer {
  name   = "Viewer"
  grants = ["document:read"]
}

role admin : viewer {
  name   = "Administrator"
  grants += ["document:delete"]
}

Install the CLI and check it:

go install github.com/xraph/warden/cmd/warden@latest
warden lint authz.warden
warden apply -f authz.warden --store memory: --dry-run

warden apply and warden diff take a store DSN (memory:, sqlite:./warden.db or a postgres:// URL), or read it from WARDEN_STORE_DSN so a password stays out of your shell history. warden fmt formats files, warden export dumps a tenant's current state back to .warden files, and warden lsp starts the language server. Apply is idempotent, and --prune deletes entries that are not in the files. ${NAME} placeholders expand from --var NAME=value or WARDEN_VAR_NAME.

From Go, dsl.ApplyFS(ctx, eng, fsys, root, dsl.ApplyOptions{TenantID: "acme"}) applies an embedded config tree at boot. The standalone-embed example shows it with //go:embed. Prebuilt warden and warden-lsp binaries for Linux, macOS and Windows are on GitHub Releases, and the VS Code extension lives in editor/vscode-warden.

Protect Forge routes

The middleware package guards your own Forge routes:

router.GET("/documents/:id", getDocument,
	forge.WithMiddleware(wardenmw.Require(eng, "read", "document")),
)

Require takes the subject from forge.UserIDFromContext and the resource ID from the :id path parameter. A caller with no identity gets a 401 (or runs as the fixed subject anonymous with wardenmw.AllowAnonymous()), a denied check gets a 403, and a store error a 503. RequireAny and RequireAll take a list of checks, and WithContext adds request attributes for your ABAC conditions. Read the middleware guide for the details.

Storage backends

Every backend implements store.Store with Migrate, Ping and Close, so you can swap one for another without touching your checks.

Backend Package Constructor
In-memory store/memory memory.New()
PostgreSQL store/postgres postgres.New(db *grove.DB, opts...)
SQLite store/sqlite sqlite.New(db *grove.DB, opts...)
MongoDB store/mongo mongo.New(db *grove.DB, opts...)

Call Migrate(ctx) before first use, or let the Forge extension do it on start. When you migrate yourself, register Grove's migration executor with a blank import of github.com/xraph/grove/drivers/pgdriver/pgmigrate or .../sqlitedriver/sqlitemigrate.

Caching and hooks

If you give the engine a cache, repeated checks skip the store:

eng, err := warden.NewEngine(
	warden.WithStore(s),
	warden.WithCache(cache.NewMemory(cache.WithTTL(5*time.Minute), cache.WithMaxSize(10000))),
)

With a cache configured, the engine registers a hook that drops affected entries when roles, permissions, assignments, relations or policies change. Those hooks fire from the management API, the dashboard and the DSL applier. If you write to the store directly, as the quick start does, call eng.InvalidateTenant or eng.InvalidateSubject yourself.

Plugins register with warden.WithPlugin. A plugin implements plugin.Plugin (just Name()) and any of 19 hooks covering checks, roles, permissions, assignments, relations, policies, obligations, audit events and shutdown. Hook errors and panics are logged and counted, and they never block the caller. plugin/auditlog writes one structured log line per audit event, and keysmithbridge lets Keysmith assign Warden roles to API keys without either module importing the other. See the plugin system docs.

Use it with Forge

The extension package registers Warden as a Forge extension. It reads extensions.warden (or a top-level warden key) and validates it, builds a store from the container's *grove.DB, provides *warden.Engine for injection, mounts the management API under /warden, runs migrations on start, and can apply .warden files at startup.

go get github.com/xraph/warden@latest github.com/xraph/grove/extension@latest
import (
	grove "github.com/xraph/grove/extension"
	wardenext "github.com/xraph/warden/extension"
	wardenmw "github.com/xraph/warden/middleware"
)

authz := wardenext.New()

app := forge.NewApp(forge.AppConfig{
	Name: "docs-api",
	Extensions: []forge.Extension{
		grove.New(), // extensions.grove: driver and dsn
		authz,       // extensions.warden
	},
})

// The engine exists once the extension has registered, so mount
// guarded routes from a hook that runs before the server listens.
err := forge.OnBeforeRun(app, "document-routes", func(_ context.Context, a forge.App) error {
	return a.Router().GET("/documents/:id", getDocument,
		forge.WithMiddleware(wardenmw.Require(authz.Engine(), "read", "document")),
	)
})

List Grove first, because Warden looks up the database during Register. If you don't use Grove, pass your store with wardenext.WithStore(...).

The extension hands the engine every store it finds, and the last one wins. In effect the order is:

  1. A grove store: the database named by WithGroveDatabase or grove_database, or else the default *grove.DB in the container, which the extension finds on its own.
  2. The store from WithStore (or a warden.WithStore passed through WithEngineOptions).
  3. A store.Store already registered in the container.

So a grove store replaces your WithStore store whenever the Grove extension has registered a default database, even if you never asked for grove. If you want WithStore in such an app, don't register a default grove database. With none of these, the engine has no store and Register fails.

The management API requires an authenticated caller on every route by default. Turning that off with auth.require_identity: false is refused at startup unless you also pass wardenext.WithInsecureAllowUnauthenticatedRoutes(), so nobody switches it off by accident. Being signed in is not enough on its own, either: the API also authorizes each call, and nobody holds the management permissions until something grants them. Call authz.BootstrapAdmin(ctx, tenantID, subject) once per tenant, after start, to create a warden-admin role and assign it to your first operator. It's safe to call again.

Metrics and tracing

Under Forge you get both without writing any code. The extension hands the app's metrics collector and logger to the engine, so /_/metrics carries forge_warden_checks_total (by decision, source and whether it came from the cache), a check-latency histogram, store errors by operation, check-log writes and drops, graph-walk limits and cache invalidations. Every label is a closed set, so a subject or tenant never becomes a series.

Each Check is a check permission span inside the request's trace. When the trace is sampled it gets a child span per model it ran and one for the ReBAC graph walk. Grove's query spans land under whichever of those made the query, so a slow check tells you which model to look at. Check-log flushes and scheduled maintenance runs get traces of their own. Subject IDs, resource IDs, tenants and error messages stay off every span, and an action or resource type that doesn't look like a schema name is recorded as invalid.

Building the engine yourself? Pass warden.WithMetricsSink(app.Metrics()), or your own warden.Metrics through warden.WithMetrics. The monitoring page lists every metric, label value, span and attribute.

Configuration

extensions:
  warden:
    base_path: /warden
    grove_database: primary          # optional; the default *grove.DB otherwise
    max_graph_depth: 10
    cache_ttl: 5m                    # > 0 builds an in-memory check cache
    evaluate_all_models: false
    check_log_retention: 2160h       # 90 days; negative turns purging off
    maintenance_interval: 1h
    auth:
      require_identity: true
      allow_anonymous_checks: false  # opens only the three /v1/authz check routes
      audit_log: true
    declarative_on_start: true
    declarative_path: ./authz/       # file, directory or glob
    declarative_strict: true         # fail startup on diagnostics
    declarative_prune: false
    dashboard:
      tenant_id: acme                # single-tenant deployments only
Key What it does Default
disable_routes, disable_migrate Skip the HTTP API, or migrations on start false
base_path Prefix for the management API /warden
max_graph_depth, max_graph_visited, max_graph_fanout ReBAC graph walk budgets depth 10
cache_ttl, cache_max_size Build a memory check cache when the TTL is above zero no cache
evaluate_all_models Run ReBAC even after RBAC allowed false
require_tenant, enable_check_log Turn off the tenant requirement or the check log both on
check_log_retention, maintenance_interval Check-log purge age and how often maintenance runs 90 days, 1h
max_batch_checks, check_log_queue_size Batch and queue bounds engine defaults
auth.* Identity, anonymous checks and the audit-log plugin identity on, audit log on
declarative_* Apply .warden files on start, with declarative_paths, declarative_tenant_id, prune and strict mode off
dashboard.tenant_id Tenant for dashboard requests with no tenant claim empty

The options are WithStore, WithConfig, WithEngineOptions, WithPlugin, WithDisableRoutes, WithDisableMigrate, WithBasePath, WithRequireConfig, WithGroveDatabase and WithInsecureAllowUnauthenticatedRoutes. The Forge dashboard shows Warden through a React plugin; MIGRATION.md maps each old templ page to its new route.

Documentation

Topic Where
Getting started Getting started, architecture, standalone usage, full example
Concepts Authorization models, decision model, multi-tenancy, namespaces
Authorization Roles and permissions, policies and conditions, relations and tuples, resource types
Declarative config DSL reference, DSL tooling
Integration Forge extension, middleware, plugin system, REST API
Operations Monitoring, retention, hardening
Storage PostgreSQL, SQLite, MongoDB, memory, custom store

The Go API is on pkg.go.dev.

Examples

Example What it shows
standalone Warden without Forge, as in the quick start
rbac Roles with inheritance
rebac Zanzibar-style relation tuples
abac Attribute-based policies
standalone-embed Applying an embedded .warden config tree at boot
declarative A .warden file with a policy and an obligation
forge Warden as a Forge extension

Run one from the repository root, for example GOWORK=off go run ./_examples/rbac.

FAQ

How do I add role-based access control to a Go API?

Create roles and permissions, assign roles to users, and call eng.Check or eng.Enforce before the protected action. On Forge routes, middleware.Require(eng, "read", "document") does it for you.

Can I use RBAC and ReBAC together in Go?

Yes. Warden evaluates both on every check (ReBAC only when RBAC has not already allowed), and ABAC policies on top. Any allow grants access unless a policy explicitly denies.

Is Warden a Zanzibar implementation?

Its ReBAC model uses Zanzibar-style relation tuples and a graph walk, inside your own process and database. There is no separate authorization service to run.

Does Warden support AuthZEN?

Yes. The management API serves the AuthZEN evaluation endpoints at /access/v1/evaluation and /access/v1/evaluations, next to Warden's own /v1/authz/check, /enforce and /batch-check.

Can I use Warden without Forge?

Yes. The engine, stores, DSL and CLI are plain Go. Only the extension and middleware packages expect a Forge app.

Contributing

Bug reports and focused pull requests are welcome. main is protected, so every change lands through a pull request with passing checks. CONTRIBUTING.md has the guidelines.

git clone https://github.com/xraph/warden.git
cd warden
make check              # fmt, vet and lint
make test               # memory and SQLite
make test-integration   # PostgreSQL and MongoDB, through Docker or WARDEN_TEST_DSN and WARDEN_TEST_MONGO_URI

Without Docker or those variables, the integration tests skip and the run still passes, so read the output for skips. AGENTS.md has the coverage gate, the lint rules that bite and the release steps. Commits follow Conventional Commits.

License

Warden is available under the Apache License 2.0.

Warden is part of the Forge forgery stack, built by Rex Raphael and contributors.

Documentation

Overview

Package warden provides composable permissions and authorization for Go.

Warden supports RBAC (role-based), ABAC (attribute-based), and ReBAC (relationship-based) authorization models individually or combined. It is tenant-scoped by default via forge.Scope and integrates with the Forge ecosystem for audit logging, feature flags, and async jobs.

eng, err := warden.NewEngine(
    warden.WithStore(memStore),
)
result, err := eng.Check(ctx, &warden.CheckRequest{
    Subject:  warden.Subject{Kind: warden.SubjectUser, ID: "user_123"},
    Action:   warden.Action{Name: "read"},
    Resource: warden.Resource{Type: "document", ID: "doc_456"},
})

Index

Constants

View Source
const MaxNamespaceDepth = 8

MaxNamespaceDepth is the default cap on namespace nesting. Practical organizations don't go deeper than this; the cap keeps walking-cost predictable. Configurable via Config.MaxNamespaceDepth.

Variables

View Source
var (
	// ErrAccessDenied is returned when an authorization check fails.
	ErrAccessDenied = errors.New("warden: access denied")

	// ErrNotFound is the common base error for the missing-entity errors
	// below. Use errors.Is(err, ErrNotFound) to match any of them.
	//
	// Defined in wardenerr so that low-level subpackages (e.g. store/memory)
	// can return typed not-found errors without an import cycle.
	ErrNotFound = wardenerr.ErrNotFound

	// ErrRoleNotFound is returned when a role cannot be found in the tenant.
	// Every store returns this for a by-ID lookup whose tenant does not own
	// the row, so a caller cannot distinguish "no such role" from "that role
	// belongs to someone else".
	ErrRoleNotFound = wardenerr.ErrRoleNotFound

	// ErrPermissionNotFound is returned when a permission cannot be found in the tenant.
	ErrPermissionNotFound = wardenerr.ErrPermissionNotFound

	// ErrAssignmentNotFound is returned when an assignment cannot be found in the tenant.
	ErrAssignmentNotFound = wardenerr.ErrAssignmentNotFound

	// ErrPolicyNotFound is returned when a policy cannot be found in the tenant.
	ErrPolicyNotFound = wardenerr.ErrPolicyNotFound

	// ErrStaleWrite is the common base error for a conditional write that
	// found the record changed since the caller read it. Use
	// errors.Is(err, ErrStaleWrite) to match any of the specialized
	// conflict errors, such as ErrPolicyVersionConflict.
	ErrStaleWrite = wardenerr.ErrStaleWrite

	// ErrPolicyVersionConflict is returned by UpdatePolicyIfVersion when the
	// stored policy's version is not the one the caller expected. Nothing is
	// written. Wraps ErrStaleWrite.
	ErrPolicyVersionConflict = wardenerr.ErrPolicyVersionConflict

	// ErrRelationNotFound is returned when a relation tuple cannot be found in the tenant.
	ErrRelationNotFound = wardenerr.ErrRelationNotFound

	// ErrResourceTypeNotFound is returned when a resource type cannot be found in the tenant.
	ErrResourceTypeNotFound = wardenerr.ErrResourceTypeNotFound

	// ErrCheckLogNotFound is returned when a check log entry cannot be found in the tenant.
	ErrCheckLogNotFound = wardenerr.ErrCheckLogNotFound

	// ErrSystemRoleImmutable is returned when a write would change or
	// delete a system role. role.CheckWritable returns an error that wraps
	// it. Defined in wardenerr so the role package can wrap it.
	ErrSystemRoleImmutable = wardenerr.ErrSystemRoleImmutable

	// ErrSystemPermissionImmutable is returned when a write would change or
	// delete a system permission. permission.CheckWritable returns an error
	// that wraps it. Defined in wardenerr so the permission package can wrap
	// it.
	ErrSystemPermissionImmutable = wardenerr.ErrSystemPermissionImmutable

	// ErrAlreadyExists is the common base error for entity-uniqueness
	// violations. Use errors.Is(err, ErrAlreadyExists) to match any of the
	// specialized ErrDuplicate* errors below.
	//
	// Defined in wardenerr so that low-level subpackages (e.g. store/memory)
	// can return typed duplicate errors without an import cycle.
	ErrAlreadyExists = wardenerr.ErrAlreadyExists

	// ErrDuplicateRole is returned when a role would violate the
	// (tenant_id, namespace_path, slug) uniqueness constraint.
	ErrDuplicateRole = wardenerr.ErrDuplicateRole

	// ErrDuplicatePermission is returned when a permission would violate the
	// (tenant_id, namespace_path, name) uniqueness constraint.
	ErrDuplicatePermission = wardenerr.ErrDuplicatePermission

	// ErrDuplicatePolicy is returned when a policy would violate the
	// (tenant_id, namespace_path, name) uniqueness constraint.
	ErrDuplicatePolicy = wardenerr.ErrDuplicatePolicy

	// ErrDuplicateResourceType is returned when a resource type would violate
	// the (tenant_id, namespace_path, name) uniqueness constraint.
	ErrDuplicateResourceType = wardenerr.ErrDuplicateResourceType

	// ErrDuplicateAssignment is returned when a role is already assigned to a
	// subject within the same scope. Wraps ErrAlreadyExists.
	ErrDuplicateAssignment = wardenerr.ErrDuplicateAssignment

	// ErrDuplicateRelation is returned when a relation tuple already exists.
	// Wraps ErrAlreadyExists.
	ErrDuplicateRelation = wardenerr.ErrDuplicateRelation

	// ErrCyclicRoleInheritance is returned when a role's parent would make
	// the role its own ancestor. role.CheckParent returns an error that
	// wraps it. Defined in wardenerr so the role package can wrap it.
	ErrCyclicRoleInheritance = wardenerr.ErrCyclicRoleInheritance

	// ErrMaxMembersExceeded is returned when a role's member limit is reached.
	ErrMaxMembersExceeded = errors.New("warden: role max members exceeded")

	// ErrInvalidCondition is returned when a policy condition is malformed.
	ErrInvalidCondition = errors.New("warden: invalid policy condition")

	// ErrGraphDepthExceeded is returned when the relation graph walk exceeds max depth.
	ErrGraphDepthExceeded = errors.New("warden: relation graph depth exceeded")

	// ErrTenantRequired is returned by Check when the resolved scope has no
	// tenant ID and Config.RequireTenant is true (the default), and by
	// RunTenantMaintenance when it is given no tenant.
	ErrTenantRequired = wardenerr.ErrTenantRequired

	// ErrGraphBudgetExceeded is returned when a ReBAC graph traversal
	// exceeds its configured visited-node or fan-out budget.
	ErrGraphBudgetExceeded = errors.New("warden: graph traversal budget exceeded")

	// ErrUnauthenticated is returned when a check cannot proceed because no
	// subject/actor identity is available.
	ErrUnauthenticated = errors.New("warden: unauthenticated")
)
View Source
var SystemActor = Actor{Kind: "system", ID: "warden", Via: "system"}

SystemActor identifies warden itself as the actor, used for internal mutations that are not attributable to a caller (maintenance jobs, etc).

Functions

func AncestorNamespaces

func AncestorNamespaces(path string) []string

AncestorNamespaces returns the path itself and every ancestor up to the tenant root, ordered from most-specific to most-general. The tenant root (empty string) is always the last element.

Examples:

AncestorNamespaces("")                   → [""]
AncestorNamespaces("eng")                → ["eng", ""]
AncestorNamespaces("eng/platform")       → ["eng/platform", "eng", ""]
AncestorNamespaces("eng/platform/sre")   → ["eng/platform/sre", "eng/platform", "eng", ""]

Used by the engine to resolve roles, permissions, policies, and resource types across the namespace ancestor chain (cascading scope inheritance).

func IsAncestorOrSelf

func IsAncestorOrSelf(ancestor, path string) bool

IsAncestorOrSelf reports whether ancestor is path itself or a strict ancestor of path. Both must be valid namespace paths (or empty string).

func JoinNamespace

func JoinNamespace(parent, child string) string

JoinNamespace concatenates two namespace paths with a "/" separator, dropping empties on either side. Used by the DSL parser to build absolute paths from nested namespace blocks.

func NamespaceFromContext

func NamespaceFromContext(ctx context.Context) string

NamespaceFromContext extracts the namespace path from the given context. Returns the empty string (tenant root) if not set.

func PolicySelectsSubject added in v1.7.0

func PolicySelectsSubject(pol *policy.Policy, kind SubjectKind, subjectID string, roleSlugs []string) bool

PolicySelectsSubject reports whether a policy's subject matchers select a subject holding roleSlugs. It is the evaluator's own test: an empty Subjects list selects everyone; otherwise any matcher whose non-empty Kind, ID and Role all agree. Selecting is not applying: the policy's actions, resources, window and conditions still decide each check.

func ScopeFromContext

func ScopeFromContext(ctx context.Context) (appID, tenantID string)

ScopeFromContext extracts the tenant scope from the given context. Returns appID and tenantID using the standard resolution chain: explicit WithTenant > forge.Scope > empty.

func ValidateNamespacePath

func ValidateNamespacePath(path string, maxDepth int) error

ValidateNamespacePath checks that a path is well-formed. Empty string (the tenant root) is always valid. Otherwise the path is "/"-separated, each segment matches namespaceSegmentRegex, no reserved segment appears, and depth ≤ maxDepth (or MaxNamespaceDepth if 0).

func WithActor added in v1.7.0

func WithActor(ctx context.Context, a Actor) context.Context

WithActor returns a context carrying an explicit Actor. Takes priority over any actor inferred from ActorFromContext's fallback chain.

func WithNamespace

func WithNamespace(ctx context.Context, namespacePath string) context.Context

WithNamespace returns a context with the given namespace path overlaid on top of the existing tenant scope. Pass empty string for the tenant root.

Combine with WithTenant when needed:

ctx := warden.WithTenant(context.Background(), "app1", "t1")
ctx  = warden.WithNamespace(ctx, "engineering/platform")

func WithRequestIP added in v1.7.0

func WithRequestIP(ctx context.Context, ip string) context.Context

WithRequestIP returns a context carrying the caller's IP address, used to populate CheckLog entries (checklog.Entry.RequestIP). Typically set once per inbound request by the HTTP/gRPC middleware layer.

func WithTenant

func WithTenant(ctx context.Context, appID, tenantID string) context.Context

WithTenant returns a context with the given app and tenant IDs. Use this for standalone mode (without Forge).

Types

type Action

type Action struct {
	Name string `json:"name"`
}

Action represents what the subject wants to do.

type Actor added in v1.7.0

type Actor struct {
	Kind string `json:"kind"`          // "user", "api_key", "service", "system"
	ID   string `json:"id"`            //
	Via  string `json:"via,omitempty"` // "forge", "declarative", "cli", ""
}

Actor identifies who or what caused a mutation, for audit trails and the plugin Audit hook. It is deliberately small and serializable so it can travel through plugin.Event.Actor (typed as `any` there to avoid an import cycle).

func ActorFromContext added in v1.7.0

func ActorFromContext(ctx context.Context) (Actor, bool)

ActorFromContext returns the Actor for the current request.

Resolution order:

  1. An explicit Actor set via WithActor.
  2. log.UserIDFromContext(ctx), wrapped as {Kind: "user", ID: uid, Via: "forge"}.
  3. Not found: the zero Actor and false.

type Cache

type Cache interface {
	// Get returns a cached check result, if available.
	Get(ctx context.Context, tenantID, namespacePath string, req *CheckRequest) (*CheckResult, bool)

	// Set stores a check result in the cache.
	Set(ctx context.Context, tenantID, namespacePath string, req *CheckRequest, result *CheckResult)

	// InvalidateTenant removes all cached results for a tenant.
	InvalidateTenant(ctx context.Context, tenantID string)

	// InvalidateSubject removes all cached results for a specific subject.
	InvalidateSubject(ctx context.Context, tenantID string, subjectKind SubjectKind, subjectID string)

	// Clear removes every cached entry across every tenant. Used by
	// maintenance and by invalidation paths that cannot be scoped to a
	// single tenant (e.g. a delete hook that only carries a bare ID).
	Clear(ctx context.Context)
}

Cache provides caching for authorization check results.

Get and Set take the resolved tenant ID and namespace path explicitly (rather than trusting CheckRequest.NamespacePath, which callers may leave unset when they rely on context-derived scope) so the cache key always reflects the namespace the check actually ran against.

type CallOption

type CallOption func(*callOptions)

CallOption is a functional option applied per-call on Check, Enforce, and CanI. It is distinct from Option, which configures the Engine at construction time.

func WithCallAppID

func WithCallAppID(appID string) CallOption

WithCallAppID overrides the app ID for this single call.

func WithCallDryRun added in v1.7.0

func WithCallDryRun() CallOption

WithCallDryRun evaluates the check without its externally visible side effects: the result cache is neither read nor written, no check log entry is enqueued, and no plugin hooks fire (neither OnBeforeCheck nor OnAfterCheck, on any path including a failed evaluation).

It exists for callers that ask "what would this decide" rather than "decide this", the dashboard playground above all. Without it, pressing a playground button writes an audit entry indistinguishable from production traffic, fires PolicyObligationFired at whatever is listening, and serves the second press from cache, which skips evaluation entirely and reports a cache lookup as the evaluation time.

Internal metrics are not suppressed: CheckEvaluated still records the decision and latency, and a failed evaluation still counts as a store error. Those are operational counters, not externally observable side effects, and a dry run should still be visible to whoever watches the engine's own health.

The decision itself is identical. Only the side effects above are suppressed.

func WithCallNamespacePath

func WithCallNamespacePath(namespacePath string) CallOption

WithCallNamespacePath overrides the namespace path for this single call. Pass empty string to scope the call to the tenant root.

func WithCallTenantID

func WithCallTenantID(tenantID string) CallOption

WithCallTenantID overrides the tenant ID for this single call. It takes precedence over context-derived scope and CheckRequest.TenantID.

type CheckLogLoss added in v1.7.0

type CheckLogLoss struct {
	// QueueFull counts entries dropped because the writer's bounded queue
	// was full, or because the writer had already stopped.
	QueueFull uint64
	// WriteFailed counts entries the store refused to write.
	WriteFailed uint64
	// Since is when the writer started counting: engine construction.
	Since time.Time
}

CheckLogLoss counts checks the engine ran whose entries may not have been recorded. Failed checks count too, because a failed check's entry goes through the same writer. A write that times out after the store committed also counts as failed.

type CheckRequest

type CheckRequest struct {
	Subject       Subject        `json:"subject"`
	Action        Action         `json:"action"`
	Resource      Resource       `json:"resource"`
	Context       map[string]any `json:"context,omitempty"`
	TenantID      string         `json:"tenant_id,omitempty"`      // Optional: overrides context-derived tenant.
	NamespacePath string         `json:"namespace_path,omitempty"` // Optional: overrides context-derived namespace.
}

CheckRequest is the input to an authorization check.

type CheckResult

type CheckResult struct {
	Allowed bool `json:"allowed"`

	Decision    Decision    `json:"decision"`
	Reason      string      `json:"reason,omitempty"`
	MatchedBy   []MatchInfo `json:"matched_by,omitempty"`
	Obligations []string    `json:"obligations,omitempty"`
	EvalTimeNs  int64       `json:"eval_time_ns"`
	// contains filtered or unexported fields
}

CheckResult is the outcome of an authorization check.

Obligations is the list of obligation names from PBAC policies that matched this check (allow OR deny). Obligations are side-effect signals — names of actions the calling system should perform (audit-log, require-mfa, notify-security, etc.). They don't change the Allowed/Decision outcome. The engine also fires the PolicyObligationFired plugin hook per obligation, so existing plugin pipelines (Chronicle audit, dispatchers) get them automatically.

type Config

type Config struct {
	// MaxGraphDepth is the maximum depth for ReBAC graph traversal.
	// Defaults to 10.
	MaxGraphDepth int `json:"max_graph_depth,omitempty"`

	// CacheTTL is the time-to-live for cached check results. When >0 and no
	// Cache was supplied via WithCache, the engine builds a memory cache
	// (NewMemoryCache) from CacheTTL and CacheMaxSize. Zero means no
	// caching unless a Cache was set explicitly.
	CacheTTL time.Duration `json:"cache_ttl,omitempty"`

	// CacheMaxSize bounds the number of entries the auto-built memory cache
	// holds per shard set. Defaults to 10000. Ignored when a Cache is
	// supplied explicitly via WithCache.
	CacheMaxSize int `json:"cache_max_size,omitempty"`

	// EnableRBAC enables role-based access control evaluation.
	// Defaults to true.
	EnableRBAC *bool `json:"enable_rbac,omitempty"`

	// EnableABAC enables attribute-based access control evaluation.
	// Defaults to true.
	EnableABAC *bool `json:"enable_abac,omitempty"`

	// EnableReBAC enables relationship-based access control evaluation.
	// Defaults to true.
	EnableReBAC *bool `json:"enable_rebac,omitempty"`

	// EnableCheckLog enables writing authorization check results to the
	// check log store. Defaults to true.
	EnableCheckLog *bool `json:"enable_check_log,omitempty"`

	// RequireTenant, when true (the default), makes Check return
	// ErrTenantRequired whenever the resolved scope has no tenant ID.
	// Set to false for single-tenant / standalone deployments that never
	// call WithTenant.
	RequireTenant *bool `json:"require_tenant,omitempty"`

	// MaxGraphVisited caps the number of distinct nodes a ReBAC graph walk
	// may visit before it aborts with ErrGraphBudgetExceeded. Defaults to
	// 5000.
	MaxGraphVisited int `json:"max_graph_visited,omitempty"`

	// MaxGraphFanout caps the number of subjects fetched per hop during a
	// ReBAC graph walk (passed through to ListRelationSubjects). Defaults
	// to 1000.
	MaxGraphFanout int `json:"max_graph_fanout,omitempty"`

	// EvaluateAllModels, when true, always evaluates RBAC, ReBAC and ABAC
	// even after an earlier model already allowed the request. Defaults to
	// false: ReBAC is skipped once RBAC already allowed.
	EvaluateAllModels bool `json:"evaluate_all_models,omitempty"`

	// CheckLogQueueSize bounds the check-log writer's internal channel.
	// Defaults to 4096. When full, new entries are dropped rather than
	// blocking Check.
	CheckLogQueueSize int `json:"check_log_queue_size,omitempty"`

	// CheckLogRetention is how long check log entries are kept before
	// RunMaintenance purges them. Defaults to 90 days. The engine reads 0 or
	// any negative value as no purging. The Forge extension reads 0 as unset
	// and fills in a default, so there only a negative value turns it off.
	CheckLogRetention time.Duration `json:"check_log_retention,omitempty"`

	// MaintenanceInterval is how often Engine.Start runs RunMaintenance in
	// the background. Defaults to 1 hour. The engine reads 0 or any negative
	// value as no loop. The Forge extension reads 0 as unset and fills in a
	// default, so there only a negative value turns it off.
	MaintenanceInterval time.Duration `json:"maintenance_interval,omitempty"`

	// MaxBatchChecks bounds the number of checks a batch-check API call may
	// request in one call. Defaults to 100.
	MaxBatchChecks int `json:"max_batch_checks,omitempty"`
}

Config holds configuration for the Warden engine.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with sensible defaults.

func (Config) Validate added in v1.7.0

func (c Config) Validate() error

Validate checks the configuration for internally inconsistent or unsafe values: negative counts, a negative cache TTL, a cache TTL over 24h, and a graph depth over 64. A negative CheckLogRetention or MaintenanceInterval is allowed: it switches purging or the maintenance loop off.

type Decision

type Decision string

Decision is the authorization outcome.

const (
	// DecisionAllow means the request is permitted.
	DecisionAllow Decision = "allow"

	// DecisionDeny means the request is denied (generic).
	DecisionDeny Decision = "deny"

	// DecisionDenyExplicit means an explicit deny policy matched.
	DecisionDenyExplicit Decision = "deny_explicit"

	// DecisionDenyDefault means no matching allow rule was found.
	DecisionDenyDefault Decision = "deny_default"

	// DecisionDenyNoRoles means the subject has no roles assigned.
	DecisionDenyNoRoles Decision = "deny_no_roles"

	// DecisionDenyNoPerms means no role grants the required permission.
	DecisionDenyNoPerms Decision = "deny_no_perms"

	// DecisionDenyCondition means an ABAC condition blocked the request.
	DecisionDenyCondition Decision = "deny_condition"

	// DecisionDenyRelation means no matching relation was found.
	DecisionDenyRelation Decision = "deny_relation"

	// DecisionError is never the Decision of a CheckResult. It's what the
	// check log and Metrics.CheckEvaluated record for a check whose
	// evaluation failed, the case where Check returns an error.
	DecisionError Decision = "error"
)

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the central authorization engine. It coordinates RBAC, ReBAC, and ABAC evaluation, manages the store, and fires extension hooks.

func NewEngine

func NewEngine(opts ...Option) (*Engine, error)

NewEngine creates a new Warden engine with the given options.

func (*Engine) CanI

func (e *Engine) CanI(ctx context.Context, subjectKind SubjectKind, subjectID, action, resourceType, resourceID string, opts ...CallOption) (bool, error)

CanI is a shorthand for a simple authorization check. Optional CallOption values override scope for this single call.

func (*Engine) Check

func (e *Engine) Check(ctx context.Context, req *CheckRequest, opts ...CallOption) (*CheckResult, error)

Check performs an authorization check. This is the hot path. Optional CallOption values override scope for this single call.

Each call is one "check permission" span, a child of whatever span ctx carries.

func (*Engine) CheckLogLoss added in v1.7.0

func (e *Engine) CheckLogLoss() (CheckLogLoss, bool)

CheckLogLoss reports how many checks the engine ran whose entries may not have been recorded since this engine started. Failed checks count too, because a failed check's entry goes through the same writer. A write that times out after the store committed also counts as failed. ok is false when check logging is off.

func (*Engine) Config

func (e *Engine) Config() Config

Config returns the engine configuration.

func (*Engine) Enforce

func (e *Engine) Enforce(ctx context.Context, req *CheckRequest, opts ...CallOption) error

Enforce returns an error if the authorization check is denied. Optional CallOption values override scope for this single call.

func (*Engine) ExpandRelation added in v1.7.0

func (e *Engine) ExpandRelation(ctx context.Context, objectType, objectID, rel string, opts ...CallOption) (*Expansion, error)

ExpandRelation walks the relation graph from objectType:objectID#rel as Check's walker does, without a target, using the configured graph budget and the call's tenant and namespace (relations cascade from ancestors). It writes nothing.

It records no walker metrics (GraphNodesVisited, GraphBudgetExceeded): those describe the walks checks make, and an expansion is someone looking. A store failure still counts under StoreError("graph_expand"), as SubjectRoles counts its own; a cancelled context does not.

func (*Engine) Explain added in v1.7.0

func (e *Engine) Explain(ctx context.Context, req *CheckRequest, opts ...CallOption) (*Explanation, error)

Explain evaluates req exactly as Check does, as a dry run, and reports each model's part. It never reads or fills the cache, fires no hooks and writes no check log. A request Check would refuse before evaluating (a missing field, a missing tenant) returns that error; a model's store failure is reported in the Explanation instead. As in a dry-run Check, a store failure still increments Metrics.StoreError and a failed permission expression still logs its warning.

func (*Engine) Health

func (e *Engine) Health(ctx context.Context) error

Health checks the health of the engine by pinging its store.

func (*Engine) InvalidateSubject added in v1.7.0

func (e *Engine) InvalidateSubject(ctx context.Context, tenantID string, subjectKind SubjectKind, subjectID string)

InvalidateSubject clears every cached Check result for one subject. A no-op when no Cache is configured.

func (*Engine) InvalidateTenant added in v1.7.0

func (e *Engine) InvalidateTenant(ctx context.Context, tenantID string)

InvalidateTenant clears every cached Check result for a tenant. A no-op when no Cache is configured.

func (*Engine) Plugins

func (e *Engine) Plugins() *plugin.Registry

Plugins returns the plugin registry (may be nil).

func (*Engine) RunMaintenance added in v1.7.0

func (e *Engine) RunMaintenance(ctx context.Context) (MaintenanceReport, error)

RunMaintenance purges role assignments that have expired and, when Config.CheckLogRetention > 0 (0 or negative is off), check log entries older than the retention window, in every tenant. When either purge actually removed rows and a Cache is configured, it also does a full Cache.Clear: Cache has no "list every tenant" operation, so a targeted per-tenant invalidation isn't possible here; a maintenance run is infrequent enough that a full flush is cheap relative to serving one stale permission.

Each call is a "run maintenance" span, a child of whatever span ctx carries. The background loop StartMaintenance runs starts each one as a root instead.

func (*Engine) RunTenantMaintenance added in v1.7.0

func (e *Engine) RunTenantMaintenance(ctx context.Context, tenantID string) (MaintenanceReport, error)

RunTenantMaintenance is RunMaintenance for one tenant. It purges that tenant's role assignments that have expired and, when Config.CheckLogRetention > 0 (0 or negative is off), that tenant's check log entries older than the retention window. No other tenant's rows are touched. When either purge removed rows and a Cache is configured, it invalidates that tenant's cached decisions (Cache.InvalidateTenant), not the whole cache.

An empty tenantID returns ErrTenantRequired before anything is deleted: the stores read an empty tenant filter as every tenant, so empty is never passed through as "all of them". RunMaintenance is the engine-wide pass.

Each call is a "run maintenance" span with scope "tenant", a child of whatever span ctx carries. The tenant ID stays off the span.

func (*Engine) SetExpressionEvaluator

func (e *Engine) SetExpressionEvaluator(ev ExpressionEvaluator)

SetExpressionEvaluator overrides the resource-type expression evaluator at runtime. Used by the extension to wire dsl.NewEngineEvaluator after engine construction, since the evaluator depends on the engine's store.

func (*Engine) Start

func (e *Engine) Start(_ context.Context) error

Start performs startup initialization: logs a summary of the engine's check-log/cache/maintenance configuration and, when Config.MaintenanceInterval > 0 (0 or negative is off), starts the background maintenance loop (RunMaintenance on that interval) that Stop later cancels.

func (*Engine) StartMaintenance added in v1.7.0

func (e *Engine) StartMaintenance(ctx context.Context)

StartMaintenance runs RunMaintenance on Config.MaintenanceInterval until ctx is cancelled. A no-op when MaintenanceInterval is 0 or negative, the values that switch the loop off. Engine.Start calls this automatically; it is also safe to call directly for full control over the maintenance context's lifetime. Every run is a root "run maintenance" span, even when ctx carries a span (a Forge boot span, say), so a run never lands in some long-finished trace.

func (*Engine) Stop

func (e *Engine) Stop(ctx context.Context) error

Stop performs graceful shutdown: cancels the maintenance loop, drains the check log writer (best-effort, bounded by ctx), and notifies Shutdown plugin hooks.

func (*Engine) Store

func (e *Engine) Store() store.Store

Store returns the underlying composite store.

func (*Engine) SubjectRoles added in v1.7.0

func (e *Engine) SubjectRoles(ctx context.Context, kind SubjectKind, subjectID string, opts ...CallOption) (direct, all []*role.Role, err error)

SubjectRoles reports the roles a subject holds at a namespace, as Check resolves them for a check on no particular resource: the roles assigned at that namespace or an ancestor (expired assignments excluded), then every parent reached through ParentSlug. direct lists the assigned roles; all lists direct plus inherited, in the order Check resolves them. Resource-scoped assignments are not included: they grant only for a check on their own resource.

type Evaluator

type Evaluator interface {
	Evaluate(ctx context.Context, policies []*policy.Policy, req *CheckRequest, roleSlugs []string) (*CheckResult, error)
}

Evaluator evaluates ABAC/PBAC policies against a check request.

roleSlugs is the set of role slugs (direct and inherited) held by the requesting subject, resolved by the engine before evaluation. It powers role-scoped policy.SubjectMatch.Role matching: a policy naming {Role: "editor"} only applies to subjects holding a role with that slug.

func DefaultEvaluator

func DefaultEvaluator() Evaluator

DefaultEvaluator returns the built-in condition evaluator backed by the system wall clock.

func NewConditionEvaluator

func NewConditionEvaluator(now func() time.Time) Evaluator

NewConditionEvaluator returns an Evaluator that uses the supplied clock for PBAC time-window evaluation (NotBefore / NotAfter). Pass time.Now for production; tests can pass a fixed-time function.

type ExpandEdge added in v1.7.0

type ExpandEdge struct {
	From, To      int // indexes into Nodes
	NamespacePath string
}

ExpandEdge is one tuple: from the object node to its subject node.

type ExpandNode added in v1.7.0

type ExpandNode struct {
	Type, ID, Relation string // Relation is "" for a single subject
	Depth              int
	// Walked is true when the walk took this node off its queue, listed
	// its tuples and went through them, so its edges are all of them. It
	// is false for a node reached but not walked: one still queued when a
	// depth or visited stop ended the walk, or the node whose hop tripped
	// the fanout limit (its tuples were listed but not walked, so it has
	// no edges). A single subject is never walked, so it is always false
	// there. A walked subject set with no tuples is Walked with no edges.
	Walked bool
}

ExpandNode is one (objectType, objectID, relation) the walk visited, or a single subject it reached.

type ExpandStop added in v1.7.0

type ExpandStop string

ExpandStop says why an expansion ended.

const (
	ExpandComplete ExpandStop = "complete" // every reachable tuple was visited
	ExpandDepth    ExpandStop = "depth"    // a node beyond MaxGraphDepth was reached
	ExpandVisited  ExpandStop = "visited"  // more than MaxGraphVisited distinct nodes
	ExpandFanout   ExpandStop = "fanout"   // one hop returned at least MaxGraphFanout tuples
)

type Expansion added in v1.7.0

type Expansion struct {
	Nodes []ExpandNode
	Edges []ExpandEdge
	Stop  ExpandStop
	// Limit is the effective value of the limit that stopped the walk,
	// the one named by Stop: the walker's own value, which is the
	// walker's default when Config holds 0 for it. 0 for ExpandComplete.
	Limit int
	// Parent maps a node index to the node it was first reached from (-1
	// for the root), in the walker's BFS order, so PathTo reproduces the
	// path Walk would report.
	Parent []int
	// ExactWalk is true when this is the walk Check makes: the engine's
	// graph walker is the built-in BFS walker, whether NewEngine built it
	// or WithGraphWalker installed one from NewGraphWalker or
	// DefaultGraphWalker. It is false when WithGraphWalker installed
	// another kind of walker: Check walks with that one, and the
	// expansion falls back to the built-in BFS walk with Config's budget,
	// so neither its shape nor PathTo is guaranteed to match Check.
	ExactWalk bool
}

Expansion is the walk Check would perform from one object and relation, with no target subject, so it visits everything reachable within the engine's budget.

func (*Expansion) PathTo added in v1.7.0

func (x *Expansion) PathTo(subjectType, subjectID string) string

PathTo returns the walk's path from the root to the first node for subjectType:subjectID, formatted exactly as the walker's reconstructPath, or "" when the expansion never reached it.

Edges are in the order the walk saw their tuples, so the first edge into a subjectType:subjectID node (a single subject or a subject set: Walk matches either) is the tuple Walk would have stopped on.

type Explanation added in v1.7.0

type Explanation struct {
	// Result is what Check would have returned. Nil when a model failed.
	Result *CheckResult
	// Err is the error Check would have returned when a model failed.
	Err               string
	RBAC, ReBAC, ABAC Lane
}

Explanation is an explained check: the merged result Check would return, and each model's own part in it, from one evaluation.

type ExpressionEvaluator

type ExpressionEvaluator interface {
	EvalPermission(ctx context.Context, tenantID, namespacePath, resourceType, permName, subjectKind, subjectID, resourceID string) (matched bool, err error)
}

ExpressionEvaluator is an optional engine hook that evaluates resource-type permission expressions (the SpiceDB-style `viewer or editor or parent->read` expressions stored on ResourceType.Permissions[].Expression).

The DSL package implements this; wire it via WithExpressionEvaluator. When nil, resource-type expressions are inert (the relation graph walker still handles direct + transitive relations).

type GraphDepthMetrics added in v1.8.0

type GraphDepthMetrics interface {
	GraphDepthExceeded()
}

GraphDepthMetrics is an optional addition to Metrics. When the engine's Metrics also implements it, GraphDepthExceeded is called each time a ReBAC graph walk stops at MaxGraphDepth. It sits apart from Metrics so an implementation written before it existed still compiles. NoopMetrics and the WithMetricsSink adapter implement it.

type GraphWalker

type GraphWalker interface {
	Walk(ctx context.Context, relStore relation.Store, tenantID, namespacePath string, req *CheckRequest) (allowed bool, path string, err error)
}

GraphWalker traverses the relation graph for ReBAC evaluation.

Walk is invoked with the namespace path of the request. Relations cascade like roles and policies: the walker follows tuples in the request namespace and every ancestor namespace (see AncestorNamespaces), so a relationship granted at a parent namespace is honored for descendant-scoped checks.

func DefaultGraphWalker

func DefaultGraphWalker(maxDepth int) GraphWalker

DefaultGraphWalker returns a BFS graph walker with the given max depth and the given metrics sink. Fan-out and visited-node budgets default to 1000 and 5000 respectively; use NewGraphWalker to configure them from Config.

func NewGraphWalker added in v1.7.0

func NewGraphWalker(maxDepth, maxVisited, maxFanout int, metrics Metrics) GraphWalker

NewGraphWalker returns a BFS graph walker bounded by maxDepth (hop count), maxVisited (distinct nodes visited before aborting), and maxFanout (subjects fetched per hop, passed through to ListRelationSubjects). Zero values fall back to sane defaults. A nil metrics sink uses NoopMetrics.

type ID

type ID = id.ID

ID is the primary identifier type for all Warden entities.

type Lane added in v1.7.0

type Lane struct {
	State LaneState
	// Result is the model's own result: set for LaneAllow and LaneDeny, and
	// for LaneNoMatch when the model returned one.
	Result *CheckResult
	// Err is the store error, for LaneError.
	Err string
	// WalkTruncated is set on the ReBAC lane when the graph walk stopped at
	// MaxGraphDepth or MaxGraphVisited before it could answer.
	WalkTruncated bool
	// ExpressionErr is set on the ReBAC lane when the resource type's
	// permission expression failed and was treated as no match.
	ExpressionErr string
}

Lane is one model's part in an explained check.

type LaneState added in v1.7.0

type LaneState string

LaneState is what one authorization model did during an explained check.

const (
	// LaneAllow: the model granted the request.
	LaneAllow LaneState = "allow"
	// LaneDeny: an explicit deny policy matched. ABAC only.
	LaneDeny LaneState = "deny"
	// LaneNoMatch: the model ran and granted nothing. Result carries its
	// reason when it gave one (RBAC and ReBAC always do; ABAC with no
	// matching policy has no result at all).
	LaneNoMatch LaneState = "noMatch"
	// LaneSkipped: ReBAC only. RBAC already allowed and EvaluateAllModels
	// is off, so the graph walk did not run.
	LaneSkipped LaneState = "skipped"
	// LaneDisabled: the model is turned off in Config.
	LaneDisabled LaneState = "disabled"
	// LaneError: the model's store read failed. Check returns an error here.
	LaneError LaneState = "error"
	// LaneNotEvaluated: an earlier model failed, so this one never ran, as
	// in Check.
	LaneNotEvaluated LaneState = "notEvaluated"
)

type MaintenanceReport added in v1.7.0

type MaintenanceReport struct {
	AssignmentsPurged int64
	CheckLogsPurged   int64
}

MaintenanceReport summarizes one RunMaintenance or RunTenantMaintenance pass.

type MatchInfo

type MatchInfo struct {
	Source string `json:"source"` // "rbac", "abac", "rebac"
	RuleID string `json:"rule_id,omitempty"`
	Detail string `json:"detail,omitempty"`
}

MatchInfo describes what rule matched during evaluation.

type MemoryCache added in v1.7.0

type MemoryCache struct {
	// contains filtered or unexported fields
}

MemoryCache is the engine's built-in Cache implementation: a sharded, bounded, TTL'd LRU with O(1)-ish tenant/subject invalidation. It lives in the root package (rather than the cache/ subpackage) because the engine must be able to construct it directly from Config.CacheTTL / Config.CacheMaxSize without an import cycle: cache/ imports warden.

func NewMemoryCache added in v1.7.0

func NewMemoryCache(opts ...MemoryCacheOption) *MemoryCache

NewMemoryCache constructs the engine's built-in memory cache.

func (*MemoryCache) Clear added in v1.7.0

func (m *MemoryCache) Clear(_ context.Context)

Clear removes every cached entry across every tenant.

func (*MemoryCache) Get added in v1.7.0

func (m *MemoryCache) Get(_ context.Context, tenantID, namespacePath string, req *CheckRequest) (*CheckResult, bool)

Get returns a cached check result, if present and not expired.

func (*MemoryCache) InvalidateSubject added in v1.7.0

func (m *MemoryCache) InvalidateSubject(_ context.Context, tenantID string, subjectKind SubjectKind, subjectID string)

InvalidateSubject removes all cached results for a specific subject, across every namespace it was checked in.

func (*MemoryCache) InvalidateTenant added in v1.7.0

func (m *MemoryCache) InvalidateTenant(_ context.Context, tenantID string)

InvalidateTenant removes all cached results for a tenant. A tenant's entries always live in a single shard (shard selection is a pure function of tenantID), so this only ever locks one shard.

func (*MemoryCache) Set added in v1.7.0

func (m *MemoryCache) Set(_ context.Context, tenantID, namespacePath string, req *CheckRequest, result *CheckResult)

Set stores a check result in the cache.

type MemoryCacheOption added in v1.7.0

type MemoryCacheOption func(*memoryCacheConfig)

MemoryCacheOption configures a MemoryCache.

func WithCacheMaxSize added in v1.7.0

func WithCacheMaxSize(n int) MemoryCacheOption

WithCacheMaxSize sets the maximum number of entries the cache holds in total, across every shard. Defaults to 10000.

func WithCacheTTL added in v1.7.0

func WithCacheTTL(ttl time.Duration) MemoryCacheOption

WithCacheTTL sets the cache entry time-to-live. Defaults to 5 minutes.

type Metrics added in v1.7.0

type Metrics interface {
	// CheckEvaluated is called once per Check that got as far as
	// evaluating, whether it hit the cache or was fully evaluated. A check
	// whose evaluation failed reports DecisionError with source "none". A
	// request Check refuses before evaluating (a missing field, a missing
	// tenant) reports nothing.
	CheckEvaluated(decision Decision, source string, dur time.Duration, cached bool)

	// StoreError is called when a store call the engine makes returns an
	// error: during evaluation, while writing the check log, or in a
	// maintenance run. op names the failing operation in snake case (e.g.
	// "list_roles_for_subject", "create_check_log", "purge_check_logs").
	StoreError(op string)

	// CheckLogDropped is called when the check log writer's queue is full
	// and an entry is dropped rather than written.
	CheckLogDropped()

	// CheckLogWritten is called after a batch of check log entries is
	// flushed to the store. n is the number of entries written; one that
	// failed is reported through StoreError("create_check_log") instead.
	CheckLogWritten(n int)

	// HookError is called when a plugin hook panics or returns an error.
	HookError(hook, plugin string)

	// GraphNodesVisited is called after a ReBAC graph walk completes with
	// the number of nodes visited during that walk, including a walk that
	// stopped at a limit. A walk that failed reports nothing.
	GraphNodesVisited(n int)

	// GraphBudgetExceeded is called when a ReBAC graph walk is aborted
	// because it exceeded MaxGraphVisited or MaxGraphFanout.
	GraphBudgetExceeded()

	// AssignmentsPurged is called after a maintenance run purges expired
	// role assignments, with the number of rows removed.
	AssignmentsPurged(n int64)

	// CheckLogsPurged is called after a maintenance run purges old check
	// log entries, with the number of rows removed.
	CheckLogsPurged(n int64)

	// CacheInvalidated is called whenever the check cache is invalidated.
	// scope is "subject", "tenant", or "all" for a full clear.
	CacheInvalidated(scope string)
}

Metrics receives instrumentation events from the engine. Implement this interface to wire warden into your metrics backend (Prometheus, OpenTelemetry, statsd, ...). WithMetrics installs a custom implementation, and WithMetricsSink installs the built-in one over a go-utils collector such as forge's app.Metrics(). NoopMetrics is the default when none is configured.

type NoopMetrics added in v1.7.0

type NoopMetrics struct{}

NoopMetrics implements Metrics with no-ops. It is the default when no Metrics is configured via WithMetrics.

func (NoopMetrics) AssignmentsPurged added in v1.7.0

func (NoopMetrics) AssignmentsPurged(int64)

func (NoopMetrics) CacheInvalidated added in v1.7.0

func (NoopMetrics) CacheInvalidated(string)

func (NoopMetrics) CheckEvaluated added in v1.7.0

func (NoopMetrics) CheckEvaluated(Decision, string, time.Duration, bool)

func (NoopMetrics) CheckLogDropped added in v1.7.0

func (NoopMetrics) CheckLogDropped()

func (NoopMetrics) CheckLogWritten added in v1.7.0

func (NoopMetrics) CheckLogWritten(int)

func (NoopMetrics) CheckLogsPurged added in v1.7.0

func (NoopMetrics) CheckLogsPurged(int64)

func (NoopMetrics) GraphBudgetExceeded added in v1.7.0

func (NoopMetrics) GraphBudgetExceeded()

func (NoopMetrics) GraphDepthExceeded added in v1.8.0

func (NoopMetrics) GraphDepthExceeded()

func (NoopMetrics) GraphNodesVisited added in v1.7.0

func (NoopMetrics) GraphNodesVisited(int)

func (NoopMetrics) HookError added in v1.7.0

func (NoopMetrics) HookError(string, string)

func (NoopMetrics) StoreError added in v1.7.0

func (NoopMetrics) StoreError(string)

type Option

type Option func(*Engine)

Option is a functional option for the Engine.

func WithCache

func WithCache(c Cache) Option

WithCache sets the check result cache.

func WithConfig

func WithConfig(c Config) Option

WithConfig sets the engine configuration.

func WithEvaluator

func WithEvaluator(ev Evaluator) Option

WithEvaluator sets the ABAC policy evaluator.

func WithExpressionEvaluator

func WithExpressionEvaluator(ev ExpressionEvaluator) Option

WithExpressionEvaluator sets the resource-type permission expression evaluator. The DSL package's NewEngineEvaluator is the canonical implementation. When unset, resource-type expressions are inert.

func WithGraphWalker

func WithGraphWalker(gw GraphWalker) Option

WithGraphWalker sets the ReBAC graph walker.

func WithLogger

func WithLogger(l log.Logger) Option

WithLogger sets the structured logger.

func WithMetrics added in v1.7.0

func WithMetrics(m Metrics) Option

WithMetrics sets the engine's metrics sink.

func WithMetricsSink added in v1.8.0

func WithMetricsSink(m gometrics.Metrics) Option

WithMetricsSink records the engine's metrics through a go-utils metrics collector, the type forge's app.Metrics() returns. It builds the same adapter the Forge extension installs, so an engine you construct yourself inside a Forge app reports the same series:

eng, err := warden.NewEngine(warden.WithStore(s), warden.WithMetricsSink(app.Metrics()))

A nil m leaves metrics off. WithMetricsSink and WithMetrics set the same sink, so the one passed last wins.

func WithPlugin

func WithPlugin(x plugin.Plugin) Option

WithPlugin registers a plugin with the engine.

func WithStore

func WithStore(s store.Store) Option

WithStore sets the composite store.

type Prefix

type Prefix = id.Prefix

Prefix identifies the entity type encoded in a TypeID.

type Resource

type Resource struct {
	Type       string         `json:"type"`
	ID         string         `json:"id"`
	Attributes map[string]any `json:"attributes,omitempty"`
}

Resource represents the target of an authorization check.

type Subject

type Subject struct {
	Kind       SubjectKind    `json:"kind"`
	ID         string         `json:"id"`
	Attributes map[string]any `json:"attributes,omitempty"`
}

Subject represents an actor in an authorization check.

type SubjectKind

type SubjectKind string

SubjectKind identifies the type of actor making an authorization request.

const (
	// SubjectUser represents a human user.
	SubjectUser SubjectKind = "user"

	// SubjectAPIKey represents an API key (e.g., from Keysmith).
	SubjectAPIKey SubjectKind = "api_key"

	// SubjectService represents a service-to-service caller.
	SubjectService SubjectKind = "service"

	// SubjectServiceAcct represents a service account.
	SubjectServiceAcct SubjectKind = "service_acct"
)

Directories

Path Synopsis
_examples
abac command
Example: attribute-based access control with conditions.
Example: attribute-based access control with conditions.
forge command
Example: Warden as a Forge extension with API routes.
Example: Warden as a Forge extension with API routes.
rbac command
Example: pure RBAC with role inheritance.
Example: pure RBAC with role inheritance.
rebac command
Example: Zanzibar-style ReBAC with transitive relations.
Example: Zanzibar-style ReBAC with transitive relations.
standalone command
Example: standalone Warden usage without Forge.
Example: standalone Warden usage without Forge.
standalone-embed command
Standalone-embed example — boots a Warden engine, applies an embedded .warden config tree, then runs a sample Check.
Standalone-embed example — boots a Warden engine, applies an embedded .warden config tree, then runs a sample Check.
Package api provides HTTP handlers for the Warden authorization engine.
Package api provides HTTP handlers for the Warden authorization engine.
Package assignment defines the Assignment entity (role→subject binding).
Package assignment defines the Assignment entity (role→subject binding).
Package cache provides caching implementations for Warden check results.
Package cache provides caching implementations for Warden check results.
Package checklog defines the check audit log Entry entity.
Package checklog defines the check audit log Entry entity.
cmd
internal/cli
Package cli holds shared helpers for the warden CLI binaries.
Package cli holds shared helpers for the warden CLI binaries.
warden command
Command warden is the CLI for the .warden DSL.
Command warden is the CLI for the .warden DSL.
warden-lsp command
Command warden-lsp is a Language Server Protocol (LSP) server for the `.warden` DSL.
Command warden-lsp is a Language Server Protocol (LSP) server for the `.warden` DSL.
Package dsl implements the .warden declarative configuration language.
Package dsl implements the .warden declarative configuration language.
Package extension provides a Forge extension entry point for Warden.
Package extension provides a Forge extension entry point for Warden.
contract
authz.go: the authorization decision in front of every dashboard intent.
authz.go: the authorization decision in front of every dashboard intent.
Package id defines TypeID-based identity types for all Warden entities.
Package id defines TypeID-based identity types for all Warden entities.
internal
telemetry
Package telemetry holds the small amount of tracing plumbing warden's packages share: the instrumentation scope, span start, and error marking that never copies an error message onto a span.
Package telemetry holds the small amount of tracing plumbing warden's packages share: the instrumentation scope, span start, and error marking that never copies an error message onto a span.
Package keysmithbridge lets Keysmith's warden_hook extension drive Warden.
Package keysmithbridge lets Keysmith's warden_hook extension drive Warden.
Package lsp implements the Warden Language Server Protocol server for the .warden DSL.
Package lsp implements the Warden Language Server Protocol server for the .warden DSL.
Package middleware provides HTTP authorization middleware for Warden.
Package middleware provides HTTP authorization middleware for Warden.
Package permission defines the Permission entity and its store interface.
Package permission defines the Permission entity and its store interface.
Package plugin defines the plugin system for Warden.
Package plugin defines the plugin system for Warden.
auditlog
Package auditlog provides an in-tree structured-log Audit plugin.
Package auditlog provides an in-tree structured-log Audit plugin.
Package policy defines the Policy entity used for ABAC and PBAC evaluation.
Package policy defines the Policy entity used for ABAC and PBAC evaluation.
Package relation defines the Tuple entity for ReBAC (Zanzibar-style relations).
Package relation defines the Tuple entity for ReBAC (Zanzibar-style relations).
Package resourcetype defines the ResourceType entity for ReBAC schema definitions.
Package resourcetype defines the ResourceType entity for ReBAC schema definitions.
Package role defines the Role entity and its store interface for RBAC.
Package role defines the Role entity and its store interface for RBAC.
Package store defines the aggregate persistence interface.
Package store defines the aggregate persistence interface.
contract
Package contract provides shared test contracts that every Warden store backend must satisfy.
Package contract provides shared test contracts that every Warden store backend must satisfy.
memory
Package memory provides an in-memory implementation of the Warden composite store.
Package memory provides an in-memory implementation of the Warden composite store.
mongo
Package mongo provides a MongoDB Store implementation for the Warden store.
Package mongo provides a MongoDB Store implementation for the Warden store.
postgres
Package postgres provides a PostgreSQL implementation of the Warden composite store using grove ORM with Go-based migrations.
Package postgres provides a PostgreSQL implementation of the Warden composite store using grove ORM with Go-based migrations.
sqlite
Package sqlite provides a SQLite Store implementation for embedded/edge use.
Package sqlite provides a SQLite Store implementation for embedded/edge use.
Package wardenerr holds shared sentinel errors used across warden subpackages.
Package wardenerr holds shared sentinel errors used across warden subpackages.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL