wardn

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

README

wardn

wardn

Simple, opinionated authorization for Go.

Define your roles, permissions and tenancy in one file. wardn stores them, checks every request,
stops privilege escalation and keeps an audit trail, inside your app, on the database you already run.

CI Release Go Reference License Go Stability Threats tested

Documentation · Quick start · Examples · Security · Changelog


Every product needs to answer the same question: is this user allowed to do this, here? Most teams answer it with a hand-built role system that grows a little more fragile with every feature. wardn replaces it with one small library:

  • Your model, not ours. wardn knows nothing about banks, schools or SaaS. Your product declares its own permissions, roles and scope tree: flat, one level, or as deep as it needs.
  • No new infrastructure. wardn is a library, not a server. It runs in your process and keeps its data in your PostgreSQL, MySQL, MariaDB, SQLite or MongoDB database.
  • Safe by default. Default deny. Deny always wins. Any error means "no". Error responses reveal nothing. None of this can be switched off.
organisation:acme                     ada  → org_admin      (everything in Acme)
 └── region:south
      └── branch:lagos                bayo → teller         (this branch only)
                                      tolu → loan_officer   (until Friday)

Contents

Quick start

1. Install (Go 1.26 or newer):

go get github.com/MogRex-Limited/wardn@latest

2. Describe your product in wardn.yaml:

scopes:
  organisation: {}
  branch: { parent: organisation }

permissions:
  customer:view: View customers
  loan:create:   Create loan applications
  loan:approve:  Approve loans

roles:
  org_admin:
    assignable_at: [organisation]
    allow: ["*"]
  teller:
    assignable_at: [branch]
    allow: [customer:view, loan:create]

3. Start wardn in main. It validates the policy and creates its tables:

w := wardn.MustNew(wardn.Config{
	File:  "wardn.yaml",
	Store: wardnsql.MustNew(db), // your existing *sql.DB
})

4. Protect your routes (net/http or chi):

r.Route("/branches/{branchID}", func(r chi.Router) {
	r.Use(w.ScopeFrom("branch", wardn.PathValue("branchID")))
	r.With(w.Require("loan:create")).Post("/loans", createLoan)
	r.With(w.Require("loan:approve")).Post("/loans/{id}/approve", approveLoan)
})

No user gets 401, no permission gets 403, and your handler never runs for either. A typo in a permission name stops the app at startup, with a "did you mean …?".

5. Prove it with a test. wardntest runs your real policy in memory:

func TestTellersCannotApproveLoans(t *testing.T) {
	w := wardntest.New(t, "wardn.yaml")
	req := httptest.NewRequest("POST", "/branches/lagos/loans/7/approve", nil)
	req = wardntest.AsRoleIn(t, w, req, wardn.NewScope("branch", "lagos"), "teller")

	rec := httptest.NewRecorder()
	newRouter(w).ServeHTTP(rec, req)
	if rec.Code != http.StatusForbidden {
		t.Fatalf("status %d, want 403", rec.Code)
	}
}

The full quick start also seeds the first administrator and covers authentication.

Features

Policy as one file Permissions, roles and scope hierarchy in YAML (or Go), validated with file, line and suggestions. wardn gen generates Go constants, so typos fail to compile. wardn check validates the file in CI.
Hierarchy and tenancy Roles assigned at a scope apply to everything below it. Parents are stored with Link or resolved from your own data with ParentOf. Tenants stay isolated.
Checks Require / RequireAny middleware, plus Can, CanAny, CanAll and Check in code. Every Decision says which role and rule decided it.
Custom roles Admins create roles at runtime for their own scope. The escalation guard stops anyone from creating, editing, assigning or revoking a role with permissions they don't hold.
Admin screen APIs Role lists, assignable roles, paginated members and permissions-of-user, ready for a "Manage access" page.
Time-limited access From and Until on any assignment. Access starts and stops on time with no cleanup job, and each expiry is recorded once.
Audit trail OnChange runs before every change and can veto it. Events marshal to stable JSON. OnDecision sees every check.
Caching Off by default. An opt-in cache, in memory or shared through Redis or Valkey, caches data, never decisions, with signed entries and instant invalidation on every instance.
Testing wardntest for your tests, AuditRoutes to catch unprotected routes, and conformance suites (storetest, cachetest) for your own store or cache.

Choosing the right tool

wardn Casbin OpenFGA / SpiceDB Home-grown
Built for Roles and tenancy inside a Go product General policy models, in many languages Sharing individual objects across millions of them (Zanzibar-style) A product's first few checks
Runs Inside your app, on your database Inside your app As a separate service Inside your app
Your team learns One YAML file Casbin's model language A modelling language and relationship tuples Nothing new, at first
Tenancy and hierarchy Built in Domains built in; scope trees are yours to build Built in, as relationships Yours to build
Admin-created roles that can't escalate Built in Yours to build Yours to build Rarely done
Time-limited access Built in Yours to build Supported Rarely done
Audit of every change Built in, and can block a change Yours to build Change history API Rarely done

Different tools for different jobs. wardn is built for roles and tenancy inside a Go product: no extra service, no new language, and the safety rules built in. OpenFGA and SpiceDB are built for sharing individual objects between people and groups at very large scale. They work well together: many products use wardn for who runs what in the organisation, and a relationship service for who can see which document. The Learn pages explain the models in depth.

Supported platforms

Supported Module
Go 1.26, 1.27 (the two releases with security support)
PostgreSQL 13+ github.com/MogRex-Limited/wardn/store/sql
MySQL 8.0+ github.com/MogRex-Limited/wardn/store/sql
MariaDB 10.6+ github.com/MogRex-Limited/wardn/store/sql
SQLite 3.24+ github.com/MogRex-Limited/wardn/store/sql
PostgreSQL through pgx 13+ github.com/MogRex-Limited/wardn/store/pgx (same tables as store/sql)
MongoDB 6.0+ github.com/MogRex-Limited/wardn/store/mongo
Redis / Valkey shared cache (optional) github.com/MogRex-Limited/wardn/cache/redis
Routers net/http, chi, and anything built on http.Handler core

The core module depends only on the standard library and YAML. The SQL store uses database/sql with the driver you already have, and pgx, MongoDB and Redis support live in their own modules, so your service pulls in only what it uses.

Examples

Four complete applications in examples/, built only on the public API, each with tests that walk through its recipe request by request:

App Shows
bank Organisations › regions › branches; tellers, managers and auditors; a custom approver role for one week; suspensions; maker-checker
saas Workspaces as tenants, customer-defined roles, per-project access
school Hierarchy from your own data with ParentOf; students see only their own grades
simple No tenancy at all: roles and permissions for one app
cd examples && go run ./bank    # then follow examples/bank/README.md

Performance

A check costs a few database reads. Measured on a bank with 1,000 branches and about 20,000 assignments:

Store One check 10 checks in one request With the cache
In memory 2–5 µs 26 µs —
SQLite ~0.1 ms 0.14 ms 25–40 µs
PostgreSQL, MySQL, MariaDB, MongoDB 0.7–1.1 ms 0.7–1.2 ms 25–45 µs

Database servers ran on the same machine. Run make bench or make bench-all to measure your own setup, and see the performance guide for tuning.

Security

A flaw in an authorization library is a flaw in every product that uses it, so wardn is tested the way an attacker thinks:

  • A threat model of 28 threats, each with its own TestThreat_ test.
  • A property test that checks about 130,000 decisions per run against an independent reference implementation, and another that attacks the escalation guard in random worlds.
  • Fault injection on every store, cache and SQL write path. Any failure must deny, never allow.
  • 58 deliberately broken security rules, every one caught by the test suite (make mutation), plus 8 fuzz targets. Both run every week.
  • An internal security review and an API review before v1.0. Every finding is fixed and has a regression test. There has been no external audit (decision D16).
  • Signed release tags. Release binaries of the CLI are built reproducibly and ship with checksums and Sigstore build provenance.

Found a vulnerability? Please don't open an issue. Follow the security policy to report it privately.

Stability

wardn follows semantic versioning. From v1.0.0, nothing in v1 will break your code, your data or the decisions wardn makes: Go API, policy files, JSON and audit event names, HTTP statuses and database schemas all stay compatible. Every change is checked automatically against the latest release (make api). Read the stability promise.

Documentation

The documentation site lives in website/: getting started, concepts, guides, recipes, reference and security. Run it locally:

make docs    # http://localhost:4321
Start here
Quick start Your first protected route in about ten minutes
Concepts Permissions, roles, scopes, assignments, and how a decision is made
Multi-tenancy Tenants, hierarchy, and "authorize first, then look up"
Custom roles A safe "Create role" screen for your admins
Design How wardn works and why, with every design decision

Contributing

Contributions follow CONTRIBUTING.md. Everything CI checks runs locally:

make hooks   # once: run every check before each push
make check   # tests, race detector, linters, vulnerabilities, dependency allowlist, API compatibility
make integration   # everything against real PostgreSQL, MySQL, MariaDB, MongoDB and Redis (Docker)

Maintainers: releases and repository settings are in docs/MAINTAINING.md.

Author

wardn was designed and built by Abakpa Dominic (@DT-GAMER), who maintains it with the code owners listed in .github/CODEOWNERS.

License

wardn is open source under the Apache License 2.0. Copyright 2026 MogRex Limited.

Contributions are welcome under the same licence: see CONTRIBUTING.md. Report security issues privately, as SECURITY.md describes, never in a public issue.

Documentation

Overview

Package wardn is simple, opinionated authorization for Go.

Products define their own permissions, roles and scope hierarchy in a single policy file; wardn stores role assignments, evaluates access on every request, protects HTTP routes, and supports admin-created custom roles with built-in protection against privilege escalation.

wardn follows the compatibility promise in docs/STABILITY.md: nothing in v1 breaks code, data or decisions that rely on v1.0.0. See docs/DESIGN.md for the design and docs/THREAT_MODEL.md for its security model.

Example
package main

import (
	"context"
	"fmt"

	"github.com/MogRex-Limited/wardn"
)

const examplePolicy = `
scopes:
  organisation: {}
  branch: { parent: organisation }
permissions:
  customer:view: View customers
  loan:create:   Create loan applications
  loan:approve:  Approve loans
roles:
  org_admin:
    assignable_at: [organisation]
    allow: ["*"]
  teller:
    assignable_at: [branch]
    allow: [customer:view, loan:create]
`

func main() {
	policy, err := wardn.ParsePolicy([]byte(examplePolicy), "wardn.yaml")
	if err != nil {
		panic(err)
	}
	w := wardn.MustNew(wardn.Config{Policy: policy}) // production: Config{File: "wardn.yaml", Store: ...}
	ctx := context.Background()

	org := wardn.NewScope("organisation", "acme")
	branch := wardn.NewScope("branch", "lagos")
	_ = w.Link(ctx, branch, org) // the Lagos branch belongs to Acme

	_ = w.System().Assign(ctx, "ada", "org_admin", org)
	_ = w.System().Assign(ctx, "bayo", "teller", branch)

	fmt.Println(w.Can(ctx, "ada", "loan:approve", branch))  // inherited from the organisation
	fmt.Println(w.Can(ctx, "bayo", "loan:create", branch))  // teller
	fmt.Println(w.Can(ctx, "bayo", "loan:approve", branch)) // tellers can't approve

	d, _ := w.Check(ctx, "bayo", "loan:approve", branch)
	fmt.Println(d.Reason)
}
Output:
true
true
false
no role grants "loan:approve" at branch:lagos

Index

Examples

Constants

View Source
const (
	// PermRolesManage allows creating, editing and deleting custom roles.
	PermRolesManage = "wardn:roles.manage"
	// PermRolesAssign allows assigning and revoking roles.
	PermRolesAssign = "wardn:roles.assign"
)

Built-in permissions. Every policy's catalog includes them automatically; grant them to roles like any other permission.

View Source
const DefaultCacheTTL = 5 * time.Second

DefaultCacheTTL is how long cache entries live when Config.CacheTTL is 0.

View Source
const DefaultPolicyFile = "wardn.yaml"

DefaultPolicyFile is the policy file New reads when Config sets neither File nor Policy.

View Source
const MaxCheckEach = 1000

MaxCheckEach is the most scopes one CheckEach or CanEach call accepts. Page longer lists.

Variables

View Source
var (
	// ErrInvalidInput reports a malformed user ID, scope, role name,
	// permission or time window.
	ErrInvalidInput = errors.New("wardn: invalid input")

	// ErrNotAssignable reports an attempt to assign a role at a scope type
	// its assignable_at list does not include.
	ErrNotAssignable = errors.New("wardn: role is not assignable at this scope")

	// ErrInvalidLink reports a scope link that contradicts the policy's
	// scope hierarchy, or a stored link that does.
	ErrInvalidLink = errors.New("wardn: invalid scope link")

	// ErrEscalation reports an attempt to create, edit, assign or revoke a
	// role containing permissions the acting user does not hold (the
	// escalation guard, design §6.2).
	ErrEscalation = errors.New("wardn: you cannot grant permissions you do not hold")

	// ErrRoleInUse reports an attempt to delete a custom role that is still
	// assigned, without wardn.Cascade.
	ErrRoleInUse = errors.New("wardn: role is still assigned")

	// ErrAlreadyLinked reports a Link to a different parent than the scope
	// already has. It usually means two tenants reuse an ID. Use Move to
	// change a parent on purpose. It also matches ErrInvalidLink.
	ErrAlreadyLinked = errors.New("wardn: scope already has a different parent")

	// ErrLinksExternal reports a call to Link when scope parents are
	// resolved by Config.ParentOf instead of stored by wardn.
	ErrLinksExternal = errors.New("wardn: scope parents are resolved by Config.ParentOf; Link is disabled")
)

