Documentation
¶
Overview ¶
Package policy defines the policy document an administrator writes: its types, its strict JSON decoding and the limits and identifier formats every policy must meet.
It decodes and checks the document's shape only; deciding what a policy means is the compiler's job, and the package never reads storage, the network or the clock. It imports no other internal package, so any gateway package may use it.
Index ¶
- Constants
- Variables
- func CheckID(id, field string, args ...any) error
- func CheckMembers(data []byte, maxDepth int) error
- func Clip(text string) string
- func DecodeBounded(data []byte, maxDepth, maxValues int) (any, string)
- func DecodeJSON(data []byte) (any, string)
- func DecodeStrict(data []byte, value any) error
- func Encode(p Policy) ([]byte, error)
- func IsSubjectID(id string) bool
- func ParseRange(text string) (netip.Prefix, bool)
- type API
- type BodyRule
- type CORS
- type Forward
- type HeaderCondition
- type Limits
- type Operation
- type Permit
- type Policy
- type PolicyError
- type RateRule
- type ResponseHeaders
- type Role
- type Upstream
- type When
Constants ¶
const ( DefaultCORSMaxAge = 600 MaxCORSMaxAge = 86_400 MaxCORSOrigins = 32 MaxCORSHeaders = 32 MaxCORSExpose = 16 MaxRemoveHeaders = 16 MaxAllowAddresses = 256 )
Limits of the per-API settings.
const ( ForwardExtension = "x-apiward-forward" CORSExtension = "x-apiward-cors" ResponseHeadersExtension = "x-apiward-response-headers" AddressesExtension = "x-apiward-addresses" )
OpenAPI root members carrying an API's settings; one API per description.
const ( DefaultPolicyBytes = 64 << 10 DefaultAPIs = 64 DefaultOperations = 256 DefaultRoles = 64 DefaultPermits = 4096 DefaultRateCounters = 262144 DefaultWindow = 10000 MaxPolicyBytes = 64 << 10 MaxAPIs = 256 MaxOperations = 4096 MaxRoles = 256 MaxPermits = 65536 MaxRateCounters = 1 << 20 MaxWindow = 100000 )
Defaults of the capacity ceilings, which a deployment may raise up to the maxima below. A rate counter keeps 257 bytes of heap, so 1,048,576 counters need about 257 MiB. MaxPolicyBytes is the store's own bound on a stored document (its CHECK constraint), so a policy is never accepted that the database would refuse to keep.
const ( MaxJSONDepth = 16 MaxJSONValues = 4096 MaxNumberLength = 32 MaxSchemaProperties = 64 MaxEnumValues = 64 MaxSchemaLength = 1 << 20 MaxHeaderName = 64 )
const ( SchemaVersion = 4 LatestSchemaVersion = 5 )
SchemaVersion is the policy document version written when nothing newer is needed; LatestSchemaVersion adds upstream URLs, TLS profiles and the Host header choice. The compiler accepts both, and compiles a version 4 document exactly as before version 5 existed.
const ( MaxPolicyDepth = 16 MaxTokenRoles = 64 MaxIDLength = 64 MaxSubjectLength = 128 MaxTargetLength = 256 MaxUpstreamURL = 2048 MaxBodyLimit = 1 << 20 )
MaxTokenRoles bounds the roles one token may carry, whatever the policy's role ceiling: it is part of the token size bound.
const ( MaxWhenHosts = 16 MaxWhenHeaders = 8 )
const ProfileCore = "core"
ProfileCore is the only enforcement profile that can be activated. Under it the gateway enforces routing, the per-operation access flag, bearer JWT authentication and role permits on every operation declared auth "required", the operation's rate rule, its body and header schemas, and the mandatory audit record before forwarding. Any other profile is rejected rather than activated with controls it cannot run.
const WhenExtension = "x-apiward-when"
WhenExtension is the OpenAPI operation member that carries a When.
Variables ¶
var AllowedMethods = map[string]bool{ http.MethodGet: true, http.MethodHead: true, http.MethodPost: true, http.MethodPut: true, http.MethodPatch: true, http.MethodDelete: true, http.MethodOptions: true, }
Functions ¶
func CheckID ¶
CheckID accepts short identifiers made of letters, digits, '.', '_' and '-', starting with a letter or digit, so identifiers are safe to log and to compare byte for byte.
func CheckMembers ¶
CheckMembers rejects JSON nested deeper than maxDepth, and objects with a repeated member name or a name outside [a-z0-9_]. encoding/json keeps the last of repeated names and matches names case-insensitively, so without this check two readers of the same document could disagree about it.
Values of members named "schema" or "headers" are JSON Schemas, whose keywords and property names are case-sensitive and not limited to [a-z0-9_]; the "headers" lists of "when" and "cors" objects are not. They are decoded as raw JSON, never into Go structs, so only the repeated-name and depth rules apply inside them.
func Clip ¶
Clip shortens untrusted text to 256 bytes, without splitting a UTF-8 sequence, before it is echoed in a report or an error.
func DecodeBounded ¶
DecodeBounded is DecodeJSON with the caller's nesting and value limits.
func DecodeJSON ¶
DecodeJSON decodes exactly one JSON value into the form the schema library validates (maps, slices, json.Number, strings, booleans and nil). It rejects invalid UTF-8, repeated member names, data after the value, nesting deeper than MaxJSONDepth, more than MaxJSONValues values and number literals the library could not compare cheaply. encoding/json alone would keep the last repeated member and replace invalid UTF-8, so the gateway and an upstream could read different documents. It returns the value, or the rejection reason.
func DecodeStrict ¶
DecodeStrict decodes exactly one JSON value, rejecting unknown fields and anything after the value.
func Encode ¶
Encode returns the canonical form of p: 2-space JSON with members in struct order and a trailing newline; upstreams, APIs and roles sorted by id; operations by API, target and method; permits by role and operation. Schemas have their members sorted and each "required" list sorted; condition hosts are sorted, header conditions sorted by canonical name; an API's origins, methods and address ranges are sorted and its header names sorted in canonical form. The input is not changed. It fails only on a schema that is not valid JSON.
func IsSubjectID ¶
IsSubjectID accepts 1 to 128 printable ASCII characters without spaces. The same rule applies to the "sub" claim, so a subject identifier is always safe to log and to forward in a header.
Types ¶
type API ¶
type API struct {
ID string `json:"id"`
Upstream string `json:"upstream"`
Forward *Forward `json:"forward,omitempty"`
CORS *CORS `json:"cors,omitempty"`
ResponseHeaders *ResponseHeaders `json:"response_headers,omitempty"`
// AllowAddresses, when present, lists the only client address ranges
// (CIDR) whose requests reach the API's operations.
AllowAddresses []string `json:"allow_addresses,omitempty"`
}
API registers one managed API and the single upstream that serves it.
type BodyRule ¶
type BodyRule struct {
MaxBytes int64 `json:"max_bytes"`
Schema json.RawMessage `json:"schema"`
}
BodyRule declares a JSON request body, its size ceiling and the JSON Schema it must satisfy. An operation without a BodyRule accepts no request body.
type CORS ¶
type CORS struct {
Origins []string `json:"origins"`
Methods []string `json:"methods"`
Headers []string `json:"headers,omitempty"`
Expose []string `json:"expose,omitempty"`
Credentials bool `json:"credentials,omitempty"`
// MaxAgeSeconds is how long browsers may cache a preflight answer;
// absent means DefaultCORSMaxAge.
MaxAgeSeconds *int64 `json:"max_age_seconds,omitempty"`
}
CORS is the API's cross-origin browser access. The gateway answers preflights itself and adds the allowed origin to responses; it never decides access with it.
type Forward ¶
type Forward struct {
StripPrefix string `json:"strip_prefix,omitempty"`
AddPrefix string `json:"add_prefix,omitempty"`
}
Forward rewrites the path an admitted request is forwarded with: the inbound path without StripPrefix, after AddPrefix. Every decision is made on the inbound path; the rewrite applies only to the outbound request.
In an OpenAPI description the rule is the root member named ForwardExtension.
type HeaderCondition ¶
HeaderCondition holds when the request carries the named header exactly once, with exactly this value. Names compare without regard to case.
type Limits ¶ added in v0.2.0
type Limits struct {
PolicyBytes int
APIs int
Operations int
Roles int
Permits int
RateCounters int
Window int
}
Limits are the capacity ceilings a deployment runs with. A zero member takes its default. APIs bounds the upstreams too; Window is the number of recent requests kept for live views.
type Operation ¶
type Operation struct {
ID string `json:"id"`
API string `json:"api"`
Method string `json:"method"`
Target string `json:"target"`
Access string `json:"access"`
Auth string `json:"auth"`
Body *BodyRule `json:"body,omitempty"`
Headers json.RawMessage `json:"headers,omitempty"` // JSON Schema for declared request headers
Rate *RateRule `json:"rate,omitempty"`
When *When `json:"when,omitempty"`
// Modules holds the members named by compiled-in modules other than the
// built-ins, keyed by module ID; it is encoded as those members.
Modules map[string]json.RawMessage `json:"-"`
}
Operation binds a method and target of an API to an access decision and the controls that apply to it.
func (Operation) MarshalJSON ¶
MarshalJSON writes the operation's own members and then its module members in ID order. Without module members the output is exactly the default encoding.
type Permit ¶
Permit grants a role an operation. Roles have no hierarchy: a permit applies to the named role only.
type Policy ¶
type Policy struct {
SchemaVersion int `json:"schema_version"`
Profile string `json:"profile"`
Upstreams []Upstream `json:"upstreams"`
APIs []API `json:"apis"`
Operations []Operation `json:"operations"`
Roles []Role `json:"roles"`
Permits []Permit `json:"permits"`
}
Policy is the complete permanent policy document. Every publication carries a whole document; there is no partial update.
func DecodePolicy ¶
DecodePolicy parses a policy document. It rejects input larger than limits.PolicyBytes (zero: the default) or deeply nested, repeated or non-lowercase member names, unknown fields, unsupported schema versions and trailing data. Limits out of range are refused as LIMIT_EXCEEDED.
type PolicyError ¶
PolicyError is a typed candidate rejection. Field is the JSON path of the offending value when one applies.
func Invalid ¶
func Invalid(code, field string, args ...any) *PolicyError
func (*PolicyError) Error ¶
func (e *PolicyError) Error() string
type RateRule ¶
type RateRule struct {
Algorithm string `json:"algorithm"`
Count int64 `json:"count"`
WindowSeconds int64 `json:"window_seconds"`
Key string `json:"key"`
}
RateRule limits requests to an operation by verified subject or direct client address within a sliding window.
type ResponseHeaders ¶
type ResponseHeaders struct {
HSTS bool `json:"hsts,omitempty"`
Nosniff bool `json:"nosniff,omitempty"`
Frame string `json:"frame,omitempty"` // "deny" or "sameorigin"
Referrer string `json:"referrer,omitempty"` // "no-referrer" or "strict-origin-when-cross-origin"
Remove []string `json:"remove,omitempty"`
}
ResponseHeaders adds security headers to the API's responses when the upstream did not set them, and removes the named upstream headers.
type Upstream ¶
type Upstream struct {
ID string `json:"id"`
Dial string `json:"dial,omitempty"`
URL string `json:"url,omitempty"`
TLS string `json:"tls,omitempty"`
Host string `json:"host,omitempty"`
}
Upstream is one entry of the closed destination inventory. It holds exactly one of Dial (a loopback address, plain HTTP) or URL (version 5: an http or https origin); TLS names a TLS profile and Host chooses the Host header ("inbound" or "upstream"), both only with URL.
type When ¶
type When struct {
Hosts []string `json:"hosts,omitempty"`
Headers []HeaderCondition `json:"headers,omitempty"`
}
When lists the conditions an operation's requests must meet: the request host matches one of Hosts (exact, or "*." followed by a domain, which matches any name ending in "."+domain) and every header condition holds. Conditions only select an operation; they never grant access.
In an OpenAPI description the conditions are the operation member named WhenExtension.