Expose canonical inbound grants in CRDs - #6499
Conversation
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
JAORMX
left a comment
There was a problem hiding this comment.
The canonical model is wired through the CRDs, conversion, and generated artifacts, but two correctness gaps block approval:
-
VirtualMCPServerinline auth validation still recognizes only legacydelegateClients/trustedIssuers[*].jwtBearerGrantas satisfying token-only operation (cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685). A valid inline configuration using onlyspec.authServerConfig.inboundGrantsis admitted but markedAuthServerConfigValidated=Falseand never deploys. Please make this path recognize configured canonical grant families, consistent with the external-auth path. -
MCPExternalAuthConfigvalidation's confidential-client transport check only considers legacyDelegateClients(cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:961-967, called at2129-2133). A canonical delegate client for an invalid non-loopback HTTP issuer can therefore reportValid=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.
fbe4ff6 to
67f5da4
Compare
5834d9c to
6dbb23e
Compare
67f5da4 to
b4c8fed
Compare
6dbb23e to
e038ceb
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Re-review at e038ceb5d38e0961cb3b332c58359863bbf392f6: the two prior blockers are still unresolved.
VirtualMCPServeraccepts canonical grants in CRD admission but its inline runtime guard still ignorescfg.InboundGrants, rejecting valid token-only configurations (cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685).ValidateConfidentialClientTransportstill ignores canonicalinboundGrants.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
left a comment
There was a problem hiding this comment.
Changes requested:
cmd/thv-operator/controllers/virtualmcpserver_controller.go:670: inline validation ignores canonicalInboundGrants, 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 publishedValid=Truedespite invalid policy values. Validate normalized canonical policies at this boundary.
JAORMX
left a comment
There was a problem hiding this comment.
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.
e038ceb to
ae26bc4
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Changes requested:
cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685: inline validation still ignores canonicalInboundGrants, 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 owningMCPExternalAuthConfigtrusted-issuer validation and can be reportedValid=Truedespite 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.
ae26bc4 to
ce2447f
Compare
|
Pushed fixes for both findings.
Both fixes verified non-vacuous by reverting each and confirming exactly the new test cases (and nothing else) fail. CI is green. |
JAORMX
left a comment
There was a problem hiding this comment.
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 owningMCPExternalAuthConfigValidboundary whenInboundGrantsis set. Normalize and run the authoritative trusted-issuer validation before reportingValid=True; add a reconcile-levelValid=Falseregression 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,allowMayActwith 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 legacydelegateClientsonly. 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-ownedconditionsarray 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.
40b49e1 to
95b1339
Compare
ce2447f to
1d2f7a3
Compare
95b1339 to
319de92
Compare
1d2f7a3 to
5314ea2
Compare
JAORMX
left a comment
There was a problem hiding this comment.
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=Truealthough runtime-invalid: legacy-equivalent CEL constraints are absent atcmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:615-648, and trusted-issuer validation is skipped wheninboundGrantsis present at:2153-2157. Normalize/validate before reportingValid=True, while retaining consumer-specific checks at consumers. - Medium:
applyDeprecatedInboundGrantConditionaddsDeprecatedInboundGrantConfiguration=Falseeven to non-embedded auth modes, atcmd/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.conditionsarray from a stale object atcmd/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.
5314ea2 to
291104f
Compare
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
291104f to
40a2370
Compare
Summary
The operator's CRDs (
VirtualMCPServer,MCPExternalAuthConfig) only expose the legacy, per-field inbound-grant configuration (delegateClients,trustedIssuers[*]policy fields). The canonicalinboundGrantsgrant-family model already exists at the runtimeRunConfiglayer (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.inboundGrantsCRD types (TokenExchange/JWTBearergrant families, delegate clients, issuer policies) to the sharedEmbeddedAuthServerConfig, with CEL admission validation for legacy/canonical mutual exclusion.authserver.RunConfigvia the same converter pattern the legacy fields already use.ConditionTypeVirtualMCPServerDeprecatedInboundGrantConfiguration) during normalization, without changing the effective authorization policy — this gives operators a visible migration signal before legacy fields are removed.Refs #6200
Type of change
Test plan
task test)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
v1beta1API —inboundGrantsis 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 viatask operator-manifests/task operator-generate) anddocs/operator/crd-api.md(regenerated viatask 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
inboundGrantsonVirtualMCPServer/MCPExternalAuthConfigdirectly, 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/InboundGrantsRunConfigshape rather than merged as-is. Stacked on #6474.