Skip to content

CEL rule on TokenExchangeIssuerPolicyConfig errors when allowMayAct is omitted + wildcard delegate client #6531

Description

@jhrozek

Summary

TokenExchangeIssuerPolicyConfig.allowMayAct (the canonical
inboundGrants.tokenExchange.issuerPolicies[] shape) has a CEL admission rule
that dereferences the optional field without a has() guard, so admission
fails with no such key instead of treating the omitted field as false.

Where

cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:830:

// +kubebuilder:validation:XValidation:rule="!(self.allowMayAct && '*' in self.allowedDelegateClients)",message="allowMayAct must not be enabled when allowedDelegateClients contains the wildcard \"*\""

Compare with the legacy TrustedIssuerConfig version of the same rule, one
struct up, which does guard it correctly (line 397):

// +kubebuilder:validation:XValidation:rule="!(has(self.allowMayAct) && self.allowMayAct && '*' in self.allowedDelegateClients)",message="..."

and with the neighboring actorClaim rule on the same canonical struct
(line 831), which also guards correctly:

// +kubebuilder:validation:XValidation:rule="!has(self.actorClaim) || !(self.actorClaim in [...])",message="..."

So the canonical TokenExchangeIssuerPolicyConfig.allowMayAct rule is the
odd one out — it was presumably ported from the legacy rule without carrying
over the has() guard.

Why it only sometimes reproduces

CEL's && is "absorbing" on false: if the right-hand operand
('*' in self.allowedDelegateClients) evaluates to false, the whole
expression short-circuits to false and the left-hand self.allowMayAct
access is never actually forced to error out. So this only surfaces when
both:

  • allowedDelegateClients contains the wildcard "*", AND
  • allowMayAct is omitted from the YAML (no default is applied for CEL
    purposes on this optional bool)

Repro

Apply a VirtualMCPServer (or MCPExternalAuthConfig) with:

authServerConfig:
  trustedIssuers:
    - name: some-issuer
      issuerUrl: "https://example.com"
  inboundGrants:
    tokenExchange:
      issuerPolicies:
        - issuerRef: some-issuer
          expectedAudience: "https://resource.example"
          allowedDelegateClients:
            - "*"
          # allowMayAct intentionally omitted

Result:

The VirtualMCPServer "..." is invalid: spec.authServerConfig.inboundGrants.tokenExchange.issuerPolicies[0]: Invalid value: "object": no such key: allowMayAct evaluating rule: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*"

Expected: admission should succeed, since an omitted allowMayAct should be
treated as false (matching the legacy struct's already-correct behavior).

Suggested fix

Add the same has() guard the legacy rule already uses:

// +kubebuilder:validation:XValidation:rule="!(has(self.allowMayAct) && self.allowMayAct && '*' in self.allowedDelegateClients)",message="allowMayAct must not be enabled when allowedDelegateClients contains the wildcard \"*\""

How this was found

Found while re-verifying ~/devel/manifests/rfc8693-delegation-private-key-jwt-demo
against current main after migrating it off the deprecated
trustedIssuers[].allowedDelegateClients/allowMayAct fields onto the
canonical inboundGrants.tokenExchange.issuerPolicies[] shape. That demo
legitimately needs allowedDelegateClients: ["*"] (the delegate client
self-registers via DCR after the CRD is applied, so its client ID isn't
known yet) and omits allowMayAct, which is exactly the failing combination.
Worked around in the demo by setting allowMayAct: false explicitly.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions