Skip to content

Expose explicit primaryUpstreamProvider for Cedar authz on VirtualMCPServer#5199

Draft
tgrunnagle wants to merge 2 commits intomainfrom
authz-provider_issue_5197
Draft

Expose explicit primaryUpstreamProvider for Cedar authz on VirtualMCPServer#5199
tgrunnagle wants to merge 2 commits intomainfrom
authz-provider_issue_5197

Conversation

@tgrunnagle
Copy link
Copy Markdown
Contributor

@tgrunnagle tgrunnagle commented May 5, 2026

DRAFT - not ready for review

Summary

A VirtualMCPServer with multiple upstream IDPs configured on its embedded auth server has no way to point Cedar at a non-first upstream — the operator unconditionally binds PrimaryUpstreamProvider to whichever upstream happens to be listed first in the spec. This forces users to reorder the list as a side-channel for an authz decision and offers no opt-out beyond editing the upstream order. This PR exposes an explicit primaryUpstreamProvider field on the inline authz config so users can pin Cedar's claim source, while preserving existing first-upstream behavior when the field is empty so existing manifests continue to work unchanged. Behavior brings Cedar/authz to parity with the SubjectProviderName precedent already established on the token-exchange and AWS-STS strategies of MCPExternalAuthConfig.

Closes #5197

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

API Compatibility

  • This PR does not break the v1beta1 API, OR the api-break-allowed label is applied and the migration guidance is described above.

The new primaryUpstreamProvider field is optional with omitempty. Existing manifests without the field continue to use the first-upstream auto-binding fallback verbatim.

Changes

File Change
cmd/thv-operator/api/v1beta1/mcpserver_types.go Add optional PrimaryUpstreamProvider to shared InlineAuthzConfig (vMCP-only in practice, mirrors SubjectProviderName).
cmd/thv-operator/api/v1beta1/virtualmcpserver_types.go Add ConditionReasonAuthzUpstreamUnknown for the new rejection paths.
cmd/thv-operator/pkg/vmcpconfig/converter.go Replace unconditional first-upstream binding with explicit-then-fallback resolution; both branches normalize via authserver.ResolveUpstreamName.
cmd/thv-operator/controllers/virtualmcpserver_controller.go Reject explicit names that don't match any declared upstream and reject explicit names without an embedded AS (AuthServerConfigValidated=False, AuthzUpstreamUnknown); suppress the AuthzUpstreamSelectionWarning advisory when an explicit choice is set; advisory message now points users at the new field.
cmd/thv-operator/controllers/virtualmcpserver_controller_test.go Validator cases for explicit-match, explicit-suppresses-multi-upstream-advisory, explicit-no-match rejection, and explicit-without-AS rejection.
cmd/thv-operator/pkg/vmcpconfig/converter_test.go Converter cases for explicit single-upstream, explicit override of multi-upstream, normalization through ResolveUpstreamName ("" upstream + "default" explicit), and explicit-forwarded-without-AS contract.
deploy/charts/operator-crds/files/crds/*.yaml, templates/*.yaml, docs/operator/crd-api.md Regenerated CRD manifests and API reference for the new field.

Does this introduce a user-facing change?

Yes. Users authoring VirtualMCPServer manifests can now set spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider to explicitly choose which upstream IDP's access token claims Cedar evaluates. When unset, behavior is unchanged: the controller defaults to the first configured upstream and emits the existing AuthzUpstreamSelectionWarning advisory if multiple upstreams are declared. Setting the field to a name that does not match any declared upstream — or setting it without an embedded auth server — now rejects the spec at admission rather than failing silently at request time.

Implementation plan

Approved implementation plan

Sub-issue of #5146, addressing root cause 1: the operator auto-binds PrimaryUpstreamProvider to the first upstream provider's name without exposing a CRD field that lets users override or opt out.

  1. CRD type: Add PrimaryUpstreamProvider string to the shared InlineAuthzConfig (matches the SubjectProviderName precedent on token-exchange/AWS-STS strategies — also defined on shared types but documented as vMCP-only).
  2. Converter wiring: Replace the unconditional first-upstream assignment with explicit-then-fallback. Run ResolveUpstreamName on the user-supplied value too, so the field is normalized consistently with the auto-selected path.
  3. Validation (validateAuthzUpstreamAvailable): Reject (SpecValidationError, AuthServerConfigValidated=False) when an explicit PrimaryUpstreamProvider is set but does not match any entry in Spec.AuthServerConfig.UpstreamProviders after ResolveUpstreamName normalization on both sides. Suppress the AuthzUpstreamSelectionWarning advisory when the user has set the field explicitly. Leave the existing "embedded AS but zero upstreams" reject path unchanged. (Code review added: also reject when an explicit name is set with no embedded auth server at all.)
  4. Tests: Converter and validator cases for the new branches.
  5. Generated artifacts: task operator-generate, task operator-manifests, task -d cmd/thv-operator crdref-gen.
  6. Backward compatibility: Existing manifests without the new field keep current behavior — the converter's fallback branch is unchanged for that path.

Special notes for reviewers

  • The new field lives on the shared InlineAuthzConfig type (also reachable from MCPServer and MCPRemoteProxy) but is documented as vMCP-only and has no effect elsewhere. This matches the SubjectProviderName precedent. The alternative — introducing a vMCP-specific VirtualMCPInlineAuthzConfig and AuthzConfigRef — was rejected as a larger refactor with knock-on changes in the converter, controllerutil.GenerateOrUpdateInlineAuthzConfigMap, and tests. Happy to revisit if reviewers prefer the stricter typing.
  • Out of scope for this PR: the Cedar opaque-token fallback fix (root cause 3 in VirtualMCPServer Cedar authorization rejects all requests when upstream OIDC provider issues opaque access tokens #5146) lives in pkg/authz/authorizers/cedar/core.go and will be a separate change. This PR only makes the auto-binding overridable.
  • The second commit (Address code review feedback) reflects feedback from /review-code-for-issue and adds the "explicit name without embedded AS" rejection plus a converter test that actually exercises ResolveUpstreamName normalization (the previous test was misleading because ResolveUpstreamName is the identity function for non-empty input).

Adds an optional primaryUpstreamProvider field to the inline authz config
on VirtualMCPServer so users with multiple upstream IDPs can pin Cedar to
a non-first provider, instead of being silently bound to whichever
upstream happens to be listed first.

Changes for issue #5197:
- Add PrimaryUpstreamProvider to InlineAuthzConfig (shared type, vMCP-only
  in practice, mirroring the SubjectProviderName precedent on the token-
  exchange and AWS-STS strategies).
- Switch the converter from unconditional first-upstream binding to an
  explicit-then-fallback resolution; both branches normalize through
  authserver.ResolveUpstreamName.
- Reject the spec with AuthServerConfigValidated=False
  (AuthzUpstreamUnknown) when the explicit name does not match any
  declared upstream — Cedar would otherwise deny every request at runtime.
- Suppress the AuthzUpstreamSelectionWarning advisory when the user has
  set the field explicitly; the auto-selection it warns about is no
  longer happening.
- Extend converter and validator tests; regenerate CRD YAMLs and API
  docs.

Existing manifests without the new field keep current behavior — the
fallback branch is unchanged for that path.
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels May 5, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.71%. Comparing base (9572f6a) to head (b5d2821).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5199      +/-   ##
==========================================
+ Coverage   67.65%   67.71%   +0.05%     
==========================================
  Files         607      607              
  Lines       61982    62060      +78     
==========================================
+ Hits        41937    42023      +86     
+ Misses      16883    16879       -4     
+ Partials     3162     3158       -4     

☔ View full report in Codecov by Sentry.
📢 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.

@tgrunnagle tgrunnagle force-pushed the authz-provider_issue_5197 branch from f24fcbe to b5d2821 Compare May 5, 2026 19:55
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels May 5, 2026
Fixed issues from code review:
- MEDIUM: Reject explicit primaryUpstreamProvider when no embedded auth
  server is configured. The early-return direct-IdP branch in
  validateAuthzUpstreamAvailable now checks for a non-empty explicit
  name first and returns SpecValidationError with
  ConditionReasonAuthzUpstreamUnknown when set — closing the silent
  misconfiguration where the converter would forward an unresolvable
  name into Cedar config at runtime.
- MEDIUM: Update the converter block comment so it accurately describes
  both rejection paths (mismatch with declared upstreams AND explicit
  name without an embedded AS), keeping the comment synchronized with
  the validator's behavior per the go-style.md rule.
- MEDIUM: Replace the misleading "is normalized via ResolveUpstreamName"
  converter test with a case that actually exercises normalization
  (upstream Name:"" resolving to "default", user pinning to "default").
  Removes redundancy with the single-upstream-honored case and matches
  the test's claimed assertion.
- MEDIUM: Add validator test case for explicit primaryUpstreamProvider
  with no embedded auth server, locking the new rejection in.
@tgrunnagle tgrunnagle force-pushed the authz-provider_issue_5197 branch from b5d2821 to b1499ed Compare May 5, 2026 20:04
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose explicit primaryUpstreamProvider for Cedar authz on VirtualMCPServer

1 participant