Documentation
¶
Overview ¶
Package upstreamfilter provides UpstreamFilter implementations for the embedded authorization server. Filters narrow the multi-upstream authorization chain based on the first upstream's resolved identity, so users only walk the upstream providers they are entitled to.
This package owns the UpstreamFilter contract and its built-in implementation. See that interface's documentation for the runtime lifecycle: single invocation per authorization on the first leg's callback, principal is read-only, fail-closed on nil/absent claims.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type FilterPrincipal ¶ added in v0.63.0
type FilterPrincipal interface {
GetSubject() string
GetCanonicalUserID() string
GetName() string
GetEmail() string
GetClaims() map[string]any
}
FilterPrincipal is a getter contract for identity and claims used in upstream authorization decisions; it does not constrain an implementation to contain only these values. The authserver callback supplies a private projection containing credential-free values: Subject, CanonicalUserID, Name, Email, and Claims. Subject is the first upstream's provider-specific mapped subject: OIDC uses the configured SubjectClaim (or "sub" by default), OAuth2 may extract it from configured userinfo fields or the token response, and a synthetic fallback may apply. It is distinct from CanonicalUserID, the canonical storage-user ID (normally the resolved internal user ID, but a synthetic identity may use the mapped subject itself, which need not be stable). Name and Email come from the first upstream's resolved identity or profile and may originate from OIDC claims, configured OAuth2 userinfo fields, or token-response extraction. Claims contains the upstream-provided ID-token/userinfo claims, or is nil when unavailable; the authorization server does not manufacture claims. Callers must treat the returned claims map as read-only.
type GroupBasedFilter ¶
type GroupBasedFilter struct {
// contains filtered or unexported fields
}
GroupBasedFilter implements UpstreamFilter by matching the principal's group claim against configured rules and returning the union of the matching rules' upstream provider sets. See package documentation and GroupBasedFilterConfig for semantics.
func NewGroupBasedFilter ¶
func NewGroupBasedFilter(cfg GroupBasedFilterConfig, configuredUpstreams []string) (*GroupBasedFilter, error)
NewGroupBasedFilter constructs and validates a GroupBasedFilter.
configuredUpstreams is the full ordered list of upstream provider names declared on the auth server, including the mandatory first upstream at index 0. It is required at construction time to validate that every referenced provider exists and that no rule or default references the mandatory first upstream (which is always included and cannot be filtered).
It returns an error for any of: fewer than two configured upstreams; no rules; empty or duplicate group values within a rule; empty or duplicate upstream references within a rule; unknown upstream references; references to the mandatory first upstream; unknown or first-upstream references in DefaultUpstreams.
func (*GroupBasedFilter) FilterUpstreams ¶
func (f *GroupBasedFilter) FilterUpstreams( _ context.Context, principal FilterPrincipal, _ []string, ) ([]string, error)
FilterUpstreams implements UpstreamFilter.
It reads principal.GetClaims()[filter.claim], decodes it as a []string via the strict OAuth claims decoder, and returns the union of every matching rule's UpstreamProviders. The claim MUST be present and MUST be a JSON string array — a missing, nil, non-array, or mixed-type claim value fails the authorization per the filter contract's fail-closed guarantee for security-relevant filters.
A valid empty array or a valid array with no matching rule falls back to the configured DefaultUpstreams. Duplicate names across matching rules and defaults are deduplicated in the returned slice; the handler layer preserves configured order and drops any name that is not a non-first configured upstream.
principal (including the map returned by GetClaims()) is treated as read-only. Claim values are not logged.
The configured argument is unused: the constructor validated every rule and default reference against the full configured upstream list, so runtime reconciliation of the returned set against the non-first upstreams is redundant with the handler layer's own drop-unknown behavior.
type GroupBasedFilterConfig ¶
type GroupBasedFilterConfig struct {
// Claim is the exact, top-level JWT/OIDC claim name to read group
// membership from. Defaults to "groups" when empty. The claim value MUST be
// a JSON array of strings; other shapes (missing, non-array, mixed types)
// fail the authorization. Nested claims (e.g. Keycloak's realm_access.roles)
// must be flattened by the identity provider — nested paths are not
// supported in this implementation.
//
// The value is read from upstream.Identity.Claims as the first upstream
// resolves it: an OIDC upstream fills that map from the validated ID
// token, a pure OAuth2 upstream from the userinfo response. The
// identityFromToken and synthesized-identity paths leave it empty, and
// groups carried only in an access token never reach it. A missing claim
// fails the authorization — DefaultUpstreams covers a valid-but-empty
// claim and unmatched groups, not an absent one.
Claim string `json:"claim,omitempty" yaml:"claim,omitempty"`
// Rules assigns groups to upstream provider subsets. A principal matches a
// rule if any of the principal's group values appears in the rule's Groups
// list (any-of / OR semantics). Multiple matching rules are combined by
// union: the effective upstream chain contains every UpstreamProviders
// value from every matching rule, deduplicated.
Rules []GroupRule `json:"rules" yaml:"rules"`
// DefaultUpstreams is used when the claim is a valid empty array or when no
// rule matches. When empty or omitted, no optional upstreams are added —
// the mandatory first upstream remains but the authorization walks no
// further.
DefaultUpstreams []string `json:"defaultUpstreams,omitempty" yaml:"defaultUpstreams,omitempty"`
}
GroupBasedFilterConfig is the serializable configuration for the group-based upstream filter. It is stored in RunConfig and delivered to the vMCP pod via the auth-server config ConfigMap.
The nested keys are camelCase where RunConfig's own fields are snake_case. They match the v1beta1 CRD spelling (spec.authServerConfig.upstreamProviders), which keeps a rule spelled the same in the CR and in the generated YAML. Renaming them would also change the published OpenAPI model and Go SDK.
type GroupRule ¶
type GroupRule struct {
// Groups is the set of principal group names this rule matches. Matching
// is case-sensitive and exact. Empty and duplicate values are rejected at
// configuration validation time.
Groups []string `json:"groups" yaml:"groups"`
// UpstreamProviders lists the non-first upstream provider names to include
// in the chain when this rule matches. All names must reference a
// configured non-first upstream. Empty and duplicate values, references to
// the mandatory first upstream, and references to undeclared upstreams are
// rejected at configuration validation time.
UpstreamProviders []string `json:"upstreamProviders" yaml:"upstreamProviders"`
}
GroupRule pairs a set of principal group names with the set of upstream provider names matching principals are entitled to walk.
type UpstreamFilter ¶ added in v0.63.0
type UpstreamFilter interface {
FilterUpstreams(
ctx context.Context,
principal FilterPrincipal,
configured []string,
) ([]string, error)
}
UpstreamFilter narrows the authorization chain to a subset of the configured upstreams, keyed on the identity established by the first upstream. It is consulted exactly once per authorization, in the callback handler, after the first upstream (upstreams[0]) resolves. The first upstream is always required: it is never passed to the filter and cannot be removed by it.
FilterUpstreams receives, in order:
- ctx: the request context of the first leg's callback.
- principal: the first upstream's credential-free identity and claims, used for authorization decisions. See FilterPrincipal for where each value comes from. The filter MUST treat principal (including its claims map) as read-only.
- configured: the names of the non-first configured upstreams, in configured order.
Claims contract: values are untyped and come straight from the upstream IdP, so implementations MUST assert types defensively (e.g. comma-ok on map/slice access) — a malformed IdP response must not be able to panic the filter. GetClaims() is nil not only for OAuth2 identityFromToken / synthetic upstreams (which resolve no structured claim set) but also on a transient OIDC claims-extraction failure, so a security-relevant filter MUST treat nil/absent claims as fail-closed. Note that the `aud` claim, when present, is the upstream IdP's client_id — not this authorization server.
It returns the subset to keep. The handler preserves configured order and ignores any returned name that is not one of the non-first configured upstreams, so the filter cannot reorder the chain or introduce unknown providers. A returned error fails the authorization with a server error — the handler never falls back to walking every upstream on error.
The "first-leg identity" guarantee holds even across a rolling upgrade of a persistent backend: a subsequent-leg pending that lacks a computed chain (e.g. one written before this feature existed) is rejected and forces a fresh authorization, rather than re-running the filter against a later leg.