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 ¶
- Variables
- func Canonical(cap string) string
- func Covers(grants []string, want string) bool
- func CoversAll(grants, wants []string) string
- func HasCapability(ctx context.Context, want string) bool
- func ParseCapabilities(s string) ([]string, error)
- func RequireCapability(ctx context.Context, want string) error
- func RequireSuperAdmin(ctx context.Context) error
- func ValidateCapabilities(caps []string) error
Constants ¶
This section is empty.
Variables ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.