Skip to content

Expose group-based upstream filter via vMCP CRD - #6740

Draft
hellouz818 wants to merge 3 commits into
stacklok:mainfrom
hellouz818:feature/6675-pr3-crd
Draft

hellouz818 wants to merge 3 commits into
stacklok:mainfrom
hellouz818:feature/6675-pr3-crd

Conversation

@hellouz818

@hellouz818 hellouz818 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR 3/3 of #6675. Surfaces the auth-server GroupBasedFilter (added in PR 2/3) through VirtualMCPServerSpec so operators can configure per-user upstream chain narrowing with YAML.

The new AuthServerUpstreamFilter field lives beside AuthServerConfig (rather than nested inside the shared EmbeddedAuthServerConfig, which is also used by MCPExternalAuthConfig where multi-upstream filtering has no meaning). Admission-time CEL enforces two cheap cross-field invariants: the field requires authServerConfig, and at least two upstream providers must be configured. Reconciliation re-runs the full Go filter constructor against the configured upstream list so a resource that bypassed admission (fake client, pre-CEL stored object) is rejected with a terminal AuthServerConfigValidated=False condition before its ConfigMap is written.

The vMCP converter extends the shared BuildAuthServerRunConfig call with the vMCP-only field translation, keeping the shared builder free of vMCP-specific concerns.

  • CRD type additions on VirtualMCPServerSpec + CEL + OpenAPI markers
  • vMCP converter extension (ConvertUpstreamFilter) layered on BuildAuthServerRunConfig
  • Reconciler re-validation (validateAuthServerUpstreamFilter)
  • Architecture doc section with YAML example
  • Regenerated deepcopy, CRD manifests, CRD reference

Stacked on #6739. Refs #6675

Type of change

  • New feature

Test plan

  • Unit tests (task test) — 6 cases for validateAuthServerUpstreamFilter, 4 cases for ConvertUpstreamFilter
  • Integration tests (envtest) — 9 Ginkgo cases in test-integration/virtualmcp/virtualmcpserver_upstreamfilter_cel_test.go exercising the CEL rules through the real apiserver
  • Linting (task lint-fix) — 0 issues
  • Manual testing — deployed to an internal mcphub deployment with the field set; viewer-only user walks only the slack upstream, successfully invokes slack tools via claude-code, with atlassian / cobi / salesforce upstream tokens correctly absent (confirmed via operator audit logs and absent-token warnings)

API Compatibility

  • This PR does not break the v1beta1 API.

New optional field on VirtualMCPServerSpec. Omitting it preserves pre-filter behavior (full chain). No existing CRs change meaning.

Does this introduce a user-facing change?

Yes — new optional field VirtualMCPServer.spec.authServerUpstreamFilter lets operators narrow the OAuth chain per user based on an identity claim. See the "Per-User Upstream Filtering" section in docs/arch/10-virtual-mcp-architecture.md for the configuration shape and semantics.

Special notes for reviewers

CEL cost budget

The two deeper reference-integrity CEL rules the issue plan called for (every referenced provider must name a configured non-first upstream; mandatory first provider must not be referenced) were originally implemented but produced a per-schema x-kubernetes-validations cost estimate over 100× the per-cluster budget in realistic deployments where upstreamProviders is unbounded.

They are enforced at reconciliation time instead, by validateAuthServerUpstreamFilter, which surfaces a terminal AuthServerConfigValidated=False condition. The reconciler calls the same Go constructor used in PR 2/3, so the admission vs. reconciliation path contains no logic drift.

A proper restoration path would add MaxItems bounds on the shared upstreamProviders type to tame the cost estimator; keeping that out of scope here keeps the shared EmbeddedAuthServerConfig type unchanged for MCPExternalAuthConfig consumers. Happy to open a follow-up PR for the bounded-list restoration if preferred.

Note on OAuth-flow E2E

The issue plan includes a functional E2E deliverable. This PR does not add a full in-repo OAuth-flow E2E; admission behavior is covered through envtest and the end-to-end flow has been validated manually against a real mcphub deployment. If maintainers prefer the automated OAuth E2E to land as part of #6675, I can add it as a follow-up in this series.

This follows the test layering used for the SPIFFE CRD exposure in #6500: envtest coverage for admission behavior, without a full OAuth-flow E2E.

Follow-up work tracked

  • Cedar / xaa migration onto the shared pkg/auth/claims helper from PR 1/3.
  • Optional CEL rule restoration after MaxItems bounds on the shared upstream type.
  • Full OAuth-flow E2E (if maintainers want this in-repo).

JWT/OIDC claim values that are string arrays commonly arrive at Go as
either []string (from typed JWT libraries) or []any whose elements are
all strings (from standard encoding/json). Cedar's extractGroups and
addMultiValuedClaimSets, and the vmcp xaa strategy, each handle this
split independently today.

Extract that normalization into pkg/auth/claims so the upcoming
group-based upstream filter can consume it without introducing another
duplicate, and so a follow-up refactor can collapse the existing
Cedar/xaa variants onto the same helper without changing their
observable behavior.

The helper is strict by design: nil values, non-array types, and
non-string array elements are treated as errors so a security-relevant
caller can rely on it to fail closed on malformed claims. Returned
slices never alias the caller's input.

Refs: stacklok#6675
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.92265% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.35%. Comparing base (61aaf42) to head (e599d67).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
pkg/authserver/runner/embeddedauthserver.go 38.46% 8 Missing ⚠️
...perator/controllers/virtualmcpserver_controller.go 89.28% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6740      +/-   ##
==========================================
+ Coverage   79.34%   79.35%   +0.01%     
==========================================
  Files         802      804       +2     
  Lines       81211    81610     +399     
==========================================
+ Hits        64436    64761     +325     
- Misses      16770    16844      +74     
  Partials        5        5              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hellouz818
hellouz818 force-pushed the feature/6675-pr3-crd branch from b6542fd to 40e2300 Compare October 2, 2026 08:41
VirtualMCPServer currently walks every configured upstream in order
for every user. When a user is missing an account on one of the
upstreams (e.g. Salesforce), the login they are forced through there
fails and the whole authorization fails — even though the
otherwise-valid upstream chain is identity-complete.

Introduce a built-in GroupBasedFilter implementing the existing
handlers.UpstreamFilter hook. The filter reads a configurable claim
(default "groups") from the first upstream's resolved principal and
returns the union of UpstreamProviders from every matching rule, with
a fallback to defaultUpstreams for valid-but-empty or unmatched
groups. Missing or malformed claims fail the authorization per the
filter's fail-closed contract; principal.Claims is treated as
read-only.

Wire the new filter through authserver.RunConfig so operator-supplied
configuration can turn it on, with the same static invariants
(unknown references, mandatory-first references, two-upstream
minimum, duplicates, empty values) rejected both at RunConfig.Validate
startup and at filter construction.

This is step two of three; the matching VirtualMCPServer CRD surface
and conversion follow in a separate commit so each layer is reviewable
independently.

Refs: stacklok#6675
Surface the auth-server GroupBasedFilter through VirtualMCPServerSpec so
operators can configure per-user upstream chain narrowing with YAML,
without reaching into the embedded auth-server RunConfig directly.

The new AuthServerUpstreamFilter field lives beside AuthServerConfig
(rather than nested inside EmbeddedAuthServerConfig, which is shared
with MCPExternalAuthConfig and has no meaning for multi-upstream
filtering). CEL at admission enforces the two cheap cross-field
invariants Kubernetes can check cheaply: the field requires
authServerConfig, and at least two upstream providers must be
configured.

The two deeper reference-integrity checks the plan called for —
every referenced provider must name a configured non-first upstream,
and the mandatory first provider must not be referenced — were
originally expressed as CEL but produced a per-schema
x-kubernetes-validations cost estimate 100x over the per-cluster
budget in realistic deployments where upstreamProviders is unbounded.
They are enforced at reconciliation time instead, by
validateAuthServerUpstreamFilter in the VirtualMCPServer controller,
which surfaces a terminal AuthServerConfigValidated=False condition.
A proper restoration would require adding MaxItems bounds on the
shared upstreamProviders type to tame the cost estimator; keeping
that out of scope here keeps the shared EmbeddedAuthServerConfig
type unchanged for MCPExternalAuthConfig consumers.

Reconciliation also protects against admission bypass (fake client,
pre-CEL stored object): the full Go filter constructor runs again
against the configured upstream list before the ConfigMap is written,
rejecting malformed configuration before it reaches a crash-looping
pod. The vMCP converter extends the shared BuildAuthServerRunConfig
call with the vMCP-only field translation, keeping the shared builder
free of vMCP-specific concerns.

Adding the new filter construction to newEmbeddedAuthServerWithStorage
pushes its cyclomatic complexity one step over the package ceiling.
The function is a sequential auth-server bootstrap — each step is a
required stage with its own error-return — so the complexity is a
property of the ordered boot flow rather than avoidable branching.
A gocyclo suppression with an explanatory docstring matches the shape
other sequential bootstraps elsewhere in the repository already use
(rmCmdFunc, runCmdFunc, MCPServerReconciler.Reconcile), and keeps the
error-cleanup defer block in the same form the original upstream code
uses, rather than extracting a helper whose only purpose would be to
reshape the complexity count.

Generated artifacts (deepcopy, CRD manifests, CRD reference) are
regenerated via task operator-generate, task operator-manifests, and
task crdref-gen.

Refs: stacklok#6675

This branch has not been deployed

No deployments
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