Skip to content

Add loginPolicy to upstream provider CRD config#5767

Open
onepabz wants to merge 1 commit into
stacklok:mainfrom
onepabz:feat/vmcp-declarative-upstream-filter
Open

Add loginPolicy to upstream provider CRD config#5767
onepabz wants to merge 1 commit into
stacklok:mainfrom
onepabz:feat/vmcp-declarative-upstream-filter

Conversation

@onepabz

@onepabz onepabz commented Jul 9, 2026

Copy link
Copy Markdown

What

Since #5725/#5733 the embedded auth server honors a non-nil Config.UpstreamFilter — but nothing feeds it: pkg/authserver/runner/embeddedauthserver.go builds resolvedCfg without ever setting it, and no CRD/Helm/env surface reaches it. So a declarative operator deployment still chains every upstreamProviders entry at every login — adding GitHub next to the identity IdP forces GitHub consent on every user at every login, even for users who never call a GitHub-backed tool.

This is a minimal strawman toward #5383: it exposes the already-merged chain narrowing through the CRD.

  • UpstreamProviderConfig gains an optional loginPolicy enum (required | onDemand; empty = required = today's behavior).
  • The operator copies it into authserver.UpstreamRunConfig in the shared BuildAuthServerRunConfig (so MCPServer / MCPRemoteProxy / VirtualMCPServer all inherit it).
  • The runner turns onDemand entries into a StaticUpstreamFilter and sets Config.UpstreamFilter, activating the dormant wiring.
  • onDemand on the first upstream is rejected at three layers (CRD validation, vMCP reconcile, RunConfig.Validate) — the first upstream anchors identity and the filter contract never removes it.

Additive and opt-in — filter nil / field empty is byte-identical to today; the CRD schema-compat check passes against the v0.34.0 baseline.

Scope (step 1 of 2): onDemand only skips login-time acquisition; already-stored tokens still inject/refresh at request time. There's no in-band flow yet for a user to grant an onDemand provider after login — SingleLeg exists in storage/callback but nothing originates it. So this removes the forced-consent blocker to aggregating backends under one endpoint; the on-demand connect flow (a browser-facing surface that originates SingleLeg — the linked-account half of #5383) is the natural follow-up and deserves maintainer direction first. Happy to build it.

Testing

go build ./..., gofmt, and golangci-lint run clean on touched packages; pkg/authserver/..., pkg/vmcp/config, and cmd/thv-operator/... unit suites pass (new tests at each layer). Generated artifacts via task operator-generate / operator-manifests / crdref-gen.

Notes

Marked draft — a strawman to help unstick #5383; happy to rename the field/values or reshape per maintainer direction. Context: we run ToolHive in production on Kubernetes (operator + vMCP + MCPRemoteProxy, an OIDC IdP as primary upstream, per-user SaaS tokens via upstream_inject), today working around the forced chain with one single-backend vMCP per SaaS provider.

Refs #5383

The embedded auth server walks every configured upstream provider at
every login: the first authorize leg always targets upstreams[0] and
callback.go's continueChainOrComplete then walks all remaining
upstreams. Since stacklok#5725/stacklok#5733 the server honors Config.UpstreamFilter to
narrow that chain, but nothing feeds it: the runner never sets the
field and no CRD, Helm, or file surface reaches it. A declarative
operator deployment therefore cannot stop a secondary SaaS provider
(e.g. GitHub next to the identity IdP) from forcing its consent screen
on every user at every login.

Expose the existing narrowing through the CRD surface:

- UpstreamProviderConfig gains an optional loginPolicy enum field
  (required|onDemand; empty means required, today's behavior).
- The operator copies it into authserver.UpstreamRunConfig via the
  shared BuildAuthServerRunConfig, covering MCPServer, MCPRemoteProxy,
  and VirtualMCPServer alike.
- The runner translates onDemand entries into a StaticUpstreamFilter
  (new, implements handlers.UpstreamFilter) and sets
  Config.UpstreamFilter, activating the previously dormant wiring.
- Validation rejects onDemand on the first upstream (it anchors the
  chain and resolves the user's identity) at CRD validation time, at
  VirtualMCPServer reconcile time, and at RunConfig load.

Tokens previously stored for an onDemand provider are still injected
and refreshed at MCP-request time; only login-time acquisition is
skipped. There is intentionally no in-band flow yet for a user to
authorize an onDemand provider after login - that is the linked-account
half of stacklok#5383 and is left for maintainer direction.

Toward stacklok#5383.

Signed-off-by: Pablo Sanchez <494757+onepabz@users.noreply.github.com>
@onepabz onepabz marked this pull request as ready for review July 9, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant