authz

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Apr 16, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package authz provides a normative authorization contract and core types used by OpenCHAMI services integrating TokenSmith.

Package authz provides net/http-compatible middleware implementing the TokenSmith authorization wire contract.

Index

Constants

View Source
const (
	DenyCodeAuthNRequired  DenyCode = "AUTHN_REQUIRED"
	DenyCodeAuthNInvalid   DenyCode = "AUTHN_INVALID"
	DenyCodeAuthzDenied    DenyCode = "AUTHZ_DENIED"
	DenyCodeAuthzUnmapped  DenyCode = "AUTHZ_UNMAPPED"
	DenyCodeAuthzEngineErr DenyCode = "AUTHZ_ENGINE_ERROR"
	DenyCodeBadRequest     DenyCode = "BAD_REQUEST"
	DenySchemaVersionV1    string   = "authz.deny.v1"
)
View Source
const (
	// EnvAuthzCacheSize configures the maximum number of cached authorization
	// decisions. Cache is disabled by default.
	EnvAuthzCacheSize = "TOKENSMITH_AUTHZ_CACHE_SIZE"
)

Variables

This section is empty.

Functions

func ContextWithHTTPRequest

func ContextWithHTTPRequest(ctx context.Context, r *http.Request) context.Context

ContextWithHTTPRequest stores an *http.Request in context for downstream helpers (e.g., observability hooks).

This is intended for internal TokenSmith middleware wiring; services generally should not rely on this.

func ContextWithRequestID

func ContextWithRequestID(ctx context.Context, requestID string) context.Context

ContextWithRequestID stores a request id string in ctx.

func DefaultRequestIDFromContext

func DefaultRequestIDFromContext(ctx context.Context) string

DefaultRequestIDFromContext returns a request id using a best-effort chain:

  1. ctx value set via ContextWithRequestID
  2. (optional) X-Request-Id request header, if r is available to the caller

Note: because authz.Middleware currently only supports a ctx extractor, header-based extraction is typically implemented in the service and stored in context using ContextWithRequestID.

func NewBadRequestError

func NewBadRequestError(msg string) error

NewBadRequestError returns an error that will be treated as bad_request.

func NormalizeEscapedPath

func NormalizeEscapedPath(u *url.URL) (string, error)

NormalizeEscapedPath implements the TokenSmith path normalization rules for path/method style authorization.

Spec: docs/authz-spec.md §3.

Behavior summary:

  • Uses u.EscapedPath() if non-empty, else returns "/".
  • Rejects malformed %-escapes with BadRequestError.
  • Preserves encoded slashes (%2F) by only unescaping *non-slash* sequences. (i.e., %2F remains %2F, preventing path segment ambiguity).
  • Cleans dot segments via path.Clean while keeping a leading slash.

The returned string is safe to feed to Casbin keyMatch/keyMatch2 matchers.

func RequestIDFromContext

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

RequestIDFromContext returns a request id string from ctx, if present.

func RequestIDFromHeader

func RequestIDFromHeader(r *http.Request, header string) string

RequestIDFromHeader returns the request id from r using the provided header name (default: X-Request-Id).

func SetPrincipal

func SetPrincipal(ctx context.Context, p *Principal) context.Context

SetPrincipal attaches a verified principal to ctx.

AuthN middleware should call this after token validation.

Types

type Authorizer

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

Authorizer evaluates authorization decisions using Casbin and provides an optional bounded LRU cache.

Policy is loaded at startup. Hot reload is not supported in v1.

func NewAuthorizer

func NewAuthorizer(enforcer *casbin.Enforcer, policyVersion string, opts ...AuthorizerOption) (*Authorizer, error)

NewAuthorizer constructs an Authorizer.

policyVersion MUST be the deterministic policy hash for the effective policy set used to create enforcer.

func (*Authorizer) Authorize

func (a *Authorizer) Authorize(ctx context.Context, principal Principal, object, action string) (Decision, *AuthzResult)

func (*Authorizer) PolicyVersion

func (a *Authorizer) PolicyVersion() string

Authorize evaluates whether principal may perform action on object.

Services should typically call this from HTTP middleware. PolicyVersion returns the policy version hash (deterministic identity) for the effective policy loaded into this authorizer.

type AuthorizerOption

type AuthorizerOption func(*Authorizer)

AuthorizerOption configures an Authorizer.

func WithDecisionCache

func WithDecisionCache(size int) AuthorizerOption

WithDecisionCache enables a bounded LRU cache for authorization decisions. size <= 0 disables the cache.

