Documentation
¶
Overview ¶
Package auth splits request handling into two deliberately separate seams:
- Authenticator — "who are you": a bearer token → a Principal. A token the control plane will not vouch for yields an *Error with status 401.
- Authorizer — "may you act on this project": a decision over a Principal and a project name, yielding an *Error with status 403 on deny.
Both are answered by the control plane and nothing else. Identity comes from a Kubernetes TokenReview; project access from a SubjectAccessReview, resolved by the platform's OpenFGA-backed webhook. The service holds no credential database of its own and never decides access locally, so there is no path where a token's own contents can widen what it may reach.
Both paths fail closed: any error, timeout or undecidable answer is a rejection, never a grant.
Index ¶
- Constants
- func BearerTokenFromContext(ctx context.Context) string
- func ContextWithBearerToken(ctx context.Context, token string) context.Context
- func ExtractBearerToken(authorization string) string
- type Authenticator
- type Authorizer
- type Error
- type Principal
- type ResourceAttributes
- type SARConfig
- type SubjectAccessReview
- type SubjectAccessReviewSpec
- type SubjectAccessReviewStatus
- type SubjectAccessReviewer
- type TokenReview
- type TokenReviewConfig
- type TokenReviewSpec
- type TokenReviewStatus
- type TokenReviewer
- type UserInfo
Constants ¶
const ( DefaultSARGroup = "assistant.miloapis.com" DefaultSARResource = "conversations" DefaultSARVerb = "create" // DefaultSARTimeout bounds a single SAR round-trip. A hung control plane // must fail closed (deny), never stall a request indefinitely. DefaultSARTimeout = 5 * time.Second // DefaultSARCacheTTL is how long an ALLOW decision is reused before the // next control-plane round-trip. Kept short: a revoked user keeps access // for at most this window (acceptable staleness), while a JUST-granted user // is never locked out — denies are never cached, so the very next request // re-checks and permits immediately. DefaultSARCacheTTL = 60 * time.Second )
Defaults for SARConfig. The resourceAttributes triple models the question "may this subject start assistant work in this project?" — a create on the assistant's conversations resource, scoped to the project's namespace. All three are overridable so a deployment can retune the check without a code change (the service-shell workstream sets them from config).
const ( // DefaultTokenReviewTimeout bounds a single TokenReview round-trip. A hung // control plane must fail closed (reject the token), never stall a request. DefaultTokenReviewTimeout = 5 * time.Second // DefaultTokenReviewCacheTTL is how long a successful identity resolution is // reused before the next control-plane round-trip. Kept short: a revoked // token keeps working for at most this window (acceptable staleness). DefaultTokenReviewCacheTTL = 60 * time.Second )
Defaults for TokenReviewConfig, mirroring the SAR side.
Variables ¶
This section is empty.
Functions ¶
func BearerTokenFromContext ¶
BearerTokenFromContext returns the caller's raw bearer token, or "" when the request carried none (any non-HTTP entry point, and every unauthenticated path). Callers must treat "" as "no identity to forward", never as an error.
func ContextWithBearerToken ¶
ContextWithBearerToken stashes the caller's RAW bearer token on ctx. Only an already-authenticated request should have one attached.
Deliberately not a field on Principal: a Principal is passed around, compared, and logged, and a credential must never ride along with an identity.
func ExtractBearerToken ¶
ExtractBearerToken pulls the token out of an "Authorization: Bearer <token>" header value. It returns "" when absent or malformed.
Types ¶
type Authenticator ¶
type Authenticator interface {
Authenticate(ctx context.Context, bearerToken string) (Principal, error)
}
Authenticator resolves a bearer token to a Principal, or returns a 401 *Error.
func NewAuthenticator ¶
func NewAuthenticator(_ context.Context, cfg *config.Config, logger *slog.Logger) (Authenticator, error)
NewAuthenticator builds the TokenReview authenticator: a bearer token is resolved to an identity by the control plane, which is the only thing that can vouch for it. There is no local token store to fall back to.
func NewTokenReviewAuthenticator ¶
func NewTokenReviewAuthenticator(cfg TokenReviewConfig) (Authenticator, error)
NewTokenReviewAuthenticator builds the production TokenReview-based Authenticator. It returns an error only on misconfiguration (no Reviewer and no APIURL, or an invalid CA bundle); once constructed it never fails open — Authenticate rejects on any review failure.
type Authorizer ¶
type Authorizer interface {
AuthorizeProject(ctx context.Context, principal Principal, projectName string) error
}
Authorizer decides whether principal may act on projectName. It returns nil on allow and a 403 *Error on deny. Context-aware so a control-plane SubjectAccessReview can slot in behind this interface unchanged.
func NewAuthorizer ¶
NewAuthorizer builds the SubjectAccessReview authorizer: project access is decided by the control plane per request, never from the credential's own contents.
func NewSubjectAccessReviewAuthorizer ¶
func NewSubjectAccessReviewAuthorizer(cfg SARConfig) (Authorizer, error)
NewSubjectAccessReviewAuthorizer builds the production SAR-based Authorizer. It returns an error only on misconfiguration (no Reviewer and no APIURL); once constructed it never fails open — AuthorizeProject denies on any SAR failure.
type Error ¶
type Error struct {
// Status is 401 (authentication) or 403 (authorization).
Status int
Message string
}
Error is an authentication (401) or authorization (403) failure.
func Unauthenticated ¶
Unauthenticated returns a 401 *Error.
type Principal ¶
type Principal struct {
// Subject is the stable identifier the control plane returned for the
// token (the TokenReview's user.username).
Subject string
// UID and Groups are the rest of the identity the TokenReview resolved.
//
// They are not decoration: Milo's authorizer binds policy to a user's ID,
// not to the username string, so a SubjectAccessReview carrying only
// Subject is answered "not allowed" even when the caller genuinely has the
// grant. Carry the whole identity through to the authorization call.
UID string
Groups []string
// Extra is whatever additional context the control plane attached to the
// identity. Carried through to authorization rather than dropped.
Extra map[string][]string
}
Principal is the authenticated identity, as resolved by the control plane.
It carries no grants. What a subject may reach is decided per request by the Authorizer against the control plane — deliberately not derivable from the credential, so a token cannot describe its own authority.
type ResourceAttributes ¶
type ResourceAttributes struct {
Namespace string `json:"namespace,omitempty"`
Verb string `json:"verb,omitempty"`
Group string `json:"group,omitempty"`
Resource string `json:"resource,omitempty"`
Name string `json:"name,omitempty"`
}
ResourceAttributes scopes the access check to a specific verb/group/resource in a namespace (the project's scope).
type SARConfig ¶
type SARConfig struct {
// APIURL is the control-plane API base URL the SAR is POSTed to
// (e.g. https://kubernetes.default.svc). Required unless Reviewer is set.
APIURL string
// BearerToken is the ASSISTANT's own service-account token, sent as the
// Authorization header so the apiserver authenticates the caller. This is
// distinct from the User in the SAR body (the principal under review).
// Ignored when Reviewer is injected.
BearerToken string
// CACert is the PEM-encoded apiserver CA bundle. Empty falls back to the
// system roots. Ignored when Reviewer is injected.
CACert []byte
// ClientCert/ClientKey are the PEM-encoded client certificate the assistant
// presents to identify ITSELF to the control plane, as an alternative to
// BearerToken. Both or neither. Ignored when Reviewer is injected.
ClientCert []byte
ClientKey []byte
// Group, Resource, Verb are the resourceAttributes the SAR asks about.
// Empty fields fall back to the Default* constants.
Group string
Resource string
Verb string
// Timeout bounds a single SAR round-trip. Zero uses DefaultSARTimeout.
Timeout time.Duration
// CacheTTL bounds how long an ALLOW is reused. Zero uses DefaultSARCacheTTL;
// a negative value disables the cache (every request round-trips).
CacheTTL time.Duration
// Logger records refused reviews. Optional.
Logger *slog.Logger
// Reviewer overrides the default HTTP reviewer — tests inject a fake.
Reviewer SubjectAccessReviewer
// contains filtered or unexported fields
}
SARConfig configures NewSubjectAccessReviewAuthorizer. In production the service-shell workstream builds it from the in-cluster rest config (APIURL from KUBERNETES_SERVICE_HOST/PORT, BearerToken and CACert from the mounted service-account); tests inject a Reviewer and leave the rest zero.
type SubjectAccessReview ¶
type SubjectAccessReview struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Spec SubjectAccessReviewSpec `json:"spec"`
Status *SubjectAccessReviewStatus `json:"status,omitempty"`
}
SubjectAccessReview is the minimal authorization.k8s.io/v1 SubjectAccessReview wire shape the assistant POSTs and reads back. Only the fields the assistant sets or inspects are modeled — the apiserver ignores unknown request fields and we ignore unknown response fields — so this stays a small typed contract with no k8s.io/client-go dependency.
type SubjectAccessReviewSpec ¶
type SubjectAccessReviewSpec struct {
ResourceAttributes *ResourceAttributes `json:"resourceAttributes,omitempty"`
// User is the subject under review — the authenticated principal, NOT the
// assistant's own identity (that is the client certificate on the HTTP
// call).
User string `json:"user,omitempty"`
// UID and Groups complete the subject's identity. Milo binds policy to a
// user's ID rather than to the username string, so omitting UID makes an
// otherwise-valid grant evaluate as "not allowed".
UID string `json:"uid,omitempty"`
Groups []string `json:"groups,omitempty"`
// Extra carries the parent-resource context that tells Milo which scope to
// evaluate the review in. See parentExtra.
Extra map[string][]string `json:"extra,omitempty"`
}
SubjectAccessReviewSpec is the review request: who (User) may do what (ResourceAttributes).
type SubjectAccessReviewStatus ¶
type SubjectAccessReviewStatus struct {
Allowed bool `json:"allowed"`
Denied bool `json:"denied,omitempty"`
Reason string `json:"reason,omitempty"`
EvaluationError string `json:"evaluationError,omitempty"`
}
SubjectAccessReviewStatus is the decision the apiserver (via Milo's OpenFGA-backed authorization webhook) returns.
type SubjectAccessReviewer ¶
type SubjectAccessReviewer interface {
Review(ctx context.Context, review *SubjectAccessReview) (*SubjectAccessReviewStatus, error)
}
SubjectAccessReviewer issues one SubjectAccessReview and returns its status. The default implementation POSTs to the control plane; tests inject a fake so no live cluster is needed. A non-nil error means the review could NOT be decided (transport, timeout, non-2xx, decode) — the authorizer treats that as a deny (fail closed).
type TokenReview ¶
type TokenReview struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Spec TokenReviewSpec `json:"spec"`
Status *TokenReviewStatus `json:"status,omitempty"`
}
TokenReview is the minimal authentication.k8s.io/v1 TokenReview wire shape the assistant POSTs and reads back. Only the fields the assistant sets or inspects are modeled — the apiserver ignores unknown request fields and we ignore unknown response fields — so this stays a small typed contract with no k8s.io/client-go dependency (mirroring SubjectAccessReview).
type TokenReviewConfig ¶
type TokenReviewConfig struct {
// APIURL is the control-plane API base URL the TokenReview is POSTed to
// (e.g. https://kubernetes.default.svc). Required unless Reviewer is set.
APIURL string
// BearerToken is the ASSISTANT's own service-account token, sent as the
// Authorization header so the apiserver authenticates the caller. This is
// distinct from the token in the TokenReview body (the token under review).
// Ignored when Reviewer is injected.
BearerToken string
// CACert is the PEM-encoded apiserver CA bundle. Empty falls back to the
// system roots. Ignored when Reviewer is injected.
CACert []byte
// ClientCert/ClientKey are the PEM-encoded client certificate the assistant
// presents to identify ITSELF to the control plane, as an alternative to
// BearerToken. Milo accepts service-account tokens only from its own
// issuer, so a workload-cluster token is rejected and mTLS is the path that
// works; see newControlPlaneTransport. Both or neither. Ignored when
// Reviewer is injected.
ClientCert []byte
ClientKey []byte
// Timeout bounds a single TokenReview round-trip. Zero uses the default.
Timeout time.Duration
// CacheTTL bounds how long a successful resolution is reused. Zero uses the
// default; a negative value disables the cache (every request round-trips).
CacheTTL time.Duration
// Reviewer overrides the default HTTP reviewer — tests inject a fake.
Reviewer TokenReviewer
// contains filtered or unexported fields
}
TokenReviewConfig configures NewTokenReviewAuthenticator. In production the service shell builds it from the in-cluster rest config (APIURL from KUBERNETES_SERVICE_HOST/PORT, BearerToken and CACert from the mounted service-account); tests inject a Reviewer and leave the rest zero.
type TokenReviewSpec ¶
type TokenReviewSpec struct {
Token string `json:"token,omitempty"`
}
TokenReviewSpec is the review request: the opaque bearer token to authenticate.
type TokenReviewStatus ¶
type TokenReviewStatus struct {
Authenticated bool `json:"authenticated"`
User UserInfo `json:"user,omitempty"`
Error string `json:"error,omitempty"`
}
TokenReviewStatus is the identity decision the apiserver returns.
type TokenReviewer ¶
type TokenReviewer interface {
Review(ctx context.Context, review *TokenReview) (*TokenReviewStatus, error)
}
TokenReviewer issues one TokenReview and returns its status. The default implementation POSTs to the control plane; tests inject a fake so no live cluster is needed. A non-nil error means the review could NOT be decided (transport, timeout, non-2xx, decode) — the authenticator treats that as a rejection (fail closed).
type UserInfo ¶
type UserInfo struct {
Username string `json:"username,omitempty"`
UID string `json:"uid,omitempty"`
Groups []string `json:"groups,omitempty"`
Extra map[string][]string `json:"extra,omitempty"`
}
UserInfo is the resolved identity, carried through to authorization (it becomes Principal.Subject); the rest are modeled for completeness/logging.