Add loginPolicy to upstream provider CRD config - #5767
Conversation
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>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5767 +/- ##
==========================================
+ Coverage 70.78% 70.80% +0.01%
==========================================
Files 683 686 +3
Lines 69236 69479 +243
==========================================
+ Hits 49012 49192 +180
- Misses 16624 16686 +62
- Partials 3600 3601 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks for digging into this, @onepabz, and sorry for the slow reply — this sat far longer than it should have. Your diagnosis was accurate: the Closing this. It's now behind The connect flow is the load-bearing half. You scoped it as step 2, but narrowing the login chain is only safe once a user can grant a provider afterward. Without it, a provider marked The resulting failure is silent, and it hits discovery rather than invocation. vMCP queries each backend for its capabilities per-user, per-request using that user's own credentials — there's no shared service identity for discovery. A backend whose upstream token was never acquired fails that query in The connect flow splits by protocol revision, which isn't visible from the auth server packages. On Legacy (2025-11-25) there's a client session and a server-initiated request channel, so a mid-session prompt is buildable. On stateless Modern (2026-07-28) there's no such channel by design, and because authentication is triggered by the MCP request itself, there's no out-of-band surface to send a user to either. So there's currently no mechanism to ask a Modern client's user for anything mid-session, which means any design here would be Legacy-only today. On #5383 — I think your ask is a different one, and that's why it's been hard to get traction. That issue was filed against two gaps: no per-backend incoming auth, and upstream providers behaving as alternates rather than a chain. The second isn't a gap — sequential chaining works, and you're running it. Your ask sits on the other side of that. Chaining works for you; the problem is that it's eager — every user is walked through every upstream's consent screen at every login, including users who never touch that backend. That's acquisition timing, not composition, and it only becomes statable because chaining already works. It's a reasonable thing to want, and it's getting lost appended to a thread about a different problem. Would you open a separate issue for it? Most useful would be your topology, what your users hit at login today, and what you'd want them to hit instead — described as behavior rather than as a config surface. Easier to discuss the shape of a solution once the problem is scoped on its own. |
What
Since #5725/#5733 the embedded auth server honors a non-nil
Config.UpstreamFilter— but nothing feeds it:pkg/authserver/runner/embeddedauthserver.gobuildsresolvedCfgwithout ever setting it, and no CRD/Helm/env surface reaches it. So a declarative operator deployment still chains everyupstreamProvidersentry 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.
UpstreamProviderConfiggains an optionalloginPolicyenum (required|onDemand; empty =required= today's behavior).authserver.UpstreamRunConfigin the sharedBuildAuthServerRunConfig(so MCPServer / MCPRemoteProxy / VirtualMCPServer all inherit it).onDemandentries into aStaticUpstreamFilterand setsConfig.UpstreamFilter, activating the dormant wiring.onDemandon 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.0baseline.Scope (step 1 of 2):
onDemandonly 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 anonDemandprovider after login —SingleLegexists 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 originatesSingleLeg— the linked-account half of #5383) is the natural follow-up and deserves maintainer direction first. Happy to build it.Testing
go build ./...,gofmt, andgolangci-lint runclean on touched packages;pkg/authserver/...,pkg/vmcp/config, andcmd/thv-operator/...unit suites pass (new tests at each layer). Generated artifacts viatask 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