func WithDecisionCacheFromEnv

func WithDecisionCacheFromEnv() AuthorizerOption

WithDecisionCacheFromEnv enables cache when TOKENSMITH_AUTHZ_CACHE_SIZE is a positive integer.

type AuthzResult

type AuthzResult struct {
	PolicyVersion string   `json:"policy_version"`
	MatchedRoles  []string `json:"matched_roles,omitempty"`
	Reason        string   `json:"reason"`
	Cached        bool     `json:"cached"`
}

AuthzResult provides additional details about an authorization decision.

Services may log fields from this struct for troubleshooting.

Note: Policy loading is performed at startup and is not hot-reloaded in v1. A restart is required to pick up policy changes.

type BadRequestError

type BadRequestError interface {
	error
	BadRequest() bool
}

BadRequestError marks an error as being caused by a malformed request. AuthZ middleware will translate this to reason=bad_request and HTTP 400.

Use cases:

  • malformed URL escapes during path normalization
  • invalid/missing required header used for domain routing

The concrete error message SHOULD be stable and SHOULD NOT include sensitive values.

type Decision

type Decision string

Decision is the outcome taxonomy returned by the core evaluator.

See docs/authz_contract.md.

const (
	DecisionAllow         Decision = "allow"
	DecisionDeny          Decision = "deny"
	DecisionIndeterminate Decision = "indeterminate"
	DecisionError         Decision = "error"
)

type DecisionRecord

type DecisionRecord struct {
	PrincipalID   string   `json:"principal_id"`
	PrincipalType string   `json:"principal_type,omitempty"`
	Roles         []string `json:"roles,omitempty"`
	RolesCount    int      `json:"roles_count"`

	Object string `json:"object"`
	Action string `json:"action"`
	Domain string `json:"domain,omitempty"`

	Decision      Decision `json:"decision"`
	Reason        Reason   `json:"reason"`
	Mode          Mode     `json:"mode"`
	PolicyVersion string   `json:"policy_version"`

	Method    string `json:"method"`
	Path      string `json:"path"`
	RequestID string `json:"request_id,omitempty"`
}

DecisionRecord is a safe-to-log summary of an AuthZ decision.

Contract:

  • MUST NOT include raw JWT strings.
  • MUST NOT include arbitrary claim values.
  • SHOULD only include roles if they are considered non-sensitive in your deployment.

DecisionRecord is intended to be emitted once per request in SHADOW and ENFORCE modes when the request is evaluated (i.e., not in OFF mode and not bypassed as public).

It is provided to the OnDecision hook.

type DenyCode

type DenyCode string

DenyCode is a stable, machine-readable denial code.

See docs/authz-spec.md.

type DenyResponseV1

type DenyResponseV1 struct {
	SchemaVersion string           `json:"schema_version"`
	Code          DenyCode         `json:"code"`
	Message       string           `json:"message"`
	Decision      Decision         `json:"decision"`
	Reason        Reason           `json:"reason"`
	Mode          string           `json:"mode"`
	Principal     PrincipalSummary `json:"principal"`
	Input         Input            `json:"input"`
	PolicyVersion string           `json:"policy_version"`
	Request       RequestSummary   `json:"request"`
	RequestID     string           `json:"request_id,omitempty"`
	Details       map[string]any   `json:"details,omitempty"`
}

DenyResponseV1 is the frozen deny response schema (authz.deny.v1).

This struct is used by both AuthN and AuthZ layers.

type DenyWriter

type DenyWriter struct{}

DenyWriter writes a DenyResponseV1 as JSON.

It is safe to use for both AuthN and AuthZ denial responses.

Contract: - Sets Content-Type to application/json; charset=utf-8 - For HEAD requests, writes headers/status but suppresses the body. - Must not log sensitive values (DenyWriter does not log). - Ensures policy_version and mode are present even if empty.

func (DenyWriter) Write

func (DenyWriter) Write(w http.ResponseWriter, r *http.Request, status int, resp DenyResponseV1) error

type ErrorCode

type ErrorCode string

ErrorCode is a stable, machine-readable code used in AuthZ error responses.

See docs/authz_contract.md.

const (
	ErrorCodeAuthzDenied        ErrorCode = "AUTHZ_DENIED"
	ErrorCodeAuthzIndeterminate ErrorCode = "AUTHZ_INDETERMINATE"
	ErrorCodeAuthzError         ErrorCode = "AUTHZ_ERROR"
)

type ErrorResponse

type ErrorResponse struct {
	Code          ErrorCode `json:"code"`
	Message       string    `json:"message"`
	RequestID     string    `json:"request_id,omitempty"`
	PolicyVersion string    `json:"policy_version"`
	Decision      Decision  `json:"decision"`
}

ErrorResponse is the standard JSON error schema returned by TokenSmith AuthZ middleware when a request is denied in enforce mode.

See docs/authz_contract.md.

type Input

type Input struct {
	Object string `json:"object"`
	Action string `json:"action"`
	Domain string `json:"domain,omitempty"`
}

Input is the normalized authorization tuple passed to the evaluator and returned in deny responses.

Domain is always present in the type shape to keep the contract stable; it may be empty when domains are unused.

type MethodToAction

type MethodToAction func(method string) string

MethodToAction maps an HTTP method to a Casbin action string.

func MethodToActionLiteral

func MethodToActionLiteral() MethodToAction

MethodToActionLiteral returns action = method (as received).

func MethodToActionREST

func MethodToActionREST() MethodToAction

MethodToActionREST returns a REST-ish action mapping:

  • GET/HEAD -> read
  • POST/PUT/PATCH -> write
  • DELETE -> delete
  • other -> method literal

type Middleware

type Middleware struct {
	Authorizer *Authorizer
	Mapper     RouteMapper

	Mode                 Mode
	RequireAuthn         bool
	AllowUnmapped        bool
	PublicPrefixes       []string
	PublicRegexps        []func(string) bool
	RequestIDFromContext func(context.Context) string

	// Observability hook (optional). Called once per evaluated request in SHADOW
	// and ENFORCE.
	OnDecision OnDecisionHook

	// IncludeRolesInDecisionRecord controls whether DecisionRecord contains the
	// role names. If false, only RolesCount is set.
	IncludeRolesInDecisionRecord bool

	DenyWriter DenyWriter
}

Middleware evaluates authorization decisions for incoming requests.

It is net/http compatible and can be used with any router.

Public bypass:

This middleware does not implement router-specific per-route bypass. Use PublicPrefixes/PublicRegexps (or a router-specific helper) to skip authz.

When the request is public, middleware will call next without evaluation even in enforce mode.

Mapping:

Services must supply a RouteMapper to convert requests into Casbin input. See PathMethodMapper for a Casbin-native path/method style.

RequireAuthn:

When enabled, the absence of a principal yields 401 even in shadow/enforce. AuthN middleware should run before this middleware.

See docs/authz-spec.md for decision semantics.

func NewMiddleware

func NewMiddleware(authorizer *Authorizer, mapper RouteMapper, opts ...MiddlewareOption) *Middleware

NewMiddleware constructs authz middleware.

func (*Middleware) Handler

func (m *Middleware) Handler(next http.Handler) http.Handler

type MiddlewareOption

type MiddlewareOption func(*Middleware)

MiddlewareOption configures authz Middleware.

func WithAllowUnmapped

func WithAllowUnmapped(allow bool) MiddlewareOption

func WithIncludeRolesInDecisionRecord

func WithIncludeRolesInDecisionRecord(include bool) MiddlewareOption

WithIncludeRolesInDecisionRecord controls whether DecisionRecord includes role names in addition to RolesCount.

func WithMode

func WithMode(mode Mode) MiddlewareOption

func WithOnDecision

func WithOnDecision(h OnDecisionHook) MiddlewareOption

WithOnDecision installs an optional observability hook invoked once per evaluated request in SHADOW and ENFORCE (not in OFF and not for public bypass).

func WithPublicPrefixes

func WithPublicPrefixes(pfx []string) MiddlewareOption

func WithPublicRegexps

func WithPublicRegexps(rs ...func(string) bool) MiddlewareOption

func WithRequestIDFromContext

func WithRequestIDFromContext(f func(context.Context) string) MiddlewareOption

func WithRequireAuthn

func WithRequireAuthn(req bool) MiddlewareOption

type Mode

type Mode string

Mode controls how authorization outcomes impact request handling.

See docs/authz_contract.md.

const (
	ModeOff     Mode = "off"
	ModeShadow  Mode = "shadow"
	ModeEnforce Mode = "enforce"
)

type OnDecisionHook

type OnDecisionHook func(ctx context.Context, rec DecisionRecord)

OnDecisionHook is invoked with a DecisionRecord for observability.

Implementations MUST be fast and MUST NOT block request handling. Callers should treat ctx as request-scoped and not retain it.

type PathMethodMapper

type PathMethodMapper struct {
	MethodToAction MethodToAction
	DomainFunc     func(r *http.Request, p Principal) (string, error)
}

