Skip to content

Commit a2d16e9

Browse files
Cali0707manusa
andauthored
feat: Multi Cluster Support (#348)
* feat: add cluster provider for kubeconfig Signed-off-by: Calum Murray <[email protected]> * feat: move server to use ClusterProvider interface Signed-off-by: Calum Murray <[email protected]> * feat: authentication middleware works with cluster provider Signed-off-by: Calum Murray <[email protected]> * fix: unit tests work after cluster provider changes Signed-off-by: Calum Murray <[email protected]> * feat: add tool mutator to add cluster parameter Signed-off-by: Calum Murray <[email protected]> * test: handle cluster parameter Signed-off-by: Calum Murray <[email protected]> * fix: handle lazy init correctly Signed-off-by: Calum Murray <[email protected]> * refactor: move to using multi-strategy ManagerProvider Signed-off-by: Calum Murray <[email protected]> * feat: add contexts_list tool Signed-off-by: Calum Murray <[email protected]> * refactor: make tool mutator generic between cluster/context naming Signed-off-by: Calum Murray <[email protected]> * feat: introduce tool filter Signed-off-by: Calum Murray <[email protected]> * refactor: use new ManagerProvider/mutator/filter within mcp server Signed-off-by: Calum Murray <[email protected]> * fix(test): tests expect context parameter in tool defs Signed-off-by: Calum Murray <[email protected]> * feat: auth handles multi-cluster case correctly Signed-off-by: Calum Murray <[email protected]> * fix: small changes from local testing Signed-off-by: Calum Murray <[email protected]> * chore: fix enum test Signed-off-by: Calum Murray <[email protected]> * review: Multi Cluster support (#1) * nit: rename contexts_list to configuration_contexts_list Besides the conventional naming, it helps LLMs understand the context of the tool by providing a certain level of hierarchy. Signed-off-by: Marc Nuri <[email protected]> * fix(mcp): ToolMutator doesn't rely on magic strings Signed-off-by: Marc Nuri <[email protected]> * refactor(api): don't expose ManagerProvider to toolsets Signed-off-by: Marc Nuri <[email protected]> * test(mcp): configuration_contexts_list basic tests Signed-off-by: Marc Nuri <[email protected]> * test(toolsets): revert edge-case test This test should not be touched. Signed-off-by: Marc Nuri <[email protected]> * test(toolsets): add specific metadata tests for multi-cluster Signed-off-by: Marc Nuri <[email protected]> * fix(mcp): ToolFilter doesn't rely on magic strings (partially) Signed-off-by: Marc Nuri <[email protected]> * test(api): IsClusterAware and IsTargetListProvider default values Signed-off-by: Marc Nuri <[email protected]> * test(mcp): revert unneeded changes in mcp_tools_test.go Signed-off-by: Marc Nuri <[email protected]> --------- Signed-off-by: Marc Nuri <[email protected]> * fix: always include configuration_contexts_list if contexts > 1 Signed-off-by: Calum Murray <[email protected]> * feat: include server urls in configuration_contexts_list Signed-off-by: Calum Murray <[email protected]> --------- Signed-off-by: Calum Murray <[email protected]> Signed-off-by: Marc Nuri <[email protected]> Co-authored-by: Marc Nuri <[email protected]>
1 parent c447bf8 commit a2d16e9

21 files changed

+2529
-95
lines changed

internal/test/kubernetes.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,10 @@ func KubeConfigFake() *clientcmdapi.Config {
88
fakeConfig := clientcmdapi.NewConfig()
99
fakeConfig.Clusters["fake"] = clientcmdapi.NewCluster()
1010
fakeConfig.Clusters["fake"].Server = "https://127.0.0.1:6443"
11-
fakeConfig.Clusters["additional-cluster"] = clientcmdapi.NewCluster()
1211
fakeConfig.AuthInfos["fake"] = clientcmdapi.NewAuthInfo()
13-
fakeConfig.AuthInfos["additional-auth"] = clientcmdapi.NewAuthInfo()
1412
fakeConfig.Contexts["fake-context"] = clientcmdapi.NewContext()
1513
fakeConfig.Contexts["fake-context"].Cluster = "fake"
1614
fakeConfig.Contexts["fake-context"].AuthInfo = "fake"
17-
fakeConfig.Contexts["additional-context"] = clientcmdapi.NewContext()
18-
fakeConfig.Contexts["additional-context"].Cluster = "additional-cluster"
19-
fakeConfig.Contexts["additional-context"].AuthInfo = "additional-auth"
2015
fakeConfig.CurrentContext = "fake-context"
2116
return fakeConfig
2217
}

internal/test/mock_server.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,14 @@ func (m *MockServer) Kubeconfig() *api.Config {
7373
}
7474

7575
func (m *MockServer) KubeconfigFile(t *testing.T) string {
76-
kubeconfig := filepath.Join(t.TempDir(), "config")
77-
err := clientcmd.WriteToFile(*m.Kubeconfig(), kubeconfig)
76+
return KubeconfigFile(t, m.Kubeconfig())
77+
}
78+
79+
func KubeconfigFile(t *testing.T, kubeconfig *api.Config) string {
80+
kubeconfigFile := filepath.Join(t.TempDir(), "config")
81+
err := clientcmd.WriteToFile(*kubeconfig, kubeconfigFile)
7882
require.NoError(t, err, "Expected no error writing kubeconfig file")
79-
return kubeconfig
83+
return kubeconfigFile
8084
}
8185

8286
func WriteObject(w http.ResponseWriter, obj runtime.Object) {

pkg/api/toolsets.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,29 @@ import (
1010
)
1111

1212
type ServerTool struct {
13-
Tool Tool
14-
Handler ToolHandlerFunc
13+
Tool Tool
14+
Handler ToolHandlerFunc
15+
ClusterAware *bool
16+
TargetListProvider *bool
17+
}
18+
19+
// IsClusterAware indicates whether the tool can accept a "cluster" or "context" parameter
20+
// to operate on a specific Kubernetes cluster context.
21+
// Defaults to true if not explicitly set
22+
func (s *ServerTool) IsClusterAware() bool {
23+
if s.ClusterAware != nil {
24+
return *s.ClusterAware
25+
}
26+
return true
27+
}
28+
29+
// IsTargetListProvider indicates whether the tool is used to provide a list of targets (clusters/contexts)
30+
// Defaults to false if not explicitly set
31+
func (s *ServerTool) IsTargetListProvider() bool {
32+
if s.TargetListProvider != nil {
33+
return *s.TargetListProvider
34+
}
35+
return false
1536
}
1637

1738
type Toolset interface {

pkg/api/toolsets_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package api
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/suite"
7+
"k8s.io/utils/ptr"
8+
)
9+
10+
type ToolsetsSuite struct {
11+
suite.Suite
12+
}
13+
14+
func (s *ToolsetsSuite) TestServerTool() {
15+
s.Run("IsClusterAware", func() {
16+
s.Run("defaults to true", func() {
17+
tool := &ServerTool{}
18+
s.True(tool.IsClusterAware(), "Expected IsClusterAware to be true by default")
19+
})
20+
s.Run("can be set to false", func() {
21+
tool := &ServerTool{ClusterAware: ptr.To(false)}
22+
s.False(tool.IsClusterAware(), "Expected IsClusterAware to be false when set to false")
23+
})
24+
s.Run("can be set to true", func() {
25+
tool := &ServerTool{ClusterAware: ptr.To(true)}
26+
s.True(tool.IsClusterAware(), "Expected IsClusterAware to be true when set to true")
27+
})
28+
})
29+
s.Run("IsTargetListProvider", func() {
30+
s.Run("defaults to false", func() {
31+
tool := &ServerTool{}
32+
s.False(tool.IsTargetListProvider(), "Expected IsTargetListProvider to be false by default")
33+
})
34+
s.Run("can be set to false", func() {
35+
tool := &ServerTool{TargetListProvider: ptr.To(false)}
36+
s.False(tool.IsTargetListProvider(), "Expected IsTargetListProvider to be false when set to false")
37+
})
38+
s.Run("can be set to true", func() {
39+
tool := &ServerTool{TargetListProvider: ptr.To(true)}
40+
s.True(tool.IsTargetListProvider(), "Expected IsTargetListProvider to be true when set to true")
41+
})
42+
})
43+
}
44+
45+
func TestToolsets(t *testing.T) {
46+
suite.Run(t, new(ToolsetsSuite))
47+
}

pkg/config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import (
66
"github.com/BurntSushi/toml"
77
)
88

9+
const (
10+
ClusterProviderKubeConfig = "kubeconfig"
11+
ClusterProviderInCluster = "in-cluster"
12+
)
13+
914
// StaticConfig is the configuration for the server.
1015
// It allows to configure server specific settings and tools to be enabled or disabled.
1116
type StaticConfig struct {
@@ -49,6 +54,12 @@ type StaticConfig struct {
4954
StsScopes []string `toml:"sts_scopes,omitempty"`
5055
CertificateAuthority string `toml:"certificate_authority,omitempty"`
5156
ServerURL string `toml:"server_url,omitempty"`
57+
// ClusterProviderStrategy is how the server finds clusters.
58+
// If set to "kubeconfig", the clusters will be loaded from those in the kubeconfig.
59+
// If set to "in-cluster", the server will use the in cluster config
60+
ClusterProviderStrategy string `toml:"cluster_provider_strategy,omitempty"`
61+
// ClusterContexts is which context should be used for each cluster
62+
ClusterContexts map[string]string `toml:"cluster_contexts"`
5263
}
5364

5465
func Default() *StaticConfig {

pkg/http/authorization.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package http
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"fmt"
8+
"io"
69
"net/http"
710
"strings"
811

@@ -20,7 +23,44 @@ import (
2023

2124
type KubernetesApiTokenVerifier interface {
2225
// KubernetesApiVerifyToken TODO: clarify proper implementation
23-
KubernetesApiVerifyToken(ctx context.Context, token, audience string) (*authenticationapiv1.UserInfo, []string, error)
26+
KubernetesApiVerifyToken(ctx context.Context, token, audience, cluster string) (*authenticationapiv1.UserInfo, []string, error)
27+
// GetTargetParameterName returns the parameter name used for target identification in MCP requests
28+
GetTargetParameterName() string
29+
}
30+
31+
// extractTargetFromRequest extracts cluster parameter from MCP request body
32+
func extractTargetFromRequest(r *http.Request, targetName string) (string, error) {
33+
if r.Body == nil {
34+
return "", nil
35+
}
36+
37+
// Read the body
38+
body, err := io.ReadAll(r.Body)
39+
if err != nil {
40+
return "", err
41+
}
42+
43+
// Restore the body for downstream handlers
44+
r.Body = io.NopCloser(bytes.NewBuffer(body))
45+
46+
// Parse the MCP request
47+
var mcpRequest struct {
48+
Params struct {
49+
Arguments map[string]interface{} `json:"arguments"`
50+
} `json:"params"`
51+
}
52+
53+
if err := json.Unmarshal(body, &mcpRequest); err != nil {
54+
// If we can't parse the request, just return empty cluster (will use default)
55+
return "", nil
56+
}
57+
58+
// Extract target parameter
59+
if cluster, ok := mcpRequest.Params.Arguments[targetName].(string); ok {
60+
return cluster, nil
61+
}
62+
63+
return "", nil
2464
}
2565

2666
// write401 sends a 401/Unauthorized response with WWW-Authenticate header.
@@ -132,7 +172,12 @@ func AuthorizationMiddleware(staticConfig *config.StaticConfig, oidcProvider *oi
132172
}
133173
// Kubernetes API Server TokenReview validation
134174
if err == nil && staticConfig.ValidateToken {
135-
err = claims.ValidateWithKubernetesApi(r.Context(), staticConfig.OAuthAudience, verifier)
175+
targetParameterName := verifier.GetTargetParameterName()
176+
cluster, clusterErr := extractTargetFromRequest(r, targetParameterName)
177+
if clusterErr != nil {
178+
klog.V(2).Infof("Failed to extract cluster from request, using default: %v", clusterErr)
179+
}
180+
err = claims.ValidateWithKubernetesApi(r.Context(), staticConfig.OAuthAudience, cluster, verifier)
136181
}
137182
if err != nil {
138183
klog.V(1).Infof("Authentication failed - JWT validation error: %s %s from %s, error: %v", r.Method, r.URL.Path, r.RemoteAddr, err)
@@ -200,9 +245,9 @@ func (c *JWTClaims) ValidateWithProvider(ctx context.Context, audience string, p
200245
return nil
201246
}
202247

203-
func (c *JWTClaims) ValidateWithKubernetesApi(ctx context.Context, audience string, verifier KubernetesApiTokenVerifier) error {
248+
func (c *JWTClaims) ValidateWithKubernetesApi(ctx context.Context, audience, cluster string, verifier KubernetesApiTokenVerifier) error {
204249
if verifier != nil {
205-
_, _, err := verifier.KubernetesApiVerifyToken(ctx, c.Token, audience)
250+
_, _, err := verifier.KubernetesApiVerifyToken(ctx, c.Token, audience, cluster)
206251
if err != nil {
207252
return fmt.Errorf("kubernetes API token validation error: %v", err)
208253
}

pkg/http/http_test.go

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ func TestHealthCheck(t *testing.T) {
292292
})
293293
})
294294
// Health exposed even when require Authorization
295-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true}}, func(ctx *httpContext) {
295+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
296296
resp, err := http.Get(fmt.Sprintf("http://%s/healthz", ctx.HttpAddress))
297297
if err != nil {
298298
t.Fatalf("Failed to get health check endpoint with OAuth: %v", err)
@@ -313,7 +313,7 @@ func TestWellKnownReverseProxy(t *testing.T) {
313313
".well-known/openid-configuration",
314314
}
315315
// With No Authorization URL configured
316-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true}}, func(ctx *httpContext) {
316+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
317317
for _, path := range cases {
318318
resp, err := http.Get(fmt.Sprintf("http://%s/%s", ctx.HttpAddress, path))
319319
t.Cleanup(func() { _ = resp.Body.Close() })
@@ -333,7 +333,12 @@ func TestWellKnownReverseProxy(t *testing.T) {
333333
_, _ = w.Write([]byte(`NOT A JSON PAYLOAD`))
334334
}))
335335
t.Cleanup(invalidPayloadServer.Close)
336-
invalidPayloadConfig := &config.StaticConfig{AuthorizationURL: invalidPayloadServer.URL, RequireOAuth: true, ValidateToken: true}
336+
invalidPayloadConfig := &config.StaticConfig{
337+
AuthorizationURL: invalidPayloadServer.URL,
338+
RequireOAuth: true,
339+
ValidateToken: true,
340+
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
341+
}
337342
testCaseWithContext(t, &httpContext{StaticConfig: invalidPayloadConfig}, func(ctx *httpContext) {
338343
for _, path := range cases {
339344
resp, err := http.Get(fmt.Sprintf("http://%s/%s", ctx.HttpAddress, path))
@@ -358,7 +363,12 @@ func TestWellKnownReverseProxy(t *testing.T) {
358363
_, _ = w.Write([]byte(`{"issuer": "https://example.com","scopes_supported":["mcp-server"]}`))
359364
}))
360365
t.Cleanup(testServer.Close)
361-
staticConfig := &config.StaticConfig{AuthorizationURL: testServer.URL, RequireOAuth: true, ValidateToken: true}
366+
staticConfig := &config.StaticConfig{
367+
AuthorizationURL: testServer.URL,
368+
RequireOAuth: true,
369+
ValidateToken: true,
370+
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
371+
}
362372
testCaseWithContext(t, &httpContext{StaticConfig: staticConfig}, func(ctx *httpContext) {
363373
for _, path := range cases {
364374
resp, err := http.Get(fmt.Sprintf("http://%s/%s", ctx.HttpAddress, path))
@@ -401,7 +411,12 @@ func TestWellKnownOverrides(t *testing.T) {
401411
}`))
402412
}))
403413
t.Cleanup(testServer.Close)
404-
baseConfig := config.StaticConfig{AuthorizationURL: testServer.URL, RequireOAuth: true, ValidateToken: true}
414+
baseConfig := config.StaticConfig{
415+
AuthorizationURL: testServer.URL,
416+
RequireOAuth: true,
417+
ValidateToken: true,
418+
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
419+
}
405420
// With Dynamic Client Registration disabled
406421
disableDynamicRegistrationConfig := baseConfig
407422
disableDynamicRegistrationConfig.DisableDynamicClientRegistration = true
@@ -488,7 +503,7 @@ func TestMiddlewareLogging(t *testing.T) {
488503

489504
func TestAuthorizationUnauthorized(t *testing.T) {
490505
// Missing Authorization header
491-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true}}, func(ctx *httpContext) {
506+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
492507
resp, err := http.Get(fmt.Sprintf("http://%s/mcp", ctx.HttpAddress))
493508
if err != nil {
494509
t.Fatalf("Failed to get protected endpoint: %v", err)
@@ -513,7 +528,7 @@ func TestAuthorizationUnauthorized(t *testing.T) {
513528
})
514529
})
515530
// Authorization header without Bearer prefix
516-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true}}, func(ctx *httpContext) {
531+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
517532
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/mcp", ctx.HttpAddress), nil)
518533
if err != nil {
519534
t.Fatalf("Failed to create request: %v", err)
@@ -538,7 +553,7 @@ func TestAuthorizationUnauthorized(t *testing.T) {
538553
})
539554
})
540555
// Invalid Authorization header
541-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true}}, func(ctx *httpContext) {
556+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
542557
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/mcp", ctx.HttpAddress), nil)
543558
if err != nil {
544559
t.Fatalf("Failed to create request: %v", err)
@@ -569,7 +584,7 @@ func TestAuthorizationUnauthorized(t *testing.T) {
569584
})
570585
})
571586
// Expired Authorization Bearer token
572-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true}}, func(ctx *httpContext) {
587+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
573588
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/mcp", ctx.HttpAddress), nil)
574589
if err != nil {
575590
t.Fatalf("Failed to create request: %v", err)
@@ -600,7 +615,7 @@ func TestAuthorizationUnauthorized(t *testing.T) {
600615
})
601616
})
602617
// Invalid audience claim Bearer token
603-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "expected-audience", ValidateToken: true}}, func(ctx *httpContext) {
618+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "expected-audience", ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
604619
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/mcp", ctx.HttpAddress), nil)
605620
if err != nil {
606621
t.Fatalf("Failed to create request: %v", err)
@@ -633,7 +648,7 @@ func TestAuthorizationUnauthorized(t *testing.T) {
633648
// Failed OIDC validation
634649
oidcTestServer := NewOidcTestServer(t)
635650
t.Cleanup(oidcTestServer.Close)
636-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "mcp-server", ValidateToken: true}, OidcProvider: oidcTestServer.Provider}, func(ctx *httpContext) {
651+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "mcp-server", ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}, OidcProvider: oidcTestServer.Provider}, func(ctx *httpContext) {
637652
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/mcp", ctx.HttpAddress), nil)
638653
if err != nil {
639654
t.Fatalf("Failed to create request: %v", err)
@@ -670,7 +685,7 @@ func TestAuthorizationUnauthorized(t *testing.T) {
670685
"aud": "mcp-server"
671686
}`
672687
validOidcToken := oidctest.SignIDToken(oidcTestServer.PrivateKey, "test-oidc-key-id", oidc.RS256, rawClaims)
673-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "mcp-server", ValidateToken: true}, OidcProvider: oidcTestServer.Provider}, func(ctx *httpContext) {
688+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "mcp-server", ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}, OidcProvider: oidcTestServer.Provider}, func(ctx *httpContext) {
674689
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/mcp", ctx.HttpAddress), nil)
675690
if err != nil {
676691
t.Fatalf("Failed to create request: %v", err)
@@ -703,7 +718,7 @@ func TestAuthorizationUnauthorized(t *testing.T) {
703718
}
704719

705720
func TestAuthorizationRequireOAuthFalse(t *testing.T) {
706-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: false}}, func(ctx *httpContext) {
721+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: false, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
707722
resp, err := http.Get(fmt.Sprintf("http://%s/mcp", ctx.HttpAddress))
708723
if err != nil {
709724
t.Fatalf("Failed to get protected endpoint: %v", err)
@@ -728,7 +743,7 @@ func TestAuthorizationRawToken(t *testing.T) {
728743
{"mcp-server", true}, // Audience set, validation enabled
729744
}
730745
for _, c := range cases {
731-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: c.audience, ValidateToken: c.validateToken}}, func(ctx *httpContext) {
746+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: c.audience, ValidateToken: c.validateToken, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
732747
tokenReviewed := false
733748
ctx.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
734749
if req.URL.EscapedPath() == "/apis/authentication.k8s.io/v1/tokenreviews" {
@@ -777,7 +792,7 @@ func TestAuthorizationOidcToken(t *testing.T) {
777792
validOidcToken := oidctest.SignIDToken(oidcTestServer.PrivateKey, "test-oidc-key-id", oidc.RS256, rawClaims)
778793
cases := []bool{false, true}
779794
for _, validateToken := range cases {
780-
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "mcp-server", ValidateToken: validateToken}, OidcProvider: oidcTestServer.Provider}, func(ctx *httpContext) {
795+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, OAuthAudience: "mcp-server", ValidateToken: validateToken, ClusterProviderStrategy: config.ClusterProviderKubeConfig}, OidcProvider: oidcTestServer.Provider}, func(ctx *httpContext) {
781796
tokenReviewed := false
782797
ctx.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
783798
if req.URL.EscapedPath() == "/apis/authentication.k8s.io/v1/tokenreviews" {
@@ -833,13 +848,14 @@ func TestAuthorizationOidcTokenExchange(t *testing.T) {
833848
cases := []bool{false, true}
834849
for _, validateToken := range cases {
835850
staticConfig := &config.StaticConfig{
836-
RequireOAuth: true,
837-
OAuthAudience: "mcp-server",
838-
ValidateToken: validateToken,
839-
StsClientId: "test-sts-client-id",
840-
StsClientSecret: "test-sts-client-secret",
841-
StsAudience: "backend-audience",
842-
StsScopes: []string{"backend-scope"},
851+
RequireOAuth: true,
852+
OAuthAudience: "mcp-server",
853+
ValidateToken: validateToken,
854+
StsClientId: "test-sts-client-id",
855+
StsClientSecret: "test-sts-client-secret",
856+
StsAudience: "backend-audience",
857+
StsScopes: []string{"backend-scope"},
858+
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
843859
}
844860
testCaseWithContext(t, &httpContext{StaticConfig: staticConfig, OidcProvider: oidcTestServer.Provider}, func(ctx *httpContext) {
845861
tokenReviewed := false

0 commit comments

Comments
 (0)