Sentinel errors. Errors returned by wardn wrap these, so test for them with errors.Is.

View Source
var (
	// ErrUnauthenticated reports a request with no authenticated user.
	ErrUnauthenticated = errors.New("wardn: no authenticated user")

	// ErrDenied reports a request whose user lacks the required permissions.
	// The error is a *DeniedError carrying the decisions.
	ErrDenied = errors.New("wardn: permission denied")
)

HTTP errors passed to Config.OnError. Test for them with errors.Is.

View Source
var (
	// ErrRoleNameTaken: a custom role with the same normalised name already
	// exists in the owner scope.
	ErrRoleNameTaken = errors.New("wardn: a role with this name already exists here")

	// ErrRoleNotFound: no custom role has the given ID.
	ErrRoleNotFound = errors.New("wardn: role not found")

	// ErrConflict: the role was changed by someone else since it was read.
	ErrConflict = errors.New("wardn: role was changed by someone else; reload and try again")
)

Store errors. Stores return these (possibly wrapped) so wardn can react.

View Source
var ErrAuditRejected = errors.New("wardn: the audit hook rejected the change")

ErrAuditRejected wraps an error returned by Config.OnChange, which stops the change (fail closed).

View Source
var ErrCacheInvalidation = errors.New("wardn: change saved, but invalidating the cache failed")

ErrCacheInvalidation reports that a change was saved, but invalidating the cache afterwards failed: other instances may serve the old state for up to the cache TTL. Retrying the (idempotent) change is safe.

View Source
var ErrNoTransaction = errors.New("wardn: a change inside an application transaction needs a context from Warden.Transaction")

ErrNoTransaction reports a change made with a context that carries an application transaction, but did not come from Warden.Transaction. wardn refuses it, because it could not keep its cache in step with the commit.

View Source
var Global = Scope{}

Global is the implicit scope above every other scope. A role assigned at Global applies everywhere.

Functions

func DecodeMemberCursor

func DecodeMemberCursor(cursor string) (user, role string, err error)

DecodeMemberCursor parses a cursor made by EncodeMemberCursor.

func EncodeMemberCursor

func EncodeMemberCursor(user, role string) string

EncodeMemberCursor builds a Members cursor from the last user and role of a page. Stores may use it, or any opaque encoding of their own.

func HTTPStatus

func HTTPStatus(err error) int

HTTPStatus returns the HTTP status code for an error from wardn: 401 when no user is authenticated; 403 when denied or when the escalation guard refuses; 400 for invalid input; 404 for an unknown custom role (also what an actor without wardn:roles.manage sees for an existing one); 409 for an edit conflict, a taken name, a role still in use or a scope already linked elsewhere; and 500 for anything else, such as a store failure. Admin APIs can use it for management errors too.

func PathValue

func PathValue(name string) func(*http.Request) string

PathValue returns a function that reads a path parameter, for use with ScopeFrom. It works with net/http's ServeMux and with chi.

func UserOf

func UserOf(ctx context.Context) (string, bool)

UserOf returns the user ID stored by WithUser.

func WithScope

func WithScope(ctx context.Context, s Scope) context.Context

WithScope returns a copy of ctx carrying the scope a request acts in.

func WithUser

func WithUser(ctx context.Context, userID string) context.Context

WithUser returns a copy of ctx carrying the authenticated user's ID. The product's authentication middleware calls this; wardn's HTTP middleware reads it.

Types

type AssignOption

type AssignOption func(*assignWindow)

AssignOption limits an assignment in time: From or Until. Options can set only the time window, never the user, role or scope that the guard and the audit trail see.

func From

func From(t time.Time) AssignOption

From makes an assignment apply only from t onwards.

func Until

func Until(t time.Time) AssignOption

Until makes an assignment stop applying at t.

type Assignment

type Assignment struct {
	User  string `json:"user"`
	Role  string `json:"role"` // a system role name, or a custom role ID ("cr-...")
	Scope Scope  `json:"scope"`

	// StartsAt and ExpiresAt bound when the assignment applies. The zero
	// time means "no bound". The window is half-open: it applies from
	// StartsAt (inclusive) until ExpiresAt (exclusive).
	StartsAt  time.Time `json:"starts_at,omitzero"`
	ExpiresAt time.Time `json:"expires_at,omitzero"`
}

Assignment records that a user holds a role in a scope, optionally only within a time window.

type AssignmentStore

type AssignmentStore interface {
	// Assign saves an assignment. There is at most one assignment per
	// (user, role, scope): assigning again replaces the time window and
	// clears any expiry report.
	Assign(ctx context.Context, a Assignment) error

	// Revoke deletes the assignment of role to user at scope, if any.
	// Revoking an assignment that does not exist is not an error.
	Revoke(ctx context.Context, user, role string, scope Scope) error

	// Assignments returns the user's assignments at any of the given
	// scopes, regardless of time window. It must never return assignments
	// for other users or other scopes.
	Assignments(ctx context.Context, user string, scopes []Scope) ([]Assignment, error)

	// Members returns up to limit assignments at exactly scope, ordered by
	// user then role, starting after cursor ("" for the first page). It
	// returns the cursor for the next page, or "" after the last page.
	// Cursors are opaque to callers.
	Members(ctx context.Context, scope Scope, cursor string, limit int) ([]Assignment, string, error)

	// RoleInUse reports whether any assignment refers to role.
	RoleInUse(ctx context.Context, role string) (bool, error)

	// RevokeRole deletes every assignment of role.
	RevokeRole(ctx context.Context, role string) error

	// ExpiredUnreported returns up to limit assignments whose ExpiresAt is
	// at or before t and that have not been marked as reported.
	ExpiredUnreported(ctx context.Context, t time.Time, limit int) ([]Assignment, error)

	// MarkExpiryReported records that the given assignments' expiry was
	// reported. An assignment whose time window has changed since it was
	// read (it was assigned again) must not be marked.
	MarkExpiryReported(ctx context.Context, as []Assignment) error

	// RevokeExpired deletes the assignment only if its ExpiresAt still
	// equals a.ExpiresAt. If it was assigned again with a new window, it
	// stays. The check and the deletion must be atomic.
	RevokeExpired(ctx context.Context, a Assignment) error
}

AssignmentStore persists role assignments.

type Cache

type Cache interface {
	// Get returns the value stored under key, and false if there is none
	// or it has expired.
	Get(ctx context.Context, key string) ([]byte, bool, error)
	// Set stores value under key for ttl.
	Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
	// Counters returns the current values of counters; a counter that has
	// never been incremented is 0. Counters never expire.
	Counters(ctx context.Context, keys []string) ([]uint64, error)
	// Incr atomically increments each counter by one.
	Incr(ctx context.Context, keys []string) error
}

