auth

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

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

View Source
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).

View Source
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

func BearerTokenFromContext(ctx context.Context) string

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

func ContextWithBearerToken(ctx context.Context, token string) context.Context

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

func ExtractBearerToken(authorization string) string

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

func NewAuthorizer(cfg *config.Config, logger *slog.Logger) (Authorizer, error)

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

func Unauthenticated(message string) *Error

Unauthenticated returns a 401 *Error.

func Unauthorized

func Unauthorized(message string) *Error

Unauthorized returns a 403 *Error.

func (*Error) Error

func (e *Error) Error() string

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.

Jump to

Keyboard shortcuts

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