Skip to content

Commit 26df11e

Browse files
committed
Add unauthenticated auth strategy to MCPExternalAuthConfig
Implements support for the 'unauthenticated' authentication strategy to align MCPExternalAuthConfig CRD with vMCP's supported auth strategies. This change adds the third and final auth strategy type: - tokenExchange (CRD) -> token_exchange (vMCP) - headerInjection (CRD) -> header_injection (vMCP) - unauthenticated (CRD) -> unauthenticated (vMCP)
1 parent 52ee1b7 commit 26df11e

File tree

14 files changed

+948
-4
lines changed

14 files changed

+948
-4
lines changed

cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const (
1111

1212
// ExternalAuthTypeHeaderInjection is the type for custom header injection
1313
ExternalAuthTypeHeaderInjection ExternalAuthType = "headerInjection"
14+
15+
// ExternalAuthTypeUnauthenticated is the type for no authentication
16+
// This should only be used for backends on trusted networks (e.g., localhost, VPC)
17+
// or when authentication is handled by network-level security
18+
ExternalAuthTypeUnauthenticated ExternalAuthType = "unauthenticated"
1419
)
1520

1621
// ExternalAuthType represents the type of external authentication
@@ -21,7 +26,7 @@ type ExternalAuthType string
2126
// MCPServer resources in the same namespace.
2227
type MCPExternalAuthConfigSpec struct {
2328
// Type is the type of external authentication to configure
24-
// +kubebuilder:validation:Enum=tokenExchange;headerInjection
29+
// +kubebuilder:validation:Enum=tokenExchange;headerInjection;unauthenticated
2530
// +kubebuilder:validation:Required
2631
Type ExternalAuthType `json:"type"`
2732

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package v1alpha1
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"k8s.io/apimachinery/pkg/runtime"
8+
ctrl "sigs.k8s.io/controller-runtime"
9+
"sigs.k8s.io/controller-runtime/pkg/webhook"
10+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
11+
)
12+
13+
// SetupWebhookWithManager sets up the webhook with the Manager
14+
func (r *MCPExternalAuthConfig) SetupWebhookWithManager(mgr ctrl.Manager) error {
15+
return ctrl.NewWebhookManagedBy(mgr).
16+
For(r).
17+
Complete()
18+
}
19+
20+
//nolint:lll // kubebuilder webhook marker cannot be split
21+
// +kubebuilder:webhook:path=/validate-toolhive-stacklok-com-v1alpha1-mcpexternalauthconfig,mutating=false,failurePolicy=fail,sideEffects=None,groups=toolhive.stacklok.com,resources=mcpexternalauthconfigs,verbs=create;update,versions=v1alpha1,name=vmcpexternalauthconfig.kb.io,admissionReviewVersions=v1
22+
23+
var _ webhook.CustomValidator = &MCPExternalAuthConfig{}
24+
25+
// ValidateCreate implements webhook.CustomValidator
26+
func (r *MCPExternalAuthConfig) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
27+
return nil, r.validate()
28+
}
29+
30+
// ValidateUpdate implements webhook.CustomValidator
31+
func (r *MCPExternalAuthConfig) ValidateUpdate(
32+
_ context.Context, _ runtime.Object, _ runtime.Object,
33+
) (admission.Warnings, error) {
34+
return nil, r.validate()
35+
}
36+
37+
// ValidateDelete implements webhook.CustomValidator
38+
func (*MCPExternalAuthConfig) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
39+
// No validation needed for deletion
40+
return nil, nil
41+
}
42+
43+
// validate performs validation on the MCPExternalAuthConfig spec
44+
func (r *MCPExternalAuthConfig) validate() error {
45+
switch r.Spec.Type {
46+
case ExternalAuthTypeTokenExchange:
47+
if r.Spec.TokenExchange == nil {
48+
return fmt.Errorf("tokenExchange configuration is required when type is 'tokenExchange'")
49+
}
50+
if r.Spec.HeaderInjection != nil {
51+
return fmt.Errorf("headerInjection must not be set when type is 'tokenExchange'")
52+
}
53+
54+
case ExternalAuthTypeHeaderInjection:
55+
if r.Spec.HeaderInjection == nil {
56+
return fmt.Errorf("headerInjection configuration is required when type is 'headerInjection'")
57+
}
58+
if r.Spec.TokenExchange != nil {
59+
return fmt.Errorf("tokenExchange must not be set when type is 'headerInjection'")
60+
}
61+
62+
case ExternalAuthTypeUnauthenticated:
63+
if r.Spec.TokenExchange != nil {
64+
return fmt.Errorf("tokenExchange must not be set when type is 'unauthenticated'")
65+
}
66+
if r.Spec.HeaderInjection != nil {
67+
return fmt.Errorf("headerInjection must not be set when type is 'unauthenticated'")
68+
}
69+
70+
default:
71+
return fmt.Errorf("unsupported auth type: %s", r.Spec.Type)
72+
}
73+
74+
return nil
75+
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package v1alpha1
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
)
11+
12+
func TestMCPExternalAuthConfig_ValidateCreate(t *testing.T) {
13+
t.Parallel()
14+
15+
tests := []struct {
16+
name string
17+
config *MCPExternalAuthConfig
18+
expectError bool
19+
errorMsg string
20+
}{
21+
{
22+
name: "valid unauthenticated",
23+
config: &MCPExternalAuthConfig{
24+
ObjectMeta: metav1.ObjectMeta{
25+
Name: "test-unauthenticated",
26+
Namespace: "default",
27+
},
28+
Spec: MCPExternalAuthConfigSpec{
29+
Type: ExternalAuthTypeUnauthenticated,
30+
},
31+
},
32+
expectError: false,
33+
},
34+
{
35+
name: "unauthenticated with tokenExchange should fail",
36+
config: &MCPExternalAuthConfig{
37+
ObjectMeta: metav1.ObjectMeta{
38+
Name: "test-invalid",
39+
Namespace: "default",
40+
},
41+
Spec: MCPExternalAuthConfigSpec{
42+
Type: ExternalAuthTypeUnauthenticated,
43+
TokenExchange: &TokenExchangeConfig{
44+
TokenURL: "https://oauth.example.com/token",
45+
},
46+
},
47+
},
48+
expectError: true,
49+
errorMsg: "tokenExchange must not be set when type is 'unauthenticated'",
50+
},
51+
{
52+
name: "unauthenticated with headerInjection should fail",
53+
config: &MCPExternalAuthConfig{
54+
ObjectMeta: metav1.ObjectMeta{
55+
Name: "test-invalid",
56+
Namespace: "default",
57+
},
58+
Spec: MCPExternalAuthConfigSpec{
59+
Type: ExternalAuthTypeUnauthenticated,
60+
HeaderInjection: &HeaderInjectionConfig{
61+
HeaderName: "Authorization",
62+
ValueSecretRef: &SecretKeyRef{
63+
Name: "secret",
64+
Key: "key",
65+
},
66+
},
67+
},
68+
},
69+
expectError: true,
70+
errorMsg: "headerInjection must not be set when type is 'unauthenticated'",
71+
},
72+
{
73+
name: "valid tokenExchange",
74+
config: &MCPExternalAuthConfig{
75+
ObjectMeta: metav1.ObjectMeta{
76+
Name: "test-tokenexchange",
77+
Namespace: "default",
78+
},
79+
Spec: MCPExternalAuthConfigSpec{
80+
Type: ExternalAuthTypeTokenExchange,
81+
TokenExchange: &TokenExchangeConfig{
82+
TokenURL: "https://oauth.example.com/token",
83+
Audience: "backend-service",
84+
},
85+
},
86+
},
87+
expectError: false,
88+
},
89+
{
90+
name: "tokenExchange without config should fail",
91+
config: &MCPExternalAuthConfig{
92+
ObjectMeta: metav1.ObjectMeta{
93+
Name: "test-invalid",
94+
Namespace: "default",
95+
},
96+
Spec: MCPExternalAuthConfigSpec{
97+
Type: ExternalAuthTypeTokenExchange,
98+
},
99+
},
100+
expectError: true,
101+
errorMsg: "tokenExchange configuration is required when type is 'tokenExchange'",
102+
},
103+
{
104+
name: "tokenExchange with headerInjection should fail",
105+
config: &MCPExternalAuthConfig{
106+
ObjectMeta: metav1.ObjectMeta{
107+
Name: "test-invalid",
108+
Namespace: "default",
109+
},
110+
Spec: MCPExternalAuthConfigSpec{
111+
Type: ExternalAuthTypeTokenExchange,
112+
TokenExchange: &TokenExchangeConfig{
113+
TokenURL: "https://oauth.example.com/token",
114+
Audience: "backend-service",
115+
},
116+
HeaderInjection: &HeaderInjectionConfig{
117+
HeaderName: "Authorization",
118+
ValueSecretRef: &SecretKeyRef{
119+
Name: "secret",
120+
Key: "key",
121+
},
122+
},
123+
},
124+
},
125+
expectError: true,
126+
errorMsg: "headerInjection must not be set when type is 'tokenExchange'",
127+
},
128+
{
129+
name: "valid headerInjection",
130+
config: &MCPExternalAuthConfig{
131+
ObjectMeta: metav1.ObjectMeta{
132+
Name: "test-headerinjection",
133+
Namespace: "default",
134+
},
135+
Spec: MCPExternalAuthConfigSpec{
136+
Type: ExternalAuthTypeHeaderInjection,
137+
HeaderInjection: &HeaderInjectionConfig{
138+
HeaderName: "X-API-Key",
139+
ValueSecretRef: &SecretKeyRef{
140+
Name: "api-key-secret",
141+
Key: "api-key",
142+
},
143+
},
144+
},
145+
},
146+
expectError: false,
147+
},
148+
{
149+
name: "headerInjection without config should fail",
150+
config: &MCPExternalAuthConfig{
151+
ObjectMeta: metav1.ObjectMeta{
152+
Name: "test-invalid",
153+
Namespace: "default",
154+
},
155+
Spec: MCPExternalAuthConfigSpec{
156+
Type: ExternalAuthTypeHeaderInjection,
157+
},
158+
},
159+
expectError: true,
160+
errorMsg: "headerInjection configuration is required when type is 'headerInjection'",
161+
},
162+
{
163+
name: "headerInjection with tokenExchange should fail",
164+
config: &MCPExternalAuthConfig{
165+
ObjectMeta: metav1.ObjectMeta{
166+
Name: "test-invalid",
167+
Namespace: "default",
168+
},
169+
Spec: MCPExternalAuthConfigSpec{
170+
Type: ExternalAuthTypeHeaderInjection,
171+
HeaderInjection: &HeaderInjectionConfig{
172+
HeaderName: "X-API-Key",
173+
ValueSecretRef: &SecretKeyRef{
174+
Name: "secret",
175+
Key: "key",
176+
},
177+
},
178+
TokenExchange: &TokenExchangeConfig{
179+
TokenURL: "https://oauth.example.com/token",
180+
},
181+
},
182+
},
183+
expectError: true,
184+
errorMsg: "tokenExchange must not be set when type is 'headerInjection'",
185+
},
186+
}
187+
188+
for _, tt := range tests {
189+
t.Run(tt.name, func(t *testing.T) {
190+
t.Parallel()
191+
192+
warnings, err := tt.config.ValidateCreate(context.Background(), tt.config)
193+
194+
if tt.expectError {
195+
require.Error(t, err)
196+
assert.Contains(t, err.Error(), tt.errorMsg)
197+
} else {
198+
require.NoError(t, err)
199+
}
200+
201+
// Warnings should always be nil for now
202+
assert.Nil(t, warnings)
203+
})
204+
}
205+
}
206+
207+
func TestMCPExternalAuthConfig_ValidateUpdate(t *testing.T) {
208+
t.Parallel()
209+
210+
config := &MCPExternalAuthConfig{
211+
ObjectMeta: metav1.ObjectMeta{
212+
Name: "test-unauthenticated",
213+
Namespace: "default",
214+
},
215+
Spec: MCPExternalAuthConfigSpec{
216+
Type: ExternalAuthTypeUnauthenticated,
217+
},
218+
}
219+
220+
// ValidateUpdate should use the same logic as ValidateCreate
221+
warnings, err := config.ValidateUpdate(context.Background(), nil, config)
222+
require.NoError(t, err)
223+
assert.Nil(t, warnings)
224+
225+
// Test invalid update
226+
invalidConfig := &MCPExternalAuthConfig{
227+
ObjectMeta: metav1.ObjectMeta{
228+
Name: "test-invalid",
229+
Namespace: "default",
230+
},
231+
Spec: MCPExternalAuthConfigSpec{
232+
Type: ExternalAuthTypeUnauthenticated,
233+
TokenExchange: &TokenExchangeConfig{
234+
TokenURL: "https://oauth.example.com/token",
235+
},
236+
},
237+
}
238+
239+
warnings, err = invalidConfig.ValidateUpdate(context.Background(), nil, invalidConfig)
240+
require.Error(t, err)
241+
assert.Contains(t, err.Error(), "tokenExchange must not be set when type is 'unauthenticated'")
242+
assert.Nil(t, warnings)
243+
}
244+
245+
func TestMCPExternalAuthConfig_ValidateDelete(t *testing.T) {
246+
t.Parallel()
247+
248+
config := &MCPExternalAuthConfig{
249+
ObjectMeta: metav1.ObjectMeta{
250+
Name: "test-unauthenticated",
251+
Namespace: "default",
252+
},
253+
Spec: MCPExternalAuthConfigSpec{
254+
Type: ExternalAuthTypeUnauthenticated,
255+
},
256+
}
257+
258+
// ValidateDelete should always succeed
259+
warnings, err := config.ValidateDelete(context.Background(), config)
260+
require.NoError(t, err)
261+
assert.Nil(t, warnings)
262+
}