PathMethodMapper is a RouteMapper implementation that feeds Casbin with:

  • object = normalized URL path
  • action = normalized method (literal or REST-ish)
  • domain = optional extractor

Public bypass is NOT handled here; middleware remains the single owner.

func (PathMethodMapper) Map

type Principal

type Principal struct {
	// ID is the stable identifier for the principal (user id, client id, etc.).
	ID string

	// Roles is the set of RBAC roles assigned to the principal.
	Roles []string
}

Principal is the normalized caller identity used for authorization.

Services are responsible for mapping authenticated identity (e.g. JWT/OIDC claims) into this structure.

Roles are expected WITHOUT the "role:" prefix (e.g. "admin", "viewer"). The Authorizer will apply the required Casbin subject prefix.

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) (*Principal, bool)

PrincipalFromContext returns the principal previously attached to ctx.

type PrincipalSummary

type PrincipalSummary struct {
	ID    string   `json:"id"`
	Type  string   `json:"type"`
	Roles []string `json:"roles,omitempty"`
}

PrincipalSummary is safe to log and safe to return to clients.

Redaction rules (contract):

  • MUST NOT include raw JWTs.
  • MUST NOT include arbitrary claims.
  • SHOULD only include role/group identifiers if they are considered non-sensitive in your deployment.

Note: PrincipalSummary is intentionally decoupled from JWT claim structs. Services can populate it from any identity system.

type Reason

type Reason string

Reason is the coarse reason category for a denial.

See docs/authz-spec.md.

const (
	ReasonNoPrincipal   Reason = "no_principal"
	ReasonInvalidToken  Reason = "invalid_token"
	ReasonPolicyDenied  Reason = "policy_denied"
	ReasonUnmappedRoute Reason = "unmapped_route"
	ReasonEngineError   Reason = "engine_error"
	ReasonBadRequest    Reason = "bad_request"
)

type RequestSummary

type RequestSummary struct {
	Method string `json:"method"`
	Path   string `json:"path"`
}

type RouteDecision

type RouteDecision struct {
	Public bool
	Mapped bool
	Object string
	Action string
	Domain string
}

RouteDecision is the service-owned mapping output that TokenSmith uses as Casbin input.

Ownership contract:

  • TokenSmith middleware owns *public bypass* decisions.
  • RouteMapper MUST NOT attempt to enforce bypass; it only maps.
  • RouteMapper MUST be pure/fast and MUST NOT perform I/O.

Public is included for historical compatibility with early experiments, but is ignored by TokenSmith middleware.

Mapped indicates whether TokenSmith should treat the request as having a known mapping to (Object, Action[, Domain]). In enforce mode, unmapped requests are denied-by-default unless explicitly configured otherwise.

Object and Action are service-defined identifiers (or normalized path/method in path/method mode). They MUST NOT be derived from user-provided params.

Domain is optional and may be empty if domains are unused.

See docs/authz-spec.md for wire semantics.

type RouteMapper

type RouteMapper interface {
	Map(r *http.Request, p Principal) (RouteDecision, error)
}

RouteMapper maps an HTTP request + verified principal to a RouteDecision.

Contract:

  • Map MUST be deterministic and fast.
  • Map MUST NOT perform I/O.
  • Map MUST NOT mutate the request.
  • Map MUST NOT decide public bypass; middleware is the single owner of that behavior.

Error contract:

  • If Map returns a non-nil error, middleware MUST treat the request as a deterministic denial with reason either:
  • bad_request (HTTP 400) for errors that implement BadRequestError
  • engine_error (HTTP 500) for all other errors

Mapper implementations SHOULD use BadRequestError for malformed inputs that can be attributed to the request itself (e.g., invalid header used for domain selection).

NOTE: callers should prefer returning (RouteDecision{Mapped:false}, nil) for unknown routes rather than an error.

See also: BadRequestError.

Directories

Path Synopsis
Package chi provides chi-specific authorization middleware and route helpers implementing the TokenSmith AuthZ contract.
Package chi provides chi-specific authorization middleware and route helpers implementing the TokenSmith AuthZ contract.
Package engine constructs a Casbin-backed Authorizer.
Package engine constructs a Casbin-backed Authorizer.
Package policyloader loads Casbin model and policy artifacts (policy + grouping fragments) deterministically.
Package policyloader loads Casbin model and policy artifacts (policy + grouping fragments) deterministically.
Package presets provides convenience Casbin model presets.
Package presets provides convenience Casbin model presets.

Jump to

Keyboard shortcuts

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