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.
Summary
TokenExchangeIssuerPolicyConfig.allowMayAct(the canonicalinboundGrants.tokenExchange.issuerPolicies[]shape) has a CEL admission rulethat dereferences the optional field without a
has()guard, so admissionfails with
no such keyinstead of treating the omitted field asfalse.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
TrustedIssuerConfigversion of the same rule, onestruct 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
actorClaimrule 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.allowMayActrule is theodd 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" onfalse: if the right-hand operand(
'*' in self.allowedDelegateClients) evaluates tofalse, the wholeexpression short-circuits to
falseand the left-handself.allowMayActaccess is never actually forced to error out. So this only surfaces when
both:
allowedDelegateClientscontains the wildcard"*", ANDallowMayActis omitted from the YAML (no default is applied for CELpurposes on this optional bool)
Repro
Apply a
VirtualMCPServer(orMCPExternalAuthConfig) with:Result:
Expected: admission should succeed, since an omitted
allowMayActshould betreated 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-demoagainst current
mainafter migrating it off the deprecatedtrustedIssuers[].allowedDelegateClients/allowMayActfields onto thecanonical
inboundGrants.tokenExchange.issuerPolicies[]shape. That demolegitimately needs
allowedDelegateClients: ["*"](the delegate clientself-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: falseexplicitly.