cmd/thv-operator/controllers/virtualmcpserver_deployment.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,10 @@ func (r *VirtualMCPServerReconciler) getExternalAuthConfigSecretEnvVar(
438438
envVarName = generateUniqueHeaderInjectionEnvVarName(externalAuthConfigName)
439439
secretRef = externalAuthConfig.Spec.HeaderInjection.ValueSecretRef
440440

441+
case mcpv1alpha1.ExternalAuthTypeUnauthenticated:
442+
// No secrets to mount for unauthenticated
443+
return nil, nil
444+
441445
default:
442446
return nil, nil // Not applicable
443447
}

cmd/thv-operator/pkg/controllerutil/tokenexchange.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ func AddExternalAuthConfigOptions(
116116
return addTokenExchangeConfig(ctx, c, namespace, externalAuthConfig, options)
117117
case mcpv1alpha1.ExternalAuthTypeHeaderInjection:
118118
return addHeaderInjectionConfig(ctx, c, namespace, externalAuthConfig, options)
119+
case mcpv1alpha1.ExternalAuthTypeUnauthenticated:
120+
// No config to add for unauthenticated
121+
return nil
119122
default:
120123
return fmt.Errorf("unsupported external auth type: %s", externalAuthConfig.Spec.Type)
121124
}

0 commit comments

Comments
 (0)