Skip to content

Expose canonical inbound grants in CRDs - #6499

Open
jhrozek wants to merge 3 commits into
spiffe-integration-split3-3from
spiffe-integration-split3-5
Open

Expose canonical inbound grants in CRDs#6499
jhrozek wants to merge 3 commits into
spiffe-integration-split3-3from
spiffe-integration-split3-5

Conversation

@jhrozek

@jhrozek jhrozek commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

The operator's CRDs (VirtualMCPServer, MCPExternalAuthConfig) only expose the legacy, per-field inbound-grant configuration (delegateClients, trustedIssuers[*] policy fields). The canonical inboundGrants grant-family model already exists at the runtime RunConfig layer (this stack's earlier PRs), but the operator has no way to declare it — a prerequisite for adding SPIFFE associations without inventing a second, parallel Kubernetes API surface for the same underlying grants.

  • Add inboundGrants CRD types (TokenExchange/JWTBearer grant families, delegate clients, issuer policies) to the shared EmbeddedAuthServerConfig, with CEL admission validation for legacy/canonical mutual exclusion.
  • Wire the new fields through to authserver.RunConfig via the same converter pattern the legacy fields already use.
  • Report deprecated legacy field usage as a status condition (ConditionTypeVirtualMCPServerDeprecatedInboundGrantConfiguration) during normalization, without changing the effective authorization policy — this gives operators a visible migration signal before legacy fields are removed.
  • Regenerate CRD YAML/deepcopy/docs.

Refs #6200

Type of change

  • New feature

Test plan

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

New table-driven unit tests for the converter and CEL admission rules (authserver_inbound_grants_test.go, inbound_grants_cel_test.go), plus a reconcile-level test asserting the deprecation condition transitions correctly and emits a one-shot warning event.

API Compatibility

  • This PR does not break the v1beta1 API — inboundGrants is a new optional field; existing legacy fields are unchanged and continue to work standalone.

Changes

Large diff (~3,470 lines), dominated by generated CRD YAML (deploy/charts/operator-crds/**, regenerated via task operator-manifests/task operator-generate) and docs/operator/crd-api.md (regenerated via task crdref-gen) — roughly 2,600 of the changed lines are generated, not hand-written. Hand-written surface: mcpexternalauthconfig_types.go (new CRD types + CEL), controllerutil/authserver.go (converter wiring), virtualmcpserverstatus/collector.go (deprecation condition), and their tests.

Does this introduce a user-facing change?

Yes — operators can now declare inboundGrants on VirtualMCPServer/MCPExternalAuthConfig directly, and get a status condition warning if they're still using the legacy per-field configuration it's meant to replace.

Special notes for reviewers

This PR is a cherry-picked/rebuilt version of work originally done on an earlier, abandoned branch before this stack's review cycle reworked the underlying runtime model — content and tests were re-verified against the current RunConfig/InboundGrantsRunConfig shape rather than merged as-is. Stacked on #6474.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.37864% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.34%. Comparing base (7ff760b) to head (291104f).

Files with missing lines Patch % Lines
...or/controllers/mcpexternalauthconfig_controller.go 73.80% 22 Missing ⚠️
...perator/controllers/virtualmcpserver_controller.go 94.59% 2 Missing ⚠️
cmd/thv-operator/pkg/controllerutil/authserver.go 96.87% 2 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##           spiffe-integration-split3-3    #6499      +/-   ##
===============================================================
+ Coverage                        78.33%   78.34%   +0.01%     
===============================================================
  Files                              776      776              
  Lines                            75975    76121     +146     
===============================================================
+ Hits                             59517    59640     +123     
- Misses                           16453    16476      +23     
  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.

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The canonical model is wired through the CRDs, conversion, and generated artifacts, but two correctness gaps block approval:

  1. VirtualMCPServer inline auth validation still recognizes only legacy delegateClients / trustedIssuers[*].jwtBearerGrant as satisfying token-only operation (cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685). A valid inline configuration using only spec.authServerConfig.inboundGrants is admitted but marked AuthServerConfigValidated=False and never deploys. Please make this path recognize configured canonical grant families, consistent with the external-auth path.

  2. MCPExternalAuthConfig validation's confidential-client transport check only considers legacy DelegateClients (cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:961-967, called at 2129-2133). A canonical delegate client for an invalid non-loopback HTTP issuer can therefore report Valid=True, while the normalized runtime configuration later rejects it. Please apply the same normalized/canonical-aware transport validation at the owning configuration resource and add coverage for the canonical path.

The remaining red Go Vulnerability Check is also present on the stacked bases and appears unrelated to this diff.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-4 branch 2 times, most recently from fbe4ff6 to 67f5da4 Compare September 3, 2026 10:02
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from 5834d9c to 6dbb23e Compare September 3, 2026 10:04
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 3, 2026
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-4 branch from 67f5da4 to b4c8fed Compare September 3, 2026 13:00
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from 6dbb23e to e038ceb Compare September 3, 2026 13:03
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 3, 2026

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at e038ceb5d38e0961cb3b332c58359863bbf392f6: the two prior blockers are still unresolved.

  • VirtualMCPServer accepts canonical grants in CRD admission but its inline runtime guard still ignores cfg.InboundGrants, rejecting valid token-only configurations (cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685).
  • ValidateConfidentialClientTransport still ignores canonical inboundGrants.tokenExchange.delegateClients, allowing cleartext non-loopback HTTP issuers with a confidential client (cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:961-967).

Please address both with regression tests. CI is green; no local tests were run.

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested:

  • cmd/thv-operator/controllers/virtualmcpserver_controller.go:670: inline validation ignores canonical InboundGrants, so valid token-only canonical configurations are rejected and never reconcile. Include canonical grant families.
  • cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:961: confidential-client transport validation considers only legacy delegate clients; include canonical token-exchange delegate clients.
  • cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:2151: canonical issuer policies bypass equivalent trusted-issuer validation and can be published Valid=True despite invalid policy values. Validate normalized canonical policies at this boundary.

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One precision addendum to the latest review: canonical inboundGrants also skips MCPExternalAuthConfig's early trusted-issuer validation. At cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:2151-2155, tokenexchange.ValidateTrustedIssuers(...) only runs when cfg.InboundGrants == nil. Consequently an invalid issuer policy (for example, self-issuer collision or invalid issuer/JWKS URL) can leave the owning config reported Valid=True.

This is not a complete runtime bypass: BuildAuthServerRunConfig later calls RunConfig.Validate() and catches the invalid configuration before a consuming workload runs (cmd/thv-operator/pkg/controllerutil/authserver.go:952-954,1034-1052). Please apply the equivalent normalized trusted-issuer validation on the owning configuration path and test the Valid=False result for a canonical invalid issuer policy.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from e038ceb to ae26bc4 Compare September 3, 2026 14:14
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 3, 2026

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested:

  • cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685: inline validation still ignores canonical InboundGrants, so valid token-only canonical configurations are rejected and never deployed.
  • cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:961-967: confidential-client transport validation examines only legacy delegate clients. Apply it to normalized canonical token-exchange delegate clients too.
  • cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:2151-2155: canonical issuer policies bypass the owning MCPExternalAuthConfig trusted-issuer validation and can be reported Valid=True despite invalid issuer/JWKS URLs or self-issuer collisions. Normalize and validate them at this boundary.

All current CI checks are green; no local tests were run.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from ae26bc4 to ce2447f Compare September 3, 2026 16:16
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 3, 2026
@jhrozek

jhrozek commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Pushed fixes for both findings.

  1. `VirtualMCPServer`'s inline zero-alternatives check was missing the `cfg.InboundGrants == nil` condition that `MCPExternalAuthConfig`'s equivalent check already has -- added it, plus updated the rejection message to name `inboundGrants` as an accepted alternative.
  2. `ValidateConfidentialClientTransport` now also counts canonical delegate clients (`InboundGrants.TokenExchange.DelegateClients`), not just the legacy field. Since this method is shared by both CRDs, one fix covers both. Confirmed via existing CEL rule that legacy `delegateClients` and canonical `inboundGrants.tokenExchange` can't coexist at admission, so a plain OR is safe.

Both fixes verified non-vacuous by reverting each and confirming exactly the new test cases (and nothing else) fail.

CI is green.

Base automatically changed from spiffe-integration-split3-4 to spiffe-integration-split3-3 September 3, 2026 16:55

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at ce2447ff7409e4cb8f4d7c85986ace313526be72: the canonical model is wired, but validation parity remains incomplete.

  • cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:2153: canonical issuer policies bypass the owning MCPExternalAuthConfig Valid boundary when InboundGrants is set. Normalize and run the authoritative trusted-issuer validation before reporting Valid=True; add a reconcile-level Valid=False regression test.
  • cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:615: canonical token-exchange issuer policies lack legacy-equivalent CEL/field constraints (reserved actor claims, wildcard exclusivity, allowMayAct with wildcard, and item lengths). Equivalent inputs must have equivalent admission behavior.
  • cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:682: confidential-client transport CEL checks legacy delegateClients only. Include canonical token-exchange delegate clients and test loopback/non-loopback cases.
  • cmd/thv-operator/controllers/virtualmcpserver_controller.go:403: an unlocked status merge patch can replace the co-owned conditions array and erase runtime-reported conditions. Use optimistic locking or establish a single conditions owner, with a concurrent-update regression test.

The prior inline canonical-grant validation concern also remains at virtualmcpserver_controller.go:670-685.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-3 branch from 40b49e1 to 95b1339 Compare September 3, 2026 18:37
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from ce2447f to 1d2f7a3 Compare September 3, 2026 18:42
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 3, 2026
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-3 branch from 95b1339 to 319de92 Compare September 4, 2026 05:26
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from 1d2f7a3 to 5314ea2 Compare September 4, 2026 05:44
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 4, 2026

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep re-review at 5314ea2a12a770f149714740a8099f3ca586de97 confirms the canonical trusted-issuer validity gap and finds two further condition defects:

  • High: canonical issuer policies remain admitted and published Valid=True although runtime-invalid: legacy-equivalent CEL constraints are absent at cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:615-648, and trusted-issuer validation is skipped when inboundGrants is present at :2153-2157. Normalize/validate before reporting Valid=True, while retaining consumer-specific checks at consumers.
  • Medium: applyDeprecatedInboundGrantCondition adds DeprecatedInboundGrantConfiguration=False even to non-embedded auth modes, at cmd/thv-operator/controllers/mcpexternalauthconfig_controller.go:247-258,718-721. Remove this condition unless the resource has embedded auth configuration.
  • Medium: the new vMCP advisory condition writes the whole co-owned status.conditions array from a stale object at cmd/thv-operator/controllers/virtualmcpserver_controller.go:249-251,403-408. The helper documents that an RFC 7396 merge patch replaces arrays (cmd/thv-operator/pkg/controllerutil/status.go:33-44), so concurrent runtime conditions can be erased. Establish a single conditions owner and test an injected concurrent runtime update.

Prior inline canonical-grant and canonical confidential-transport validation blockers also remain.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from 5314ea2 to 291104f Compare September 4, 2026 08:07
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 4, 2026
The SPIFFE client-auth epic needs a place to configure SPIFFE
association policy without inventing a parallel trust/grant path next
to the existing delegate-client and trusted-issuer configuration. As
more inbound grant families (RFC 8693 token exchange, RFC 7523
JWT-bearer, SPIFFE) accumulate, they need one canonical surface to
configure and reason about instead of three independent ones, without
breaking deployments that already rely on the legacy fields.

Add pkg/authserver/inbound_grants.go with NormalizeInboundGrants, which
reconciles a new canonical RunConfig.InboundGrants surface (per-family
token_exchange/jwt_bearer sub-configs whose issuer_policies reference a
trusted_issuers entry by name) against the legacy top-level
delegate_clients and the RFC 8693/7523 fields embedded directly on
trusted_issuers. Legacy and canonical configuration for the same grant
family are mutually exclusive and rejected at validation time; the two
families are otherwise independent, and omitting inbound_grants
entirely preserves released behavior. Thread the normalized result
through RunConfig.Validate, the embedded-auth-server runner, and
buildProvider/discovery, adding a DisableTokenExchange capability so
RFC 8693 registration and discovery advertisement can be turned off
together and can't drift out of sync. Add TrustedIssuer.Name so
canonical issuer_policies can reference an issuer without duplicating
its fields.

SPIFFE client authentication (InboundGrants.SPIFFEClientAuth, defined
in the previous commit) is deliberately kept a sibling of TokenExchange
and JWTBearer here, not nested under either: SPIFFE authenticates a
client, it does not by itself grant it anything, so making it subordinate
to RFC 8693 enablement would mean disabling token exchange silently drops
every SPIFFE association, and every SPIFFE-authenticated client would be
implicitly token-exchange-capable. It is validated and wired directly
from RunConfig.InboundGrants in RunConfig.Validate/embeddedauthserver.go,
independent of this file's legacy/canonical projection, so authentication
method and grant-family enablement stay separately configurable.

Update docs/arch/17-token-exchange-delegation.md for the new
inbound_grants shape and the now-conditional token-exchange discovery
advertisement, and add a runner-level test proving the canonical
delegate-client, SPIFFE-client, and jwt_bearer paths reach a running
server (the existing tests only covered normalization in isolation).

SPIFFE client-auth associations always require the token-exchange
grant (the only grant type they may declare), independent of the
legacy/canonical token-exchange projection above: NormalizeInboundGrants
now sets Capabilities.TokenExchange true whenever
InboundGrants.SPIFFEClientAuth is non-empty, so a SPIFFE-only
configuration cannot leave it false and silently disable the RFC 8693
grant handler server-wide -- which would reject every SPIFFE client's
own token requests before authentication is even checked. Guarded by a
regression test in this package (not just the runner-level test above)
since the equivalent fix was previously lost during a rebase when its
only coverage lived one package away.

DCR (RFC 7591 /oauth/register) now rejects a registration whose
effective grant types include token-exchange when it is disabled
server-wide, instead of accepting the client and only failing later,
confusingly, at /oauth/token. The check runs on the post-defaulting
grant types validateGrantTypes already computes (a private_key_jwt
client with an empty grant_types is implicitly token-exchange-only),
so it catches both the explicit and implicit cases the same way scope
validation already gates DCR on ScopesSupported.

Corrected two stale doc references caught in review: the SPIFFE
client-policy field path (inbound_grants.spiffe_client_auth, not
nested under token_exchange) and the JWT-bearer legacy/canonical
conflict wording (family-wide across all issuers, not per-issuer).

Refs #6200

Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
The operator needs the shared inbound-grant model before it can add SPIFFE associations without inventing a separate Kubernetes API.
Add v1beta1 grant-family types, CEL constraints, runtime conversion, generated schemas, and compatibility coverage for existing grant fields.

Refs #6200
Operators need a visible migration signal before legacy grant fields can be removed safely.
Record deprecated field paths during normalization and surface them as status conditions without changing the effective authorization policy.

Refs #6200
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-5 branch from 291104f to 40a2370 Compare September 4, 2026 12:31
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants