From 8ebd5e5b1d65680dd9a6b816f16d9d18678ff896 Mon Sep 17 00:00:00 2001 From: Emre K <110906681+kocaemre@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:54:13 +0200 Subject: [PATCH] Guard token exchange may_act CEL Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com> --- .../api/v1beta1/mcpexternalauthconfig_types.go | 2 +- .../mcp-external-auth/inbound_grants_cel_test.go | 8 ++++++++ .../toolhive.stacklok.dev_mcpexternalauthconfigs.yaml | 6 ++++-- .../crds/toolhive.stacklok.dev_virtualmcpservers.yaml | 6 ++++-- .../toolhive.stacklok.dev_mcpexternalauthconfigs.yaml | 6 ++++-- .../toolhive.stacklok.dev_virtualmcpservers.yaml | 6 ++++-- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go index 246df8759e..9c97e20999 100644 --- a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go +++ b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go @@ -827,7 +827,7 @@ type TokenExchangeInboundGrantConfig struct { // TokenExchangeIssuerPolicyConfig binds RFC 8693 policy to a named trusted issuer. // // +kubebuilder:validation:XValidation:rule="!('*' in self.allowedDelegateClients) || size(self.allowedDelegateClients) == 1",message="allowedDelegateClients must not combine the wildcard \"*\" with specific client IDs" -// +kubebuilder:validation:XValidation:rule="!(self.allowMayAct && '*' in self.allowedDelegateClients)",message="allowMayAct must not be enabled when allowedDelegateClients contains the wildcard \"*\"" +// +kubebuilder:validation:XValidation:rule="!(has(self.allowMayAct) && self.allowMayAct && '*' in self.allowedDelegateClients)",message="allowMayAct must not be enabled when allowedDelegateClients contains the wildcard \"*\"" // +kubebuilder:validation:XValidation:rule="!has(self.actorClaim) || !(self.actorClaim in ['sub', 'iss', 'aud', 'exp', 'iat', 'nbf', 'jti', 'name', 'email', 'scope', 'scp', 'may_act'])",message="actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid" // //nolint:lll // CEL validation rules exceed line length limits. diff --git a/cmd/thv-operator/test-integration/mcp-external-auth/inbound_grants_cel_test.go b/cmd/thv-operator/test-integration/mcp-external-auth/inbound_grants_cel_test.go index 43a3325a77..d324047f0f 100644 --- a/cmd/thv-operator/test-integration/mcp-external-auth/inbound_grants_cel_test.go +++ b/cmd/thv-operator/test-integration/mcp-external-auth/inbound_grants_cel_test.go @@ -91,6 +91,14 @@ var _ = Describe("MCPExternalAuthConfig inbound grants CEL validation", func() { }}, }} }}, + {name: "canonical token exchange wildcard delegate with omitted may_act", shouldAdmit: true, mutate: func(c *mcpv1beta1.EmbeddedAuthServerConfig) { + c.TrustedIssuers = []mcpv1beta1.TrustedIssuerConfig{{Name: "issuer", IssuerURL: "https://issuer.example.com"}} + c.InboundGrants = &mcpv1beta1.InboundGrantsConfig{TokenExchange: &mcpv1beta1.TokenExchangeInboundGrantConfig{ + IssuerPolicies: []mcpv1beta1.TokenExchangeIssuerPolicyConfig{{ + IssuerRef: "issuer", ExpectedAudience: "https://mcp.example.com", AllowedDelegateClients: []string{"*"}, + }}, + }} + }}, {name: "canonical JWT bearer", shouldAdmit: true, mutate: func(c *mcpv1beta1.EmbeddedAuthServerConfig) { c.TrustedIssuers = []mcpv1beta1.TrustedIssuerConfig{{Name: "issuer", IssuerURL: "https://issuer.example.com"}} c.InboundGrants = &mcpv1beta1.InboundGrantsConfig{JWTBearer: &mcpv1beta1.JWTBearerInboundGrantConfig{ diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index 6b29c0ebe4..02e3236a7d 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -775,7 +775,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid @@ -3463,7 +3464,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml index cfe115fe79..ebd3b72190 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -651,7 +651,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid @@ -5501,7 +5502,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index 3eacb211dd..9b4429b432 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -778,7 +778,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid @@ -3466,7 +3467,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml index 4858264964..cf93566975 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -654,7 +654,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid @@ -5504,7 +5505,8 @@ spec: size(self.allowedDelegateClients) == 1' - message: allowMayAct must not be enabled when allowedDelegateClients contains the wildcard "*" - rule: '!(self.allowMayAct && ''*'' in self.allowedDelegateClients)' + rule: '!(has(self.allowMayAct) && self.allowMayAct + && ''*'' in self.allowedDelegateClients)' - message: actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid