Skip to content

Commit 070cfb4

Browse files
committed
test(auth): added tests to verify header propagation
1 parent 2bd12f8 commit 070cfb4

File tree

4 files changed

+112
-17
lines changed

4 files changed

+112
-17
lines changed

pkg/kubernetes/impersonate_roundtripper.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ package kubernetes
22

33
import "net/http"
44

5-
const (
6-
AuthorizationHeader = "Kubernetes-Authorization"
7-
AuthorizationBearerTokenHeader = "Kubernetes-Authorization-Bearer-Token"
8-
)
9-
105
type impersonateRoundTripper struct {
116
delegate http.RoundTripper
127
}

pkg/kubernetes/kubernetes.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import (
1919
"sigs.k8s.io/yaml"
2020
)
2121

22+
const (
23+
AuthorizationHeader = "Kubernetes-Authorization"
24+
AuthorizationBearerTokenHeader = "kubernetes-authorization-bearer-token"
25+
)
26+
2227
type CloseWatchKubeConfig func() error
2328

2429
type Kubernetes struct {
@@ -126,28 +131,41 @@ func (k *Kubernetes) Derived(ctx context.Context) *Kubernetes {
126131
if !ok {
127132
return k
128133
}
129-
var _ error // TODO: ignored --> should be handled eventually
130134
derivedCfg := rest.CopyConfig(k.cfg)
131135
derivedCfg.BearerToken = bearerToken
132136
derivedCfg.BearerTokenFile = ""
133-
derivedCfg.AuthProvider = nil
134137
derivedCfg.Username = ""
135138
derivedCfg.Password = ""
139+
derivedCfg.AuthProvider = nil
140+
derivedCfg.AuthConfigPersister = nil
141+
derivedCfg.ExecProvider = nil
136142
derivedCfg.Impersonate = rest.ImpersonationConfig{}
137-
clientcmdapiConfig, _ := k.clientCmdConfig.RawConfig()
138-
clientcmdapiConfig.AuthInfos = make(map[string]*clientcmdapi.AuthInfo)
143+
clientCmdApiConfig, err := k.clientCmdConfig.RawConfig()
144+
if err != nil {
145+
return k
146+
}
147+
clientCmdApiConfig.AuthInfos = make(map[string]*clientcmdapi.AuthInfo)
139148
derived := &Kubernetes{
140149
Kubeconfig: k.Kubeconfig,
141-
clientCmdConfig: clientcmd.NewDefaultClientConfig(clientcmdapiConfig, nil),
150+
clientCmdConfig: clientcmd.NewDefaultClientConfig(clientCmdApiConfig, nil),
142151
cfg: derivedCfg,
152+
scheme: k.scheme,
153+
parameterCodec: k.parameterCodec,
154+
}
155+
derived.clientSet, err = kubernetes.NewForConfig(derived.cfg)
156+
if err != nil {
157+
return k
158+
}
159+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(derived.cfg)
160+
if err != nil {
161+
return k
143162
}
144-
derived.clientSet, _ = kubernetes.NewForConfig(derived.cfg)
145-
discoveryClient, _ := discovery.NewDiscoveryClientForConfig(derived.cfg)
146163
derived.discoveryClient = memory.NewMemCacheClient(discoveryClient)
147164
derived.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(derived.discoveryClient))
148-
derived.dynamicClient, _ = dynamic.NewForConfig(derived.cfg)
149-
derived.scheme = runtime.NewScheme()
150-
derived.parameterCodec = runtime.NewParameterCodec(derived.scheme)
165+
derived.dynamicClient, err = dynamic.NewForConfig(derived.cfg)
166+
if err != nil {
167+
return k
168+
}
151169
derived.Helm = helm.NewHelm(derived)
152170
return derived
153171
}

