Documentation
¶
Overview ¶
Package api serves the operator surface: the public REST/JSON API consumed by the console and by automation, on a listener of its own (ADR-0021). Its handlers are hand-shaped over the shared domain layer (ADR-0007 as amended): they carry the semantics a browser-facing surface needs and hold no domain logic, calling the same domain functions the command line calls. Design rules for estate scale: aggregation is server-side, unbounded reads paginate by cursor, a fan-out mutation returns a recorded intent and never blocks on convergence, and bulk operations exist only where the console's screens demand them.
The read endpoints (a grouped flow rollup, a page of one workload's windows, the fleet and one workload in one shape, a workload's rendered policy) are thin handlers over internal/readmodel: each reads its parameters, calls one read model function, and encodes the result.
The write endpoints (rulesets, rules, services, address groups, a workload's labels, bulk mode changes, the selector preview, the dry-run render, and token management) are thin handlers over internal/policy, internal/fleet, internal/enroll, and internal/operator: each decodes its body, reads the version a conditional write names, calls one domain function, and encodes the result. Admission runs in the domain and its findings are returned as they are; a write without If-Match is refused as precondition required and one naming a stale version as precondition failed with the current one. The contract is api/openapi.yaml.
The package owns the listener's transport rules: TLS only; a middleware that reads a session cookie or a bearer operator token and resolves it, through the injected authn.Authenticator, to the one operator principal; an origin guard on every request that is not a read; a fixed-window throttle on the login endpoint; problem documents for every error. Handlers see a principal and nothing about how it was authenticated.
Index ¶
- Constants
- func ConsoleHandler(tree fs.FS) http.Handler
- func CrossOriginGuard(next http.Handler) http.Handler
- func Fingerprint(der []byte) string
- func ListenerCertificate(certFile, keyFile, dir string, hosts []string, now time.Time, log *slog.Logger) (tls.Certificate, error)
- func NewHTTPServer(tlsCfg *tls.Config, handler http.Handler) *http.Server
- func ServerTLSConfig(cert tls.Certificate) *tls.Config
- type Deps
- type Finding
- type Me
- type Middleware
- type Problem
- type Route
- type Server
- type Surface
- type Throttle
Constants ¶
const ( ProblemUnauthenticated = problemTypeBase + "unauthenticated" ProblemInvalidCredentials = problemTypeBase + "invalid-credentials" ProblemNoPassword = problemTypeBase + "no-password" ProblemCrossOrigin = problemTypeBase + "cross-origin" ProblemTooManyAttempts = problemTypeBase + "too-many-attempts" ProblemInvalidRequest = problemTypeBase + "invalid-request" ProblemInvalidParameter = problemTypeBase + "invalid-parameter" ProblemValidation = problemTypeBase + "validation" ProblemPreconditionRequired = problemTypeBase + "precondition-required" ProblemPreconditionFailed = problemTypeBase + "precondition-failed" ProblemMatchCountMismatch = problemTypeBase + "match-count-mismatch" ProblemDuplicateName = problemTypeBase + "duplicate-name" ProblemInUse = problemTypeBase + "in-use" ProblemAlreadyRevoked = problemTypeBase + "already-revoked" ProblemAgentOffline = problemTypeBase + "agent-offline" ProblemWorkloadRetired = problemTypeBase + "workload-retired" ProblemNotFound = problemTypeBase + "not-found" ProblemMethodNotAllowed = problemTypeBase + "method-not-allowed" ProblemInternal = problemTypeBase + "internal" )
The problem types the surface emits. A console branches on ProblemNoPassword to show its fresh-install state, and on ProblemUnauthenticated to show the login form. ProblemInvalidRequest is a body that cannot be read; ProblemInvalidParameter is a query or path parameter that cannot be, and its detail names the parameter. ProblemValidation is a body that was read and refused by admission, with its findings. ProblemPreconditionRequired is a write without a version; ProblemPreconditionFailed is a write naming a version the resource no longer holds. ProblemMatchCountMismatch is a mode change whose selection resolved to a different number of workloads than the operator expected. ProblemDuplicateName, ProblemInUse, and ProblemAlreadyRevoked are the conflicts an authored or token write can meet. ProblemAgentOffline is a directed reconnect refused because the workload's agent is offline as last recorded. ProblemWorkloadRetired is a write naming a workload the operator retired, whose attributes are frozen, or a second retirement of one (ADR-0023).
const ( DefaultLoginAttempts = 10 DefaultLoginWindow = time.Minute )
Login throttling defaults: attempts per source address per fixed window.
const ( OperatorCertFile = "operator.crt" OperatorKeyFile = "operator.key" )
The files a generated operator certificate is persisted as, beside the authority directory's other operator-provisioned configuration.
const APIPrefix = "/api/v1"
APIPrefix is where the surface is mounted. Every other path belongs to the console.
const ContentTypeProblem = "application/problem+json"
ContentTypeProblem is the media type of every error the surface returns.
const SessionCookie = "innerwall_session"
SessionCookie is the name of the cookie that carries a session identifier.
Variables ¶
This section is empty.
Functions ¶
func ConsoleHandler ¶
ConsoleHandler serves the console tree at the mount point. A nil tree, or one without an entry point, is the mount point answering not found. Otherwise a file at the request path is served as itself; content-addressed files under the asset directory are marked immutable, everything else must be revalidated; and any other path is the entry point, so a deep link into the console loads the console. Only reads are served: the surface's writes are all under the API prefix.
func CrossOriginGuard ¶
CrossOriginGuard refuses every request that is not a read and that a browser marks as coming from another origin. The console is served from the same origin as the façade and the session cookie does not travel on cross-site navigations, so this check is the whole cross-site request forgery defense and no token scheme is needed (ADR-0021).
Two signals are consulted. A browser that sends Sec-Fetch-Site says outright whether the request is same-origin; anything else is refused. A browser that sends only Origin is held to the origin this listener is serving: the request's own host, over TLS. A client that sends neither header is not a browser and is not subject to forgery through ambient credentials, so it passes.
func Fingerprint ¶
Fingerprint is the SHA-256 digest of a certificate's DER encoding, in the colon-separated form a browser's certificate viewer shows.
func ListenerCertificate ¶
func ListenerCertificate(certFile, keyFile, dir string, hosts []string, now time.Time, log *slog.Logger) (tls.Certificate, error)
ListenerCertificate returns the operator listener's certificate. When certFile and keyFile are both set they are loaded and must be valid; when both are empty a self-signed certificate persisted in dir is used, generated on first start or when the persisted one has expired, and its SHA-256 fingerprint is logged either way so the first connection can be verified out of band (ADR-0021). Setting one of the two paths without the other is a configuration error.
func NewHTTPServer ¶
NewHTTPServer wraps the handler in a server with the listener's TLS configuration and header timeouts set.
func ServerTLSConfig ¶
func ServerTLSConfig(cert tls.Certificate) *tls.Config
ServerTLSConfig is the operator listener's TLS configuration: the certificate above, current TLS only, and both HTTP versions offered.
Types ¶
type Deps ¶
type Deps struct {
// Authenticator resolves the credentials the surface is presented,
// behind the login endpoint and the authentication middleware.
Authenticator authn.Authenticator
// Operators is the operator domain, behind operator token management.
Operators *operator.Service
// Reads is the read model the read endpoints call.
Reads *readmodel.Reader
// Authoring is the authored model's write domain, behind the
// ruleset, rule, service, and address group endpoints.
Authoring *policy.Authoring
// Fleet is the fleet write domain, behind label edits, mode
// changes, the selector preview, the dry run, and the directed
// reconnect.
Fleet *fleet.Service
// Enroll is the enrollment domain, behind provisioning token
// management.
Enroll *enroll.Service
// Site is the label the console header shows; empty when none is
// configured.
Site string
// GatewayAddress is the host:port agents reach the agent gateway at,
// as the operator configured it for the console's enroll command;
// empty when none is configured. It is never derived from the bind
// address or the host's interfaces.
GatewayAddress string
// Console is the built console tree served at every path outside the
// API prefix; nil when this build carries none (ADR-0008).
Console fs.FS
// LoginAttempts and LoginWindow configure the login throttle; the
// defaults apply when zero.
LoginAttempts int
LoginWindow time.Duration
Log *slog.Logger
// Now is the clock; time.Now if nil.
Now func() time.Time
}
Deps is what the operator surface is built from.
type Finding ¶
type Finding struct {
Path string `json:"path"`
Rule string `json:"rule"`
Message string `json:"message"`
}
Finding is one admission failure: where it is, which rule it breaks, and what to tell the operator.
type Me ¶
type Me struct {
DisplayName *string `json:"display_name"`
Site string `json:"site"`
GatewayAddress *string `json:"gateway_address"`
Version string `json:"version"`
}
Me is the operator as the console sees it: the display name set with the password (null until one is set, and the console renders a generic fallback), the site label configured on the control plane (empty when none is), the advertised agent gateway address (null when none is configured, and the console keeps its placeholder), and the version of the control plane answering, as internal/buildinfo names it. Only an authenticated request reaches it, so the version is never disclosed before login.
type Middleware ¶ added in v0.5.0
Middleware is one named layer in front of the API routes.
type Problem ¶
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
// Errors are the admission findings of a validation problem, one per
// field the caller must fix.
Errors []Finding `json:"errors,omitempty"`
// CurrentVersion is the version the resource holds now, on a
// precondition-failed problem.
CurrentVersion string `json:"current_version,omitempty"`
// Expected and Matched are the counts of a match-count problem.
Expected *int `json:"expected,omitempty"`
Matched *int `json:"matched,omitempty"`
// LastSeenAt is when the agent was last heard from, on an
// agent-offline problem, where it is always present and null when the
// agent never was.
LastSeenAt *nullableInstant `json:"last_seen_at,omitempty"`
// RetiredAt is when the workload was retired, on a workload-retired
// problem.
RetiredAt string `json:"retired_at,omitempty"`
}
Problem is an error document. Type is a stable identifier a client branches on; the other fields are for people, except the extensions, which carry what a client needs to act: the findings of a refused write, the current version of a resource a stale write named, the two counts of a refused mode change, the last-seen instant of an agent a directed reconnect found offline, and the instant a workload a write named was retired.
type Route ¶ added in v0.5.0
type Route struct {
Method string
Pattern string
Handler http.HandlerFunc
}
Route is one endpoint: a method and a path pattern whose segments are literals or one-segment parameters written {name}.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server holds the surface's handlers and the listener's middleware. The handlers are hand-shaped over the domain layer (ADR-0007 as amended): each carries the transport semantics of its endpoint and calls the same domain functions the command line calls, holding no logic of its own.
type Surface ¶ added in v0.5.0
type Surface struct {
// Middleware wraps the API routes, outermost first.
Middleware []Middleware
// Routes are the endpoints under APIPrefix.
Routes []Route
// Console serves every path outside APIPrefix.
Console http.Handler
}
Surface is the composition of the operator listener's handler: the middleware in front of the API routes, the routes under APIPrefix, and the console at every other path. The listener's handler is built from it and from nothing else, so what the surface serves, and in what order its middleware runs, is read off this structure.
func (*Surface) Handle ¶ added in v0.5.0
func (sf *Surface) Handle(method, pattern string, h http.HandlerFunc)
Handle registers an endpoint.
func (*Surface) Handler ¶ added in v0.5.0
Handler builds the listener's handler: the routes behind the middleware, outermost first, under APIPrefix, and the console at every other path.
func (*Surface) RouteNames ¶ added in v0.5.0
RouteNames lists every method and path pattern registered, in the form "METHOD /api/v1/path/{param}", sorted.
func (*Surface) Use ¶ added in v0.5.0
func (sf *Surface) Use(m Middleware)
Use appends a middleware inside those already added.
type Throttle ¶
type Throttle struct {
// Limit is the number of attempts allowed per window;
// DefaultLoginAttempts if zero.
Limit int
// Window is the fixed window length; DefaultLoginWindow if zero.
Window time.Duration
// Now is the clock; time.Now if nil.
Now func() time.Time
// contains filtered or unexported fields
}
Throttle is a fixed-window attempt limiter keyed by source address. It lives in one process and protects that process: a brake on online password guessing, not a distributed rate limiter (ADR-0021). The key is the connection's own address, never a forwarded header, so a proxy in front collapses every client into one source and the brake tightens accordingly. The table is bounded (throttleMaxSources), so the brake is bounded too: a guesser that rotates through more source addresses than the bound within one window pushes its own oldest windows out and resets their counts. Against that, the throttle costs the process bounded memory and constant work per attempt, and nothing more is claimed; protection from a distributed guesser sits in front of the control plane (ADR-0021).
func (*Throttle) Allow ¶
Allow records one attempt from key and reports whether it is within the window's budget; when it is not, retryAfter is the time until the window resets.
func (*Throttle) Middleware ¶
Middleware applies the throttle to the login route only.