Skip to content

Normalize canonical inbound grants - #6473

Open
jhrozek wants to merge 1 commit into
spiffe-integration-split3-2from
spiffe-integration-split3-3
Open

Normalize canonical inbound grants#6473
jhrozek wants to merge 1 commit into
spiffe-integration-split3-2from
spiffe-integration-split3-3

Conversation

@jhrozek

@jhrozek jhrozek commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

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.

Stacked on #6467.

  • Adds 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.
  • Threads the normalized result through RunConfig.Validate, the embedded-auth-server runner, and buildProvider/discovery — this is also where spiffe_trust_domains (added in Define SPIFFE trust configuration #6467) finally gets validated and wired into Config.SPIFFETrust, closing the gap flagged in that PR's review.
  • Adds a DisableTokenExchange capability so RFC 8693 registration and discovery advertisement turn off together and can't drift out of sync.
  • Adds TrustedIssuer.Name so canonical issuer_policies can reference an issuer without duplicating its fields.
  • Updates docs/arch/17-token-exchange-delegation.md for the new inbound_grants shape and the now-conditional token-exchange discovery advertisement.
  • Adds 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).

Fixes #

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (describe below)

pkg/authserver/inbound_grants_test.go covers normalization: legacy/canonical mutual exclusion per grant family, issuer-policy resolution by name (unknown/duplicate refs), and capability derivation. pkg/authserver/runner/embeddedauthserver_test.go adds coverage proving the canonical delegate-client, SPIFFE-client, and jwt_bearer paths actually reach a running embedded auth server, not just the normalization step in isolation.

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.

Special notes for reviewers

This closes the "spiffe_trust_domains is a silent no-op" gap flagged in review on #6467RunConfig.Validate() now calls ValidateSPIFFETrust via NormalizeInboundGrants, and Config.SPIFFETrust is built and threaded into the embedded auth server in embeddedauthserver.go. Live SVID/bundle verification is still out of scope here; this is config normalization and wiring only.

@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Aug 31, 2026
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.11111% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.12%. Comparing base (3413cc2) to head (f1fc2d8).

Files with missing lines Patch % Lines
pkg/authserver/runner/embeddedauthserver.go 79.16% 5 Missing ⚠️
pkg/authserver/server_impl.go 85.71% 2 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##           spiffe-integration-split3-2    #6473      +/-   ##
===============================================================
+ Coverage                        78.09%   78.12%   +0.03%     
===============================================================
  Files                              768      769       +1     
  Lines                            74722    74776      +54     
===============================================================
+ Hits                             58351    58420      +69     
+ Misses                           16366    16351      -15     
  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.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-3 branch from a93bd9b to 047b301 Compare August 31, 2026 11: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 Aug 31, 2026
@JAORMX JAORMX mentioned this pull request Aug 31, 2026
11 tasks

@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.

I reviewed this against #6467 and #6200. CI is green, but I found two blocking model/correctness issues:

  1. SPIFFEClients is nested under inbound_grants.token_exchange (pkg/authserver/inbound_grants.go:19-24). That makes SPIFFE client authentication subordinate to RFC 8693: disabling or omitting token exchange removes the association, and every SPIFFE-authenticated client is necessarily token-exchange-enabled. #6200 calls for spiffeClientAuth under inboundGrants with grants and token-exchange permission configured per association. Authentication method and grant authorization need to remain independent in the canonical schema.

  2. SPIFFE validation reports paths under inbound_grants.spiffe_client_auth[...], but this PR's actual serialized path is inbound_grants.token_exchange.spiffe_clients[...] (pkg/authserver/spiffe_trust.go, reached from RunConfig.Validate). Operators therefore get errors naming a field that does not exist. Please make the validation paths match the final schema.

The normalization direction is useful, but it currently projects the canonical wire types back into the legacy combined TrustedIssuer runtime representation. That leaves parallel policy shapes and several field-by-field copy/conflict-detection sites that must evolve in lockstep. I would prefer one grant-oriented normalized runtime model consumed by validation/provider construction, with legacy and canonical wire inputs translated into it once.

Standards notes: the commit is missing the required Signed-off-by trailer (CONTRIBUTING.md:91), and the PR is well beyond the repository's 400-line guideline. The signoff must be fixed; please also consider whether the compatibility normalization can be split from the SPIFFE wiring to reduce review risk.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-2 branch 2 times, most recently from 8abeeaa to 108bc9c Compare August 31, 2026 14:19
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-3 branch from 047b301 to 7c5e172 Compare August 31, 2026 14:26
@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 Aug 31, 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.

I re-reviewed the rebased head (7c5e172). The rebase fixes the two model issues from my previous review: spiffe_client_auth remains a sibling of the grant families, validation paths now match the schema, and the commit has its DCO trailer.

One new enablement inconsistency blocks approval:

  • Canonical inbound_grants can disable token exchange, and this PR correctly removes the Fosite handler and discovery advertisement via TokenExchangeEnabled. However, /oauth/register still calls ValidateDCRRequest without that capability (pkg/authserver/server/handlers/dcr.go:243-257). It can therefore return 201 Created for a token-exchange-only client even though the server has no token-exchange handler. This is especially direct for private_key_jwt, whose token-only path explicitly accepts a token-exchange registration. Please thread TokenExchangeEnabled into DCR validation (or reject allow_private_key_jwt_registration when canonical configuration disables token exchange) and add a regression test proving a disabled grant cannot be registered.