pkg/mcp/common_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"github.com/mark3labs/mcp-go/client"
8+
"github.com/mark3labs/mcp-go/client/transport"
89
"github.com/mark3labs/mcp-go/mcp"
910
"github.com/mark3labs/mcp-go/server"
1011
"github.com/pkg/errors"
@@ -97,6 +98,7 @@ type mcpContext struct {
9798
profile Profile
9899
readOnly bool
99100
disableDestructive bool
101+
clientOptions []transport.ClientOption
100102
before func(*mcpContext)
101103
after func(*mcpContext)
102104
ctx context.Context
@@ -124,8 +126,8 @@ func (c *mcpContext) beforeEach(t *testing.T) {
124126
t.Fatal(err)
125127
return
126128
}
127-
c.mcpHttpServer = server.NewTestServer(c.mcpServer.server)
128-
if c.mcpClient, err = client.NewSSEMCPClient(c.mcpHttpServer.URL + "/sse"); err != nil {
129+
c.mcpHttpServer = server.NewTestServer(c.mcpServer.server, server.WithSSEContextFunc(contextFunc))
130+
if c.mcpClient, err = client.NewSSEMCPClient(c.mcpHttpServer.URL+"/sse", c.clientOptions...); err != nil {
129131
t.Fatal(err)
130132
return
131133
}

pkg/mcp/mcp_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package mcp
22

33
import (
44
"context"
5+
"github.com/mark3labs/mcp-go/client"
56
"github.com/mark3labs/mcp-go/mcp"
7+
"net/http"
68
"os"
79
"path/filepath"
810
"runtime"
@@ -88,3 +90,81 @@ func TestDisableDestructive(t *testing.T) {
8890
})
8991
})
9092
}
93+
94+
func TestSseHeaders(t *testing.T) {
95+
mockServer := NewMockServer()
96+
defer mockServer.Close()
97+
before := func(c *mcpContext) {
98+
c.withKubeConfig(mockServer.config)
99+
c.clientOptions = append(c.clientOptions, client.WithHeaders(map[string]string{"kubernetes-authorization-bearer-token": "a-token-from-mcp-client"}))
100+
}
101+
pathHeaders := make(map[string]http.Header, 0)
102+
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
103+
pathHeaders[req.URL.Path] = req.Header.Clone()
104+
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
105+
if req.URL.Path == "/api" {
106+
w.Header().Set("Content-Type", "application/json")
107+
_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
108+
return
109+
}
110+
// Request Performed by DiscoveryClient to Kube API (Get API Groups)
111+
if req.URL.Path == "/apis" {
112+
w.Header().Set("Content-Type", "application/json")
113+
//w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}}]}`))
114+
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[]}`))
115+
return
116+
}
117+
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
118+
if req.URL.Path == "/api/v1" {
119+
w.Header().Set("Content-Type", "application/json")
120+
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","resources":[{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","list","watch","create","update","patch","delete"]}]}`))
121+
return
122+
}
123+
// Request Performed by DynamicClient
124+
if req.URL.Path == "/api/v1/namespaces/default/pods" {
125+
w.Header().Set("Content-Type", "application/json")
126+
_, _ = w.Write([]byte(`{"kind":"PodList","apiVersion":"v1","items":[]}`))
127+
return
128+
}
129+
// Request Performed by kubernetes.Interface
130+
if req.URL.Path == "/api/v1/namespaces/default/pods/a-pod-to-delete" {
131+
w.WriteHeader(200)
132+
return
133+
}
134+
w.WriteHeader(404)
135+
}))
136+
testCaseWithContext(t, &mcpContext{before: before}, func(c *mcpContext) {
137+
c.callTool("pods_list", map[string]interface{}{})
138+
t.Run("DiscoveryClient propagates headers to Kube API", func(t *testing.T) {
139+
if len(pathHeaders) == 0 {
140+
t.Fatalf("No requests were made to Kube API")
141+
}
142+
if pathHeaders["/api"] == nil || pathHeaders["/api"].Get("Authorization") != "Bearer a-token-from-mcp-client" {
143+
t.Fatalf("Overridden header Authorization not found in request to /api")
144+
}
145+
if pathHeaders["/apis"] == nil || pathHeaders["/apis"].Get("Authorization") != "Bearer a-token-from-mcp-client" {
146+
t.Fatalf("Overridden header Authorization not found in request to /apis")
147+
}
148+
if pathHeaders["/api/v1"] == nil || pathHeaders["/api/v1"].Get("Authorization") != "Bearer a-token-from-mcp-client" {
149+
t.Fatalf("Overridden header Authorization not found in request to /api/v1")
150+
}
151+
})
152+
t.Run("DynamicClient propagates headers to Kube API", func(t *testing.T) {
153+
if len(pathHeaders) == 0 {
154+
t.Fatalf("No requests were made to Kube API")
155+
}
156+
if pathHeaders["/api/v1/namespaces/default/pods"] == nil || pathHeaders["/api/v1/namespaces/default/pods"].Get("Authorization") != "Bearer a-token-from-mcp-client" {
157+
t.Fatalf("Overridden header Authorization not found in request to /api/v1/namespaces/default/pods")
158+
}
159+
})
160+
c.callTool("pods_delete", map[string]interface{}{"name": "a-pod-to-delete"})
161+
t.Run("kubernetes.Interface propagates headers to Kube API", func(t *testing.T) {
162+
if len(pathHeaders) == 0 {
163+
t.Fatalf("No requests were made to Kube API")
164+
}
165+
if pathHeaders["/api/v1/namespaces/default/pods/a-pod-to-delete"] == nil || pathHeaders["/api/v1/namespaces/default/pods/a-pod-to-delete"].Get("Authorization") != "Bearer a-token-from-mcp-client" {
166+
t.Fatalf("Overridden header Authorization not found in request to /api/v1/namespaces/default/pods/a-pod-to-delete")
167+
}
168+
})
169+
})
170+
}

0 commit comments

Comments
 (0)