Cache is an optional cache shared across requests, and across instances of the application when the implementation is shared (Redis, for example). It is off by default (design decision D1).

wardn caches what it reads from the store (assignments, scope parents, custom roles), never decisions: every check is still evaluated, against the clock, so time-limited access starts and ends on time however long an entry is cached. Entries are invalidated by version counters: every cache key includes the current counters of the tenant it belongs to, and every change increments them before and after writing, so stale entries become unreachable everywhere at once. The TTL is only a safety net, for changes made directly in the database, bypassing wardn.

Implementations must be safe for concurrent use and must keep Incr atomic. Keys are opaque, fixed-length hashes; wardn validates everything it reads back. A shared cache must be private to the application (threat model T24).

type Clock

type Clock interface {
	Now() time.Time
}

Clock supplies the current time.

type Config

type Config struct {
	// File is the path of a YAML policy file. Default: "wardn.yaml".
	File string

	// Policy is a policy defined in Go, used instead of File.
	Policy *Policy

	// Store persists assignments and scope links. Default: a new
	// MemoryStore, which is lost on exit. Use a database store in
	// production.
	Store Store

	// ParentOf, if set, resolves a scope's parent from the application's own
	// data instead of links stored with Link. It must return the true parent
	// from authoritative data; returning ok=false means "no parent".
	ParentOf func(ctx context.Context, child Scope) (parent Scope, ok bool, err error)

	// Cache, if set, caches store reads across requests (see Cache). Off
	// by default: without it every request reads the store, so a change is
	// visible on the very next request. Use a shared cache when running
	// more than one instance.
	Cache Cache

	// CacheSecret seals cache entries so they cannot be forged by whoever
	// can write to a shared cache. Required with a shared Cache: at least 32
	// random bytes from your secret store, the same on every instance.
	// Optional with NewMemoryCache.
	CacheSecret []byte

	// CacheTTL is how long cache entries live (default 5s, at most 5m). It
	// only matters for changes made behind wardn's back; wardn's own
	// changes invalidate the cache immediately.
	CacheTTL time.Duration

	// ManualMigrations stops New from migrating the Store's database
	// schema. Set it if your deploy pipeline runs migrations itself.
	ManualMigrations bool

	// Clock supplies the current time for time-limited assignments.
	// Default: the system clock. Tests can inject a fake clock.
	Clock Clock

	// UserFrom, if set, returns the authenticated user's ID for a request.
	// By default wardn's HTTP middleware reads it from the request context,
	// where the application's authentication middleware put it with
	// WithUser.
	UserFrom func(r *http.Request) (userID string, ok bool)

	// OnError, if set, writes the response when wardn's HTTP middleware
	// rejects a request. Use HTTPStatus(err) for the status code. By default
	// wardn writes a generic JSON error that does not reveal which
	// permission was missing.
	OnError func(w http.ResponseWriter, r *http.Request, err error)

	// OnChange, if set, is called before every change to assignments,
	// custom roles and scope links, with the acting user, for the audit
	// trail. If it returns an error, the change is not made (fail closed).
	// If the change then fails, it is called again with Event.Err set.
	OnChange func(ctx context.Context, e Event) error

	// OnDecision, if set, is called with every authorization decision and
	// any error, for audit logging and debugging. It must be fast and must
	// not panic.
	OnDecision func(ctx context.Context, d Decision, err error)
}

Config configures a Warden.

type CustomRole

type CustomRole struct {
	ID          string `json:"id"`          // generated; starts with "cr-", so it never clashes with a system role name
	Owner       Scope  `json:"owner"`       // where the role was created
	Name        string `json:"name"`        // display name, unique within the owner scope and its ancestors
	NameKey     string `json:"name_key"`    // normalised name used for uniqueness; set by wardn
	Description string `json:"description"` //

	// Allow lists the permissions the role grants: concrete permissions or
	// wildcards from the catalog.
	Allow []string `json:"allow"`

	// AssignableAt lists the scope types where the role may be assigned:
	// the owner's type and types below it. Default: the owner's type.
	AssignableAt []string `json:"assignable_at"`

	Version   int       `json:"version"` // incremented on every update; used to detect conflicting edits
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

CustomRole is a role created at runtime, usually by an administrator. It belongs to an owner scope and applies only within that scope's subtree. Custom roles only grant; they cannot deny (design decision D2).

type Decision

type Decision struct {
	Allowed    bool   `json:"allowed"`
	User       string `json:"user"`
	Permission string `json:"permission"`
	Scope      Scope  `json:"scope"` // the scope that was checked

	// Role is the role that decided the outcome: the role whose deny rule
	// matched, or the role that granted the permission. Empty when no role
	// matched or an error occurred.
	Role string `json:"role,omitempty"`
	// At is the scope where Role is assigned: Scope itself or an ancestor.
	At Scope `json:"at,omitzero"`
	// Rule is the allow or deny pattern that matched, e.g. "loan:*".
	Rule string `json:"rule,omitempty"`
	// Reason explains the decision in words, for logs and debugging.
	Reason string `json:"reason"`
}

Decision is the outcome of an authorization check, with the reason.

type DeleteOption

type DeleteOption func(*deleteOptions)

DeleteOption configures DeleteRole.

func Cascade

func Cascade() DeleteOption

Cascade makes DeleteRole also revoke every assignment of the role. Without it, deleting a role that is still assigned fails with ErrRoleInUse.

type DeniedError

type DeniedError struct {
	Decisions []Decision
}

DeniedError is returned when a request is denied. It wraps ErrDenied and carries the decisions, so a custom OnError can log why.

func (*DeniedError) Error

func (e *DeniedError) Error() string

func (*DeniedError) Unwrap

func (e *DeniedError) Unwrap() error

Unwrap makes errors.Is(err, ErrDenied) true.

type Event

type Event struct {
	Type EventType `json:"type"`
	// Actor is the acting user's ID. It is empty when System is true.
	Actor string `json:"actor,omitempty"`
	// System is true where no user is acting: changes made through
	// Warden.System, Link and Move called with no signed-in user in ctx,
	// and the expiry sweeper.
	System bool      `json:"system"`
	At     time.Time `json:"at,omitzero"`

	User   string `json:"user,omitempty"`  // assignments: whose access changed
	Role   string `json:"role,omitempty"`  // assignments and roles: system role name or custom role ID
	Scope  Scope  `json:"scope,omitzero"`  // assignments: where; roles: the owner; links and moves: the child
	Parent Scope  `json:"parent,omitzero"` // links and moves: the new parent
	// PreviousParent is the parent before a move (EventMoved).
	PreviousParent Scope `json:"previous_parent,omitzero"`

	StartsAt  time.Time `json:"starts_at,omitzero"` // assignments: the time window
	ExpiresAt time.Time `json:"expires_at,omitzero"`

	Before *CustomRole `json:"before,omitempty"` // role updates and deletions
	After  *CustomRole `json:"after,omitempty"`  // role creations and updates

	// Err is set when OnChange is called a second time for the same change
	// because it failed after OnChange approved it. A failed change may
	// have been partly applied (for example, a cascade deletion that deleted
	// the role but not all its assignments); the store is the source of
	// truth. OnChange's return value is ignored on that call.
	Err error `json:"-"`
}

Event describes a change to who can do what, for the audit trail (Config.OnChange). Fields that do not apply to a type are zero.

Events marshal to JSON with stable snake_case field names, leaving out fields that do not apply; Err is written as its message ("error").

func (Event) MarshalJSON

func (e Event) MarshalJSON() ([]byte, error)

MarshalJSON writes the event with Err as its message, under "error".

func (*Event) UnmarshalJSON

func (e *Event) UnmarshalJSON(data []byte) error

UnmarshalJSON reads an event written by MarshalJSON. A recorded error comes back as an error with the same message.

type EventType

type EventType string

EventType names a kind of change.

const (
	EventAssigned    EventType = "assignment.created"
	EventRevoked     EventType = "assignment.revoked"
	EventExpired     EventType = "assignment.expired"
	EventRoleCreated EventType = "role.created"
	EventRoleUpdated EventType = "role.updated"
	EventRoleDeleted EventType = "role.deleted"
	EventLinked      EventType = "scope.linked"
	EventMoved       EventType = "scope.moved"
)

Event types.

type MemoryCache

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

MemoryCache is a Cache held in this process. It is correct only when a single instance of the application runs: other instances would not see its invalidations. Use a shared cache (wardn/cache/redis) when you run more than one.

func NewMemoryCache

func NewMemoryCache(opts ...MemoryCacheOption) *MemoryCache

NewMemoryCache returns an empty MemoryCache.

func (*MemoryCache) Counters

func (c *MemoryCache) Counters(ctx context.Context, keys []string) ([]uint64, error)

Counters implements Cache.

func (*MemoryCache) Get

func (c *MemoryCache) Get(ctx context.Context, key string) ([]byte, bool, error)

Get implements Cache.

func (*MemoryCache) Incr

func (c *MemoryCache) Incr(ctx context.Context, keys []string) error

Incr implements Cache.

func (*MemoryCache) Set

func (c *MemoryCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

Set implements Cache.

type MemoryCacheOption

type MemoryCacheOption func(*MemoryCache)

MemoryCacheOption configures a MemoryCache.

func WithCacheClock

func WithCacheClock(now func() time.Time) MemoryCacheOption

WithCacheClock sets the clock used for entry expiry, for tests.

func WithMaxEntries

func WithMaxEntries(n int) MemoryCacheOption

WithMaxEntries bounds the number of cached entries (default 100 000).

type MemoryStore

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

MemoryStore is a Store that keeps everything in memory. Its contents are lost when the process exits, so it is meant for tests, examples and demos. Use a database-backed Store in production.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty MemoryStore.

func (*MemoryStore) Assign

func (m *MemoryStore) Assign(ctx context.Context, a Assignment) error

Assign implements Store.

func (*MemoryStore) Assignments

func (m *MemoryStore) Assignments(ctx context.Context, user string, scopes []Scope) ([]Assignment, error)

Assignments implements Store.

func (*MemoryStore) CreateRole

func (m *MemoryStore) CreateRole(ctx context.Context, r CustomRole) error

CreateRole implements Store.

func (*MemoryStore) DeleteRole

func (m *MemoryStore) DeleteRole(ctx context.Context, id string) error

DeleteRole implements Store.

func (*MemoryStore) ExpiredUnreported

func (m *MemoryStore) ExpiredUnreported(ctx context.Context, t time.Time, limit int) ([]Assignment, error)

ExpiredUnreported implements Store.

func (m *MemoryStore) Link(ctx context.Context, child, parent Scope) error

Link implements Store.

func (*MemoryStore) MarkExpiryReported

func (m *MemoryStore) MarkExpiryReported(ctx context.Context, as []Assignment) error

MarkExpiryReported implements Store.

func (*MemoryStore) Members

func (m *MemoryStore) Members(ctx context.Context, scope Scope, cursor string, limit int) ([]Assignment, string, error)

Members implements Store.

func (*MemoryStore) Parent

func (m *MemoryStore) Parent(ctx context.Context, child Scope) (Scope, bool, error)

Parent implements Store.

func (*MemoryStore) Revoke

func (m *MemoryStore) Revoke(ctx context.Context, user, role string, scope Scope) error

Revoke implements Store.

func (*MemoryStore) RevokeExpired

func (m *MemoryStore) RevokeExpired(ctx context.Context, a Assignment) error

RevokeExpired implements Store.

func (*MemoryStore) RevokeRole

func (m *MemoryStore) RevokeRole(ctx context.Context, role string) error

RevokeRole implements Store.

func (*MemoryStore) RoleInUse

func (m *MemoryStore) RoleInUse(ctx context.Context, role string) (bool, error)

RoleInUse implements Store.

func (*MemoryStore) Roles

func (m *MemoryStore) Roles(ctx context.Context, ids []string) ([]CustomRole, error)

Roles implements Store.

func (*MemoryStore) RolesOwnedBy

func (m *MemoryStore) RolesOwnedBy(ctx context.Context, owners []Scope) ([]CustomRole, error)

RolesOwnedBy implements Store.

func (*MemoryStore) UpdateRole

func (m *MemoryStore) UpdateRole(ctx context.Context, r CustomRole, expectedVersion int) error

UpdateRole implements Store.

type Migrator

type Migrator interface {
	Migrate(ctx context.Context) error
}

Migrator is implemented by stores that manage their own database schema. New calls Migrate at startup unless Config.ManualMigrations is set.

type Permission

type Permission struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

Permission is an entry in the permission catalog.

type Policy

type Policy struct {
	// Scopes declares the scope types and how they nest. Leave it empty for
	// a product without tenancy, which uses only the Global scope.
	Scopes map[string]ScopeType

	// Permissions is the catalog: every permission the code checks, mapped
	// to a human-readable description. The built-in wardn:... permissions
	// are added automatically and must not be declared.
	Permissions map[string]string

	// Roles are the system roles, keyed by name.
	Roles map[string]Role
	// contains filtered or unexported fields
}

Policy is a product's authorization model: its scope types, the catalog of permissions its code checks, and its system roles. It is usually loaded from a YAML file with LoadPolicyFile, but can be built in Go.

func LoadPolicyFile

func LoadPolicyFile(path string) (*Policy, error)

LoadPolicyFile reads and parses a YAML policy file. It checks the file's structure; New (or Policy.Validate) checks its meaning.

func ParsePolicy

func ParsePolicy(data []byte, source string) (*Policy, error)

ParsePolicy parses a YAML policy. source names it in error messages, usually the file name.

func (*Policy) Validate

func (p *Policy) Validate() error

Validate checks the policy and reports every problem it finds as a *PolicyError. New calls it automatically.

type PolicyError

type PolicyError struct {
	Source string        // the file name, or "policy" for a Go-defined policy
	Issues []PolicyIssue // sorted by line, then path
}

PolicyError reports every problem found in a policy, so they can all be fixed at once.

func (*PolicyError) Error

func (e *PolicyError) Error() string

type PolicyIssue

type PolicyIssue struct {
	Line    int    // line in the source file; 0 if unknown
	Path    string // location in the policy, e.g. "roles.teller.allow[1]"
	Message string
}

PolicyIssue is one problem in a policy.

type Role

type Role struct {
	Description string

	// Allow lists the permissions this role grants. Entries may be concrete
	// permissions ("loan:create"), resource wildcards ("loan:*"), or "*".
	Allow []string

	// Deny lists permissions this role takes away. A deny from any role
	// overrides an allow from every role.
	Deny []string

	// AssignableAt lists the scope types (or "global") at which this role
	// may be assigned. Empty means anywhere.
	AssignableAt []string
}

Role is a system role: a named set of permission patterns.

type RoleInfo

type RoleInfo struct {
	ID           string   `json:"id"`   // system role name, or custom role ID
	Name         string   `json:"name"` // display name (the system role name for system roles)
	Description  string   `json:"description"`
	Allow        []string `json:"allow"`
	Deny         []string `json:"deny,omitempty"`          // system roles only
	AssignableAt []string `json:"assignable_at,omitempty"` // empty: anywhere
	Custom       bool     `json:"custom"`
	Owner        Scope    `json:"owner,omitzero"`   // custom roles only
	Version      int      `json:"version,omitzero"` // custom roles only
}

RoleInfo describes a role for admin screens.

type RoleSpec

type RoleSpec struct {
	Name         string   `json:"name"`
	Description  string   `json:"description"`
	Allow        []string `json:"allow"`
	AssignableAt []string `json:"assignable_at,omitempty"` // default: the owner scope's type
	// Deny is not supported for custom roles in this release: a RoleSpec
	// with Deny entries is rejected (design decision D2).
	Deny []string `json:"deny,omitempty"`
}

RoleSpec describes a custom role to create or the new content of one to update.

type RoleStore

type RoleStore interface {
	// CreateRole saves a new custom role. It returns ErrRoleNameTaken if a
	// role with the same Owner and NameKey exists; the check and the write
	// must be atomic.
	CreateRole(ctx context.Context, r CustomRole) error

	// UpdateRole replaces a custom role if its stored version equals
	// expectedVersion, and returns ErrConflict otherwise, ErrRoleNotFound if
	// it does not exist, or ErrRoleNameTaken. r.Version is the new version.
	UpdateRole(ctx context.Context, r CustomRole, expectedVersion int) error

	// DeleteRole deletes a custom role. Deleting a role that does not exist
	// is not an error.
	DeleteRole(ctx context.Context, id string) error

	// Roles returns the custom roles with the given IDs, omitting IDs that
	// do not exist.
	Roles(ctx context.Context, ids []string) ([]CustomRole, error)

	// RolesOwnedBy returns the custom roles owned by any of the scopes.
	RolesOwnedBy(ctx context.Context, owners []Scope) ([]CustomRole, error)
}

RoleStore persists custom roles.

type Scope

type Scope struct {
	Type string `json:"type"` // a scope type declared in the policy, e.g. "branch"
	ID   string `json:"id"`   // the application's identifier for this instance, e.g. "b7"
}

Scope is where a role applies: an instance of one of the scope types the policy declares, such as an organisation or a branch. The zero value is Global, the implicit scope above all others.

Scopes are compared exactly, field by field: "o1" and "O1" are different scopes. wardn never normalises IDs.

func NewScope

func NewScope(scopeType, id string) Scope

NewScope returns the scope of the given type and ID.

func ScopeOf

func ScopeOf(ctx context.Context) (Scope, bool)

ScopeOf returns the scope stored by WithScope.

func (Scope) IsGlobal

func (s Scope) IsGlobal() bool

IsGlobal reports whether s is the Global scope.

func (Scope) String

func (s Scope) String() string

String returns "global" or "type:id", for display and logs only. Never use it as a key: IDs may themselves contain ':'.

type ScopeStore

type ScopeStore interface {
	// Link records that parent is the parent of child, replacing any
	// existing parent.
	Link(ctx context.Context, child, parent Scope) error

	// Parent returns child's parent, and false if none is recorded.
	Parent(ctx context.Context, child Scope) (Scope, bool, error)
}

ScopeStore persists the scope hierarchy.

type ScopeType

type ScopeType struct {
	// Parent is the scope type this one sits inside, or "" for a top-level
	// type. Roles assigned at a parent scope apply in its children.
	Parent string
}

ScopeType declares one kind of scope.

type Store

type Store interface {
	AssignmentStore
	RoleStore
	ScopeStore
}

Store persists assignments, custom roles and scope links. wardn's core does all the authorization logic (inheritance, wildcards, deny-wins, time windows, the escalation guard), so a Store only saves and loads records.

Implementations must be safe for concurrent use, must honour context cancellation, and must compare IDs exactly, byte for byte (no case- or accent-insensitive matching). Every method must either succeed completely or return an error; wardn treats any error as "deny". Stores may round times to a coarser precision, but never in a direction that widens a time window. The storetest package checks all of this.

type SweepOption

type SweepOption func(*sweepOptions)

SweepOption configures SweepExpired.

func RemoveExpired

func RemoveExpired() SweepOption

RemoveExpired makes SweepExpired also delete the expired assignments it reports. Without it they stay (harmlessly, since they grant nothing).

type System

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

System performs management operations without the escalation guard, for code that has no human actor: seeding the first administrator, data migrations and background jobs. Calls to it should stand out in code review. Every change is still audited, with Event.System set.

func (*System) Assign

func (s *System) Assign(ctx context.Context, user, role string, scope Scope, opts ...AssignOption) error

Assign gives user the role at scope without the escalation guard.

func (*System) CreateRole

func (s *System) CreateRole(ctx context.Context, owner Scope, spec RoleSpec) (CustomRole, error)

CreateRole creates a custom role without the escalation guard.

func (*System) DeleteRole

func (s *System) DeleteRole(ctx context.Context, id string, opts ...DeleteOption) error

DeleteRole deletes a custom role without the escalation guard.

func (*System) Revoke

func (s *System) Revoke(ctx context.Context, user, role string, scope Scope) error

Revoke removes role from user at scope without the escalation guard.

func (*System) UpdateRole

func (s *System) UpdateRole(ctx context.Context, id string, spec RoleSpec, version int) (CustomRole, error)

UpdateRole updates a custom role without the escalation guard.

type TxStore

type TxStore interface {
	Store
	// InTx reports whether ctx carries an application transaction that
	// this store will use instead of starting its own. A wrapper around a
	// TxStore must implement it too, by calling the wrapped store.
	InTx(ctx context.Context) bool
}

TxStore is a Store that can work inside the application's own database transaction, so a change to access commits or rolls back together with the application's other writes. The SQL store implements it (wardnsql.Store.WithTx).

type UndefinedError

type UndefinedError struct {
	Kind       string // "permission", "role" or "scope type"
	Name       string // the undefined name
	Suggestion string // the closest defined name, or ""
}

UndefinedError reports a reference to a permission, role or scope type that the policy does not define. It carries a suggestion when the name looks like a typo of one that is defined.

func (*UndefinedError) Error

func (e *UndefinedError) Error() string

type Warden

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

Warden answers authorization questions for one policy. It is safe for concurrent use; create one at startup and share it.

func MustNew

func MustNew(cfg Config) *Warden

MustNew is like New but panics on error. It is meant for main(), where a misconfigured policy should stop the application.

func New

func New(cfg Config) (*Warden, error)

New validates the policy and returns a Warden. Any problem in the policy is reported here, so a misconfigured application fails at startup rather than when a user is first denied.

func (*Warden) Assign

func (w *Warden) Assign(ctx context.Context, user, role string, scope Scope, opts ...AssignOption) error

Assign gives user the role at scope, on behalf of the user in ctx. The actor needs wardn:roles.assign at scope and must hold every permission the role grants there (the escalation guard). role is a system role name or a custom role ID. Assigning again replaces the time window.

func (*Warden) AssignableRoles

func (w *Warden) AssignableRoles(ctx context.Context, scope Scope) ([]RoleInfo, error)

AssignableRoles returns the roles the user in ctx may assign at scope: those from Roles that pass the escalation guard. Use it to fill the role picker in an admin screen, so people only see roles they can grant.

func (*Warden) Can

func (w *Warden) Can(ctx context.Context, user, permission string, scope Scope) bool

Can reports whether user holds permission at scope. It returns false on any error, including an undefined permission; use Check to see why.

func (*Warden) CanAll

func (w *Warden) CanAll(ctx context.Context, user string, scope Scope, permissions ...string) bool

CanAll reports whether user holds every one of the permissions at scope. It returns false if the list is empty, if any permission is undefined, or on any error.

func (*Warden) CanAny

func (w *Warden) CanAny(ctx context.Context, user string, scope Scope, permissions ...string) bool

CanAny reports whether user holds at least one of the permissions at scope. It returns false if any permission is undefined, or on any error.

func (*Warden) CanEach

func (w *Warden) CanEach(ctx context.Context, user, permission string, scopes []Scope) []bool

CanEach reports, for each scope, whether user holds permission there. The result has one entry per scope, in order. Use it to filter a list, such as the projects a user may see, without one round trip per item. Every entry is false on any error; use CheckEach to see why.

func (*Warden) Catalog

func (w *Warden) Catalog() []Permission

Catalog returns every permission in the policy, including the built-in ones, sorted by name. Admin screens use it to let people pick permissions.

func (*Warden) Check

func (w *Warden) Check(ctx context.Context, user, permission string, scope Scope) (Decision, error)

Check decides whether user holds permission at scope and explains why. Errors (an undefined permission, an invalid scope, a store failure) are returned with a Decision that denies: wardn always fails closed.

Example (Typo)
package main

import (
	"context"
	"fmt"

	"github.com/MogRex-Limited/wardn"
)

const examplePolicy = `
scopes:
  organisation: {}
  branch: { parent: organisation }
permissions:
  customer:view: View customers
  loan:create:   Create loan applications
  loan:approve:  Approve loans
roles:
  org_admin:
    assignable_at: [organisation]
    allow: ["*"]
  teller:
    assignable_at: [branch]
    allow: [customer:view, loan:create]
`

func main() {
	policy, _ := wardn.ParsePolicy([]byte(examplePolicy), "wardn.yaml")
	w := wardn.MustNew(wardn.Config{Policy: policy})

	_, err := w.Check(context.Background(), "ada", "loan:aprove", wardn.Global)
	fmt.Println(err)
}
Output:
wardn: permission "loan:aprove" is not defined; did you mean "loan:approve"?

func (*Warden) CheckEach

func (w *Warden) CheckEach(ctx context.Context, user, permission string, scopes []Scope) ([]Decision, error)

CheckEach decides, for each scope, whether user holds permission there, with the same rules and reasons as Check. It reads the user's assignments for all the scopes together, so it costs a few store reads rather than a few per scope.

The whole call fails, denying every scope, if the permission or user is invalid, any scope is invalid, there are more than MaxCheckEach scopes, or a read fails.

func (*Warden) CreateRole

func (w *Warden) CreateRole(ctx context.Context, owner Scope, spec RoleSpec) (CustomRole, error)

CreateRole creates a custom role owned by owner, on behalf of the user in ctx. The actor needs wardn:roles.manage at owner and must hold every permission in the role.

func (*Warden) CustomRole

func (w *Warden) CustomRole(ctx context.Context, id string) (CustomRole, error)

CustomRole returns a custom role by ID, for example to fill an edit form. Pass its Version to UpdateRole.

func (*Warden) DeleteRole

func (w *Warden) DeleteRole(ctx context.Context, id string, opts ...DeleteOption) error

DeleteRole deletes a custom role, on behalf of the user in ctx, under the same guard as UpdateRole.

func (*Warden) InvalidateCache

func (w *Warden) InvalidateCache(ctx context.Context, scopes ...Scope) error

InvalidateCache makes cached data for the tenants of the given scopes stale on every instance. wardn invalidates automatically after its own changes; call this after changing scope parents behind its back (when Config.ParentOf reads them from your data) or editing wardn's tables directly.

func (w *Warden) Link(ctx context.Context, child, parent Scope) error

Link records that parent is the parent of child, so roles assigned at parent (and its ancestors) apply in child. The parent's type must be the one the policy declares for child.

Link never changes an existing parent. Linking a scope to the parent it already has does nothing, so it is safe to call on every start. Linking it to a different parent fails with ErrAlreadyLinked (which also matches ErrInvalidLink): two tenants that reuse the same ID ("cardiology" in two hospitals) would otherwise move one tenant's scope under the other's administrators. Use Move to change a parent on purpose.

Link is plumbing for the application's backend, called when it creates an entity. Never expose it directly to end users: whoever controls links controls inheritance (threat model T11). Every link is audited.

func (*Warden) Members

func (w *Warden) Members(ctx context.Context, scope Scope, cursor string, limit int) ([]Assignment, string, error)

Members returns one page of the assignments made at exactly scope (not inherited ones), ordered by user then role, for admin screens. Pass "" as cursor for the first page and the returned cursor for the next; it is "" after the last page. limit is capped at 500.

Members does not check who is asking: protect the route that exposes it with Require.

func (*Warden) Move

func (w *Warden) Move(ctx context.Context, child, newParent Scope) error

Move changes child's parent, for example when a branch moves to another region. Roles inherited from the old parent stop applying, and those of the new parent start, on the next check. Every move is audited as EventMoved, with the old parent. Moving a scope that has no parent yet links it (EventLinked); moving it to the parent it has does nothing.

func (*Warden) Now

func (w *Warden) Now() time.Time

Now returns the current time on the Warden's clock (Config.Clock). Use it when your code computes a time window, such as Until(w.Now().Add(72 * time.Hour)), so tests that move wardn's clock move yours too.

func (*Warden) PermissionsOf

func (w *Warden) PermissionsOf(ctx context.Context, user string, scope Scope) ([]string, error)

PermissionsOf returns every catalog permission the user holds at scope right now, sorted. Frontends use it to show or hide actions.

func (*Warden) Require

func (w *Warden) Require(permissions ...string) func(http.Handler) http.Handler

Require returns middleware that lets a request through only if its user holds every one of the permissions at the request's scope. It works with net/http, chi, and any router that uses func(http.Handler) http.Handler.

The user comes from Config.UserFrom, or from the context (see WithUser). The scope comes from ScopeFrom middleware, or is Global if none is set.

Require panics if a permission is not in the policy, so a typo stops the application when routes are registered rather than denying users later.

func (*Warden) RequireAny

func (w *Warden) RequireAny(permissions ...string) func(http.Handler) http.Handler

RequireAny is like Require, but one of the permissions is enough.

func (*Warden) Revoke

func (w *Warden) Revoke(ctx context.Context, user, role string, scope Scope) error

Revoke removes role from user at scope, on behalf of the user in ctx, under the same guard as Assign, so a lower administrator cannot strip a higher one (threat model T04).

func (*Warden) Roles

func (w *Warden) Roles(ctx context.Context, scope Scope) ([]RoleInfo, error)

Roles returns the roles that can be assigned at scope: system roles whose assignable_at allows it, and custom roles owned by scope or an ancestor that are assignable at its type. System roles come first, then custom roles, each sorted by name.

func (*Warden) RolesOf

func (w *Warden) RolesOf(ctx context.Context, user string, scope Scope) ([]Assignment, error)

RolesOf returns the user's assignments that apply at scope right now, including those inherited from ancestor scopes and Global, nearest first.

func (*Warden) RunExpiry

func (w *Warden) RunExpiry(ctx context.Context, interval time.Duration, onError func(error), opts ...SweepOption)

RunExpiry calls SweepExpired every interval until ctx is done, passing errors to onError (which may be nil). Run it in its own goroutine, on one instance or on all. Reporting is at least once: instances sweeping at the same time, or a sweep that fails part-way, can report an expiry twice, so an audit log should ignore a repeat of the same user, role, scope and expiry time.

func (*Warden) ScopeFrom

func (w *Warden) ScopeFrom(scopeType string, id func(*http.Request) string) func(http.Handler) http.Handler

ScopeFrom returns middleware that sets the request's scope to the given scope type, with the ID taken from the request by id (for example PathValue("orgID")). Place it where the ID is available: with chi, inside r.Route or r.With, after routing.

A missing or invalid ID is answered with 400 Bad Request; the request never reaches Require. ScopeFrom panics if scopeType is not declared in the policy.

func (*Warden) SweepExpired

func (w *Warden) SweepExpired(ctx context.Context, opts ...SweepOption) (int, error)

SweepExpired reports every assignment that has expired since the last sweep through Config.OnChange (as EventExpired), for the audit trail, and returns how many it reported. Expired assignments stop working on their own (evaluation rule 7); the sweep only records that they did. If OnChange fails, the sweep stops and those assignments are reported again next time.

func (*Warden) System

func (w *Warden) System() *System

System returns the unguarded management handle. Requests made on behalf of a user must use the guarded methods on Warden instead.

func (*Warden) SystemRoles

func (w *Warden) SystemRoles() []string

SystemRoles returns the names of the policy's system roles, sorted.

func (*Warden) Transaction

func (w *Warden) Transaction(ctx context.Context) (context.Context, func(context.Context) error)

Transaction prepares ctx for wardn changes made inside the application's own database transaction. The context passed to those changes must also carry the transaction for the store (for the SQL store, Store.WithTx), added before or after Transaction. Call the returned function once the transaction has ended, whether it committed or rolled back, never before the commit:

tx, err := db.BeginTx(ctx, nil)
...
defer tx.Rollback()
ctx, ended := w.Transaction(store.WithTx(ctx, tx))
if err := w.Assign(ctx, "bayo", "teller", branch); err != nil { ... }
// ... the application's own writes, using tx ...
err = tx.Commit()
if endErr := ended(ctx); endErr != nil { ... log it ... }

Inside the transaction, wardn reads through it, so it sees the transaction's own uncommitted writes, and it neither reads nor fills the cache or the request memo: uncommitted data must never reach other requests. The cache is invalidated before each change and again when ended is called, so no instance serves the old state once the transaction has committed. If ended fails, other instances may serve the old state for up to the cache TTL (ErrCacheInvalidation).

A change made with a transaction context that did not come from Transaction fails with ErrNoTransaction, so a forgotten call cannot silently leave the cache stale.

func (*Warden) UpdateRole

func (w *Warden) UpdateRole(ctx context.Context, id string, spec RoleSpec, version int) (CustomRole, error)

UpdateRole replaces a custom role's name, description and permissions, on behalf of the user in ctx. version must be the version the caller read; if the role changed since, ErrConflict is returned. The actor needs wardn:roles.manage at the role's owner and must hold every permission in both the old and the new role.

func (*Warden) WithMemo

func (w *Warden) WithMemo(ctx context.Context) context.Context

WithMemo returns a copy of ctx in which this Warden memoises store reads, so repeated checks in one request do not repeat queries. Use it for the lifetime of a single request only. wardn's HTTP middleware does this automatically.

Directories

Path Synopsis
cache
redis module
Package cachetest is the conformance suite for wardn.Cache implementations:
Package cachetest is the conformance suite for wardn.Cache implementations:
cmd
wardn command
Command wardn checks policy files and generates Go code from them.
Command wardn checks policy files and generates Go code from them.
store
sql
Package wardnsql is a wardn.Store for SQL databases: PostgreSQL, MySQL, MariaDB and SQLite.
Package wardnsql is a wardn.Store for SQL databases: PostgreSQL, MySQL, MariaDB and SQLite.
mongo module
Package storetest is the conformance suite for wardn.Store implementations, and their benchmark suite (Bench).
Package storetest is the conformance suite for wardn.Store implementations, and their benchmark suite (Bench).
Package wardntest helps test code that uses wardn, without a database.
Package wardntest helps test code that uses wardn, without a database.

Jump to

Keyboard shortcuts

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