policy

package
v0.2.7 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: MPL-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package policy gates handlers behind capability checks. Capabilities follow Apache Shiro's `domain:instance:action` shape with `*` wildcards at any segment:

opstack:*:read     — read any opstack
opstack:abc:read   — read specifically opstack "abc" (future)
opstack:*:*        — any action on any opstack
*:*:*              — chassis-wide admin

Two aliases stay valid for muscle memory and back-compat:

"admin:all" ⇄ "*:*:*"
"*"         ⇄ "*:*:*"

v1 doesn't issue per-instance checks — every want/grant uses `*` in the instance slot — but the matcher already supports them so adding per-resource scoping later is a one-line handler change.

Tenant scoping is enforced at the URL prefix (`/v1/tenants/{t}/…`) in the admin mux: the tenant resolver loads the caller's membership for that tenant and replaces auth.Context.Capabilities with the scoped set BEFORE the handler runs. So RequireCapability itself stays a pure list-match — what changes between routes is which list it sees on the context.

Two short-circuits live here:

  • SuperAdmin (signed actor with actors.super_admin = 1): allow every RequireCapability call regardless of memberships.
  • basic-auth / open-dev callers: the auth middleware already synthesized admin:all for them, so they pass the list match. RequireSuperAdmin also treats these two sources as operators (chassis credentials). Browser/signed sources are NOT — they need the real super_admin flag.

Index

Constants

This section is empty.

Variables

View Source
var ErrCapabilityDenied = errors.New("capability_denied")

ErrCapabilityDenied is returned by RequireCapability when none of the caller's granted capabilities match the requested one. Middleware maps this to HTTP 403 with the standard error code.

View Source
var ErrUnknownCapability = errors.New("unknown_capability")

ErrUnknownCapability is returned by ValidateCapabilities and ParseCapabilities for any string outside KnownCapabilities. The offending value is wrapped via fmt.Errorf so callers can render it to users; an errors.Is check against this sentinel still works.

View Source
var KnownCapabilities = map[string]bool{

	"admin:all": true,
	"*":         true,
	"*:*:*":     true,

	"opstack:*:read":     true,
	"opstack:*:update":   true,
	"opstack:*:activate": true,
	"opstack:*:*":        true,

	"actor:*:read":   true,
	"actor:*:invite": true,
	"actor:*:revoke": true,
	"actor:*:*":      true,

	"hostname:*:read":  true,
	"hostname:*:write": true,
	"hostname:*:*":     true,

	"secret:*:read":  true,
	"secret:*:write": true,
	"secret:*:*":     true,

	"dns:*:read":  true,
	"dns:*:write": true,
	"dns:*:*":     true,
}

KnownCapabilities is the canonical v2 whitelist. The matcher does not consult this map — it accepts any well-formed 3-segment string and wildcards. The whitelist is a write-time guard: ParseCapabilities and the server's invite + grant-member handlers refuse anything outside it so users don't end up with non-functional roles from a typo.

Adding per-instance grants later means relaxing the validator to accept "<known-domain>:<id-or-*>:<known-action>", not editing this list per resource.

Functions

func Canonical

func Canonical(cap string) string

Canonical returns the storage form of a capability string. The matcher tolerates either shape; we canonicalize on write so the database stays consistent and `txco auth whoami` doesn't render the same role two different ways depending on history.

"admin:all"      → "*:*:*"
"*"              → "*:*:*"
"opstack:read"   → "opstack:*:read"   (2-seg legacy normalisation)
"opstack:*:read" → unchanged

Returns "" for empty input. Returns the original (unchanged) for anything with 4+ colons — those fail validation downstream.

func Covers

func Covers(grants []string, want string) bool

Covers reports whether `want` is matched by any string in `grants`. Same wildcard rules as RequireCapability (3-segment, segment-by- segment), used at write time by the anti-privilege-escalation guards on /auth/invitations and /auth/members so a granter can't hand out capabilities they don't have themselves.

func CoversAll

func CoversAll(grants, wants []string) string

CoversAll returns the first `want` capability not covered by `grants`, or "" if all are covered. The empty-string sentinel lets callers branch on (`if missing := CoversAll(...); missing != ""`) instead of allocating an error in the happy path. The caller formats the surface error (HTTP 403 + denied_capability body).

func HasCapability

func HasCapability(ctx context.Context, want string) bool

HasCapability is the non-erroring shorthand. Useful when a handler wants to branch on whether a caller can do something without outright denying the request.

func ParseCapabilities

func ParseCapabilities(s string) ([]string, error)

ParseCapabilities is the user-facing entrypoint used by the CLI's `--caps` flag. It splits on commas, trims whitespace, dedupes, canonicalizes each entry, and validates against the whitelist. Empty input returns (nil, nil) so callers can apply their own default (e.g. `["admin:all"]` for the invitation flow).

Errors are wrapped with %w so callers can errors.Is against ErrUnknownCapability for typed handling.

func RequireCapability

func RequireCapability(ctx context.Context, want string) error

RequireCapability checks the auth context attached to ctx and returns nil iff the caller holds a capability matching `want`. If the context is unset (a path that bypasses auth like /healthz) or no granted capability matches, returns ErrCapabilityDenied.

`want` is a 3-segment capability string (`domain:instance:action`). 2-segment legacy strings (`opstack:read`) are normalised at compare time, so a stale call site won't silently miss its grant. Granted capabilities are compared segment-by-segment, with `*` matching anything at that position. The aliases `admin:all` and bare `*` expand to `*:*:*`.

func RequireSuperAdmin

func RequireSuperAdmin(ctx context.Context) error

RequireSuperAdmin gates a chassis-wide endpoint behind the super_admin flag (or a basic-auth/open-dev operator). Used for chassis-wide actions — tenant CRUD, global DNS config — that no per-tenant membership should be able to perform. A browser session passes only if it carries the real super_admin flag it snapshotted at bootstrap; a plain tenant-member session does not.

func ValidateCapabilities

func ValidateCapabilities(caps []string) error

ValidateCapabilities returns a wrapped ErrUnknownCapability the first time it sees a string outside KnownCapabilities. Inputs are canonicalized before lookup so "admin:all" and "*:*:*" both pass.

An empty slice is valid — callers (the invite handler) supply the back-compat default elsewhere when no caps are sent.

Types

This section is empty.

Jump to

Keyboard shortcuts

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