Documentation
¶
Overview ¶
Package appauth is the hub's login-time authorization endpoint for published applications.
The design deliberately keeps the hub OFF the per-request data path:
- Public apps never contact the hub at all.
- Private apps redirect an unauthenticated browser here exactly once. The hub reuses the shared portal browser session (or bounces through the normal /login flow), evaluates ONE SubjectAccessReview against the tenant workspace, and hands the app's access proxy a one-use code. The proxy exchanges the code server-to-server and then maintains its own bounded local session; the hub is not consulted again until that session expires.
- Non-browser clients (CLIs, CI, agents) exchange their hub bearer AT THE HUB for a short-lived app access token bound to one instance (POST /auth/apps/token) and present that to the app. The proxy asks the hub once (POST /auth/apps/verify), then caches the verdict locally for at most the session TTL. A gate never sees a hub bearer (see apptoken.go and verify.go).
Access policy is plain kcp RBAC in the tenant workspace: visiting a private app requires `get` on the instance resource with subresource `access` (e.g. `applications/my-shop` + subresource `access`). Granting a member access is creating a ClusterRole/ClusterRoleBinding pair; revoking is deleting it. The hub carries no knowledge of any provider CRD schema — only resource coordinates supplied by the (controller-authored) proxy config, validated syntactically here.
Index ¶
- Constants
- Variables
- func NewKCPPreviewIdentityResolver(config *rest.Config) func(*http.Request, InstanceRef) (browsersession.Identity, error)
- type CodeRecord
- type CodeStore
- type Config
- type ExchangeResponse
- type Handler
- func (h *Handler) HandleAuthorize(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleExchange(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleMintToken(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandlePreviewHandoff(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleVerify(w http.ResponseWriter, r *http.Request)
- func (h *Handler) RegisterRoutes(router *mux.Router, limit func(http.HandlerFunc) http.HandlerFunc)
- type InstanceHostResolver
- type InstanceRef
- type MintRequest
- type MintResponse
- type SARFactory
- type VerifyResponse
Constants ¶
const ( // AuthorizePath is the browser-facing entry point the access proxy // redirects to when a private app has no session. AuthorizePath = "/auth/apps/authorize" // ExchangePath is the server-to-server endpoint the access proxy calls to // turn a one-use code into a session grant. ExchangePath = "/auth/apps/exchange" // TokenPath is where a caller exchanges their hub bearer for an app // access token bound to one instance (see HandleMintToken). TokenPath = "/auth/apps/token" // VerifyPath is the server-to-server endpoint the access proxy calls to // authorize a request carrying an app access token (see HandleVerify). VerifyPath = "/auth/apps/verify" // CallbackPath is the reserved path on the app host that authorize // redirects back to. It must stay in lockstep with the access proxy's // callback route (providers/infrastructure/accessproxy). CallbackPath = "/__railgrid/auth/callback" // AccessSubresource is the RBAC convention gating private apps: a visitor // needs `get` on `<resource>/<name>` with this subresource in the tenant // workspace. The subresource is never served; it exists only as an // authorization tuple. AccessSubresource = "access" // AccessVerb is the verb checked against the access subresource. AccessVerb = "get" )
const ( // AppTokenPrefix marks an app access token. The access proxy mirrors it // (providers/infrastructure/accessproxy) and refuses to relay anything // else, so a raw hub token presented at a gate never leaves that gate. AppTokenPrefix = "fapp_" )
const PreviewHandoffPath = "/auth/apps/preview-handoff"
PreviewHandoffPath exchanges a scoped workload credential for one-app browser access.
Variables ¶
var ErrInstanceNotPublished = errors.New("instance has no published host")
ErrInstanceNotPublished is returned by an InstanceHostResolver when the referenced instance has no published host to pin the redirect against.
var ErrInvalidPreviewIdentity = errors.New("invalid preview identity")
ErrInvalidPreviewIdentity marks a caller credential that was absent, malformed, unauthenticated, or resolved to an identity outside the requested workspace service-account contract. Other resolver errors represent an identity-verification dependency failure and must not be charged as a bad credential.
Functions ¶
func NewKCPPreviewIdentityResolver ¶ added in v0.2.14
func NewKCPPreviewIdentityResolver(config *rest.Config) func(*http.Request, InstanceRef) (browsersession.Identity, error)
NewKCPPreviewIdentityResolver authenticates in the requested workspace. A provider account from another cluster must never become a tenant-local SA merely because both accounts have the same username.
Types ¶
type CodeRecord ¶
type CodeRecord struct {
Purpose string
Ref InstanceRef
RedirectHost string
Identity browsersession.Identity
ExpiresAt time.Time
}
CodeRecord is what authorize binds a code to and exchange verifies against. It carries identity metadata only — never a credential.
type CodeStore ¶
type CodeStore interface {
Put(ctx context.Context, code string, record CodeRecord) error
Take(ctx context.Context, code string) (CodeRecord, bool)
}
CodeStore persists one-use authorization codes between the browser's authorize hop and the access proxy's server-to-server exchange.
Take MUST be atomic across every hub replica: a code that two callers can redeem is a replayable app sign-in.
type Config ¶
type Config struct {
// PreviewIdentity verifies a workspace service account for a scoped preview.
// It should return ErrInvalidPreviewIdentity only for caller credential or
// identity rejection; other errors are treated as transient dependencies.
PreviewIdentity func(*http.Request, InstanceRef) (browsersession.Identity, error)
// Sessions is the shared hub browser-session store (portal SSO).
Sessions *browsersession.Store
// PreviewSessions stores short-lived one-app browser sessions separately
// from portal sessions. A separate backend is required for mixed-version
// hubs because older replicas ignore unknown scoped identity fields.
PreviewSessions *browsersession.Store
// SARClient resolves per-workspace SubjectAccessReview clients.
SARClient SARFactory
// InstanceHost resolves the published host of the instance a sign-in is
// for. Redirects are only issued to exactly that host.
InstanceHost InstanceHostResolver
// Codes persists the one-use authorization codes. It defaults to a
// bounded process-local map, which is only correct for a single hub
// replica: authorize and exchange are separate requests and a scaled hub
// serves them from different pods. Supply a shared store (see
// pkg/hub/sharedstore) whenever the hub runs more than one replica.
Codes CodeStore
// PreviewCodes stores scoped handoff and final app-authorization codes in
// an isolated collection. Ordinary Codes must never be used as a fallback.
PreviewCodes CodeStore
// LoginPath is the hub-relative path of the interactive login page an
// unauthenticated browser is sent to. Defaults to "/ui/login" (the
// portal SPA is mounted under /ui/).
LoginPath string
// BearerIdentity authenticates the Authorization bearer on a request and
// resolves it to the platform identity. It MUST be the same validator the
// hub uses to mint browser sessions from a bearer (KCPProxy.BrowserIdentity),
// so the token endpoint admits exactly the identities the browser flow
// admits. Nil leaves the token endpoint unregistered.
BearerIdentity func(*http.Request) (browsersession.Identity, error)
// TokenKey returns the hub secret (>= 32 bytes, identical on every
// replica) that app access tokens are HKDF-derived from. Nil leaves both
// the token and verify endpoints unregistered.
TokenKey func(context.Context) ([]byte, error)
// credential was accepted but the identity could not be resolved (a
// transient hub-side failure). Such errors answer 503 and are not counted
// as failed verifications. Nil treats every BearerIdentity error as an
// invalid credential.
IdentityUnavailable error
// ClientIP keys the failed-verification limiter. It should see through
// the hub's trusted reverse proxies (proxy.ClientIP). Nil uses the
// connection peer address.
ClientIP func(*http.Request) string
// Now and Random are test seams.
Now func() time.Time
Random io.Reader
}
Config assembles a Handler.
type ExchangeResponse ¶
type ExchangeResponse struct {
Allowed bool `json:"allowed"`
UserID string `json:"userId"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
SessionTTLSeconds int64 `json:"sessionTtlSeconds"`
}
ExchangeResponse is returned to the proxy on a successful exchange. It contains identity metadata only — never a credential of any kind.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves the authorize, exchange and verify endpoints.
func (*Handler) HandleAuthorize ¶
func (h *Handler) HandleAuthorize(w http.ResponseWriter, r *http.Request)
HandleAuthorize is the browser entry point.
GET /auth/apps/authorize?cluster=&group=&resource=&name=&redirect_uri=&state=
func (*Handler) HandleExchange ¶
func (h *Handler) HandleExchange(w http.ResponseWriter, r *http.Request)
HandleExchange consumes a one-use code.
POST /auth/apps/exchange {"code":..., "host":..., "cluster":..., ...}
func (*Handler) HandleMintToken ¶
func (h *Handler) HandleMintToken(w http.ResponseWriter, r *http.Request)
HandleMintToken exchanges the caller's hub bearer for an app access token.
POST /auth/apps/token
Authorization: Bearer <hub token>
{"cluster":..., "group":..., "resource":..., "name":..., "ttlSeconds":600}
The caller is authenticated with the same validator that mints browser sessions from a bearer (Config.BearerIdentity) and must pass the same SubjectAccessReview as a browser sign-in. Answers 200 MintResponse, 400 malformed, 401 not a hub user credential, 403 no access grant, 404 instance not published, 429 source over its failure budget, 503 dependency outage.
func (*Handler) HandlePreviewHandoff ¶ added in v0.2.14
func (h *Handler) HandlePreviewHandoff(w http.ResponseWriter, r *http.Request)
HandlePreviewHandoff issues a short-lived, one-app browser capability. It never creates a portal session or accepts an asserted end-user identity. Existing name-scoped instances/proxy rights authorize the same app here.
func (*Handler) HandleVerify ¶
func (h *Handler) HandleVerify(w http.ResponseWriter, r *http.Request)
HandleVerify authorizes a non-browser request at a private app's gate.
POST /auth/apps/verify
Authorization: Bearer <app access token>
{"cluster":..., "group":..., "resource":..., "name":...}
Only app access tokens minted by HandleMintToken are accepted — never a hub bearer, which a gate must not be trusted with. The token must be bound to exactly the instance the gate names and unexpired, and the SubjectAccessReview is re-run so a revoked grant stops working within the gate's cache window. Answers 200 VerifyResponse, 400 malformed, 401 not a valid token for this instance, 403 no access grant, 429 source over its failure budget, 503 dependency outage.
Like HandleExchange this endpoint does not authenticate the calling gate — gates hold no credentials by design. It is safe to expose because the request is self-authenticating: nothing is disclosed unless the caller holds a valid app token, and then only the token's own user ID and its yes/no.
func (*Handler) RegisterRoutes ¶
func (h *Handler) RegisterRoutes(router *mux.Router, limit func(http.HandlerFunc) http.HandlerFunc)
RegisterRoutes mounts the endpoints. limit wraps each handler with the hub's auth rate limiter; pass nil to mount unwrapped (tests).
type InstanceHostResolver ¶
type InstanceHostResolver func(ctx context.Context, ref InstanceRef) (string, error)
InstanceHostResolver reports the public host a published instance is served on — the FQDN the provider deployment stamped onto the instance itself. Redirects are pinned to exactly this host, which is what lets published apps live under ANY zone (platform, BYO edge, or a customer-owned domain) without the hub keeping a domain registry: the instance is the single source of truth for where its traffic terminates.
ErrInstanceNotPublished means the instance exists but exposes no host (or does not exist); anything else is an infrastructure failure and fails the authorize closed with a retryable error page.
func NewKCPInstanceHostResolver ¶
func NewKCPInstanceHostResolver(kcpConfig *rest.Config) (InstanceHostResolver, error)
NewKCPInstanceHostResolver returns an InstanceHostResolver that reads the referenced instance in its tenant workspace through the hub's admin kcp config and reports the host the provider stamped onto it.
The host is taken from spec.values.expose.fqdn — the field the instance controller stamps on every exposed instance, i.e. on exactly the set of instances whose access gate can ever start this flow — falling back to status.host for resources that mirror it there instead. A missing object and a missing host are deliberately the same ErrInstanceNotPublished: authorize has already SAR-checked the caller against these coordinates, and collapsing the two keeps the error page from confirming which one it was.
The instance's group version is discovered per call (one GET of /apis/<group> before the instance read). Sign-ins are per-user-per-app-session rare and already cost a SubjectAccessReview, so two extra reads beat carrying a discovery cache that can go stale across provider upgrades.
type InstanceRef ¶
type InstanceRef struct {
Cluster string // logical cluster ID of the tenant workspace
Group string // API group of the instance CRD
Resource string // REST plural of the instance CRD
Name string // instance name
}
InstanceRef are the resource coordinates of a published template instance. They are authorization inputs only; the hub never reads the object.
type MintRequest ¶
type MintRequest struct {
Cluster string `json:"cluster"`
Group string `json:"group"`
Resource string `json:"resource"`
Name string `json:"name"`
TTLSeconds int64 `json:"ttlSeconds,omitempty"`
}
MintRequest is the body of POST /auth/apps/token: the published instance to mint an app access token for, in the same coordinates the access gate uses (they appear in the query of the gate's sign-in redirect and in its 401 body). TTLSeconds is optional: 0 means the default (600), otherwise 60..900.
type MintResponse ¶
type MintResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expiresAt"`
Host string `json:"host"`
}
MintResponse is the body of a successful mint. Token is sent to the app as `Authorization: Bearer <token>`; Host is the instance's published host.
type SARFactory ¶
type SARFactory func(clusterID string) (authorizationv1client.SubjectAccessReviewInterface, error)
SARFactory returns a SubjectAccessReview client scoped to one logical cluster. The hub wires this from its kcp admin config; tests use fakes.
type VerifyResponse ¶
type VerifyResponse struct {
Allowed bool `json:"allowed"`
UserID string `json:"userId"`
SessionTTLSeconds int64 `json:"sessionTtlSeconds"`
}
VerifyResponse is returned to the gate on an allowed verify.