The architecture doc also still places SPIFFE clients at inbound_grants.token_exchange.spiffe_clients; the implemented path is the sibling inbound_grants.spiffe_client_auth. Please update those references and make the documented legacy/canonical exclusion scope match the family-wide behavior in NormalizeInboundGrants.

CI currently has two failures. Both look unrelated to this authserver diff: one VirtualMCP Redis/session E2E timeout interrupted the remaining specs, and one MCPOIDCConfig integration test timed out amid resource-version conflicts and a missing test field index. They still need a clean rerun before approval.

@jhrozek

jhrozek commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a commit that resolves both blocking issues — thanks for catching these.

  1. SPIFFE subordinate to token exchange: SPIFFEClientAuth is no longer nested under inbound_grants.token_exchange. Following Define SPIFFE trust configuration #6467's schema fix, InboundGrantsRunConfig now has SPIFFEClientAuth as a sibling of TokenExchange/JWTBearer, and NormalizeInboundGrants no longer touches SPIFFE at all — ValidateSPIFFETrust/NewSPIFFETrustConfig read cfg.InboundGrants directly in RunConfig.Validate()/embeddedauthserver.go, independent of the legacy/canonical projection this file does. Disabling or omitting token_exchange no longer drops SPIFFE associations.

    This surfaced a real bug while fixing it: since SPIFFE client auth is now independent of NormalizedInboundGrants.Capabilities.TokenExchange, a config with only SPIFFE associations (no inbound_grants.token_exchange) was computing DisableTokenExchange: true and silently disabling the token-exchange grant server-wide — even though SPIFFE clients exclusively use that grant. Fixed by having prepareInboundGrantConfiguration OR in whether any SPIFFE client-auth association is configured. Caught by re-running the existing TestEmbeddedAuthServer_SPIFFEAssociationDoesNotAuthenticateClient test, which started failing with invalid_request instead of the expected invalid_client once the schema was decoupled.

  2. Validation paths didn't match the schema: fixed as part of Define SPIFFE trust configuration #6467 — error paths already say inbound_grants.spiffe_client_auth[...], matching the real field now that it's flat.

On the normalization-direction note (projecting canonical types back into the legacy TrustedIssuer representation): left as-is for now — agreed it's worth revisiting, but a larger refactor of the runtime model felt like more risk than this PR should carry given the SPIFFE-specific fixes above. Happy to open a follow-up if you'd like it tracked separately.

Signed-off-by trailer fixed. CI is green on the amended commit.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-2 branch from 108bc9c to e497cae Compare August 31, 2026 15:44
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-3 branch from 7c5e172 to a1a21ea Compare August 31, 2026 15:59
@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 Aug 31, 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.

I re-reviewed the current rebased head (a1a21ea). The earlier SPIFFE placement and validation-path fixes remain good, but the latest blocker is still present: validateDCRRequest does not receive TokenExchangeEnabled, so /oauth/register can create a token-exchange-only client after canonical configuration has removed the token-exchange handler and discovery advertisement (pkg/authserver/server/handlers/dcr.go:243-257). Please gate DCR validation on the effective capability and add the disabled-grant regression test.

The architecture documentation still names inbound_grants.token_exchange.spiffe_clients at docs/arch/17-token-exchange-delegation.md:127, while the implemented path is inbound_grants.spiffe_client_auth. Its “mutually exclusive per issuer” wording around line 703 also does not match the family-wide conflict implemented by NormalizeInboundGrants; please align one side.

The amended commit has its DCO trailer. CI is still running on this head.

@jhrozek
jhrozek force-pushed the spiffe-integration-split3-2 branch from e497cae to 3413cc2 Compare August 31, 2026 19:48
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>
@jhrozek
jhrozek force-pushed the spiffe-integration-split3-3 branch from a1a21ea to f1fc2d8 Compare August 31, 2026 20:12
@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 Aug 31, 2026
@jhrozek

jhrozek commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a commit addressing both findings.

DCR/token-exchange capability gating. /oauth/register now rejects a registration whose effective grant types include token-exchange when it's 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 (so it catches both an explicit grant_types field and the implicit private_key_jwt default), using the same h.config.TokenExchangeEnabled runtime-capability pattern the existing scope validation already uses. Added a regression test covering the explicit-grant, implicit-default, and enabled-success cases.

Docs drift. Fixed the stale inbound_grants.token_exchange.spiffe_clients field path (now inbound_grants.spiffe_client_auth) and corrected the "mutually exclusive per issuer" wording — the actual conflict check in NormalizeInboundGrants is family-wide across all issuers, not per-issuer.

Also worth flagging: rebasing onto #6467's fix (the hard-reject on non-empty spiffe_trust_domains, per your review there) surfaced that the SPIFFE-forces-token-exchange-capability regression fix from earlier in this PR's development had been silently lost during a rebase — NormalizeInboundGrants no longer set Capabilities.TokenExchange = true for a SPIFFE-only config. Caught it via a test failure while adapting to the hard-reject, re-applied it (now living directly in NormalizeInboundGrants instead of a separate helper), and added a same-package regression test this time specifically so a future rebase can't silently drop it again without a local test catching it.

CI is green.

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