Skip to content

Commit 639a85a

Browse files
committed
Add scope based authorization for resource_* tools
Signed-off-by: Arda Güçlü <[email protected]>
1 parent 94f7055 commit 639a85a

File tree

5 files changed

+27
-40
lines changed

5 files changed

+27
-40
lines changed

pkg/http/authorization.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package http
33
import (
44
"context"
55
"fmt"
6+
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
67
"net/http"
78
"strings"
89

@@ -89,6 +90,9 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
8990
scopes := claims.GetScopes()
9091
klog.V(2).Infof("JWT token validated - Scopes: %v", scopes)
9192

93+
newCtx := context.WithValue(r.Context(), kubernetes.ScopesContextKey, scopes)
94+
newR := r.WithContext(newCtx)
95+
9296
// Now, there are a couple of options:
9397
// 1. If there is no authorization url configured for this MCP Server,
9498
// that means this token will be used against the Kubernetes API Server.
@@ -101,7 +105,7 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
101105
// 2. b. If this is not the only token in the headers, the token in here is used
102106
// only for authentication and authorization. Therefore, we need to send TokenReview request
103107
// with the other token in the headers (TODO: still need to validate aud and exp of this token separately).
104-
_, _, err = mcpServer.VerifyTokenAPIServer(r.Context(), token, audience)
108+
/*_, _, err = mcpServer.VerifyTokenAPIServer(r.Context(), token, audience)
105109
if err != nil {
106110
klog.V(1).Infof("Authentication failed - API Server token validation error: %s %s from %s, error: %v", r.Method, r.URL.Path, r.RemoteAddr, err)
107111
@@ -112,9 +116,9 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
112116
}
113117
http.Error(w, "Unauthorized: Invalid token", http.StatusUnauthorized)
114118
return
115-
}
119+
}*/
116120

117-
next.ServeHTTP(w, r)
121+
next.ServeHTTP(w, newR)
118122
})
119123
}
120124
}

pkg/http/http.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7-
"github.com/coreos/go-oidc/v3/oidc"
87
"net/http"
98
"os"
109
"os/signal"
1110
"syscall"
1211
"time"
1312

13+
"github.com/coreos/go-oidc/v3/oidc"
14+
1415
"k8s.io/klog/v2"
1516

1617
"github.com/containers/kubernetes-mcp-server/pkg/config"
@@ -61,7 +62,7 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat
6162
response := map[string]interface{}{
6263
"authorization_servers": authServers,
6364
"authorization_server": authServers[0],
64-
"scopes_supported": []string{},
65+
"scopes_supported": mcpServer.GetEnabledTools(),
6566
"bearer_methods_supported": []string{"header"},
6667
}
6768

pkg/kubernetes-mcp-server/cmd/root.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -227,18 +227,6 @@ func (m *MCPServerOptions) Validate() error {
227227
klog.Warningf("authorization-url is using http://, this is not recommended production use")
228228
}
229229
}
230-
if m.StaticConfig.ServerURL != "" {
231-
u, err := url.Parse(m.StaticConfig.ServerURL)
232-
if err != nil {
233-
return err
234-
}
235-
if u.Scheme != "https" && u.Scheme != "http" {
236-
return fmt.Errorf("--server-url must be a valid URL")
237-
}
238-
if u.Scheme == "http" {
239-
klog.Warningf("server-url is using http://, this is not recommended production use")
240-
}
241-
}
242230
if m.StaticConfig.JwksURL != "" {
243231
u, err := url.Parse(m.StaticConfig.JwksURL)
244232
if err != nil {

pkg/kubernetes/kubernetes.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package kubernetes
22

33
import (
44
"context"
5-
"errors"
65
"strings"
76

87
"k8s.io/apimachinery/pkg/runtime"
@@ -31,6 +30,7 @@ type HeaderKey string
3130
const (
3231
CustomAuthorizationHeader = HeaderKey("kubernetes-authorization")
3332
OAuthAuthorizationHeader = HeaderKey("Authorization")
33+
ScopesContextKey = HeaderKey("scopes")
3434

3535
CustomUserAgent = "kubernetes-mcp-server/bearer-token-auth"
3636
)
@@ -146,9 +146,6 @@ func (m *Manager) ToRESTMapper() (meta.RESTMapper, error) {
146146
func (m *Manager) Derived(ctx context.Context) (*Kubernetes, error) {
147147
authorization, ok := ctx.Value(OAuthAuthorizationHeader).(string)
148148
if !ok || !strings.HasPrefix(authorization, "Bearer ") {
149-
if m.staticConfig.RequireOAuth {
150-
return nil, errors.New("oauth token required")
151-
}
152149
return &Kubernetes{manager: m}, nil
153150
}
154151
klog.V(5).Infof("%s header found (Bearer), using provided bearer token", OAuthAuthorizationHeader)
@@ -172,10 +169,6 @@ func (m *Manager) Derived(ctx context.Context) (*Kubernetes, error) {
172169
}
173170
clientCmdApiConfig, err := m.clientCmdConfig.RawConfig()
174171
if err != nil {
175-
if m.staticConfig.RequireOAuth {
176-
klog.Errorf("failed to get kubeconfig: %v", err)
177-
return nil, errors.New("failed to get kubeconfig")
178-
}
179172
return &Kubernetes{manager: m}, nil
180173
}
181174
clientCmdApiConfig.AuthInfos = make(map[string]*clientcmdapi.AuthInfo)
@@ -186,10 +179,6 @@ func (m *Manager) Derived(ctx context.Context) (*Kubernetes, error) {
186179
}}
187180
derived.manager.accessControlClientSet, err = NewAccessControlClientset(derived.manager.cfg, derived.manager.staticConfig)
188181
if err != nil {
189-
if m.staticConfig.RequireOAuth {
190-
klog.Errorf("failed to get kubeconfig: %v", err)
191-
return nil, errors.New("failed to get kubeconfig")
192-
}
193182
return &Kubernetes{manager: m}, nil
194183
}
195184
derived.manager.discoveryClient = memory.NewMemCacheClient(derived.manager.accessControlClientSet.DiscoveryClient())
@@ -199,10 +188,6 @@ func (m *Manager) Derived(ctx context.Context) (*Kubernetes, error) {
199188
)
200189
derived.manager.dynamicClient, err = dynamic.NewForConfig(derived.manager.cfg)
201190
if err != nil {
202-
if m.staticConfig.RequireOAuth {
203-
klog.Errorf("failed to initialize dynamic client: %v", err)
204-
return nil, errors.New("failed to initialize dynamic client")
205-
}
206191
return &Kubernetes{manager: m}, nil
207192
}
208193
return derived, nil

pkg/mcp/mcp.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package mcp
33
import (
44
"context"
55
"fmt"
6-
"k8s.io/klog/v2"
76
"net/http"
87
"slices"
98

9+
"k8s.io/klog/v2"
10+
1011
"github.com/mark3labs/mcp-go/mcp"
1112
"github.com/mark3labs/mcp-go/server"
1213
authenticationapiv1 "k8s.io/api/authentication/v1"
@@ -44,6 +45,7 @@ func (c *Configuration) isToolApplicable(tool server.ServerTool) bool {
4445
type Server struct {
4546
configuration *Configuration
4647
server *server.MCPServer
48+
enabledTools []string
4749
k *internalk8s.Manager
4850
}
4951

@@ -80,6 +82,7 @@ func (s *Server) reloadKubernetesClient() error {
8082
continue
8183
}
8284
applicableTools = append(applicableTools, tool)
85+
s.enabledTools = append(s.enabledTools, tool.Tool.Name)
8386
}
8487
s.server.SetTools(applicableTools...)
8588
return nil
@@ -124,6 +127,10 @@ func (s *Server) GetKubernetesAPIServerHost() string {
124127
return s.k.GetAPIServerHost()
125128
}
126129

130+
func (s *Server) GetEnabledTools() []string {
131+
return s.enabledTools
132+
}
133+
127134
func (s *Server) Close() {
128135
if s.k != nil {
129136
s.k.Close()
@@ -153,12 +160,6 @@ func NewTextResult(content string, err error) *mcp.CallToolResult {
153160
}
154161

155162
func contextFunc(ctx context.Context, r *http.Request) context.Context {
156-
// Get the standard Authorization header (OAuth compliant)
157-
authHeader := r.Header.Get(string(internalk8s.OAuthAuthorizationHeader))
158-
if authHeader != "" {
159-
return context.WithValue(ctx, internalk8s.OAuthAuthorizationHeader, authHeader)
160-
}
161-
162163
// Fallback to custom header for backward compatibility
163164
customAuthHeader := r.Header.Get(string(internalk8s.CustomAuthorizationHeader))
164165
if customAuthHeader != "" {
@@ -171,6 +172,14 @@ func contextFunc(ctx context.Context, r *http.Request) context.Context {
171172
func toolCallLoggingMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
172173
return func(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
173174
klog.V(5).Infof("mcp tool call: %s(%v)", ctr.Params.Name, ctr.Params.Arguments)
175+
176+
scopes, ok := ctx.Value(internalk8s.ScopesContextKey).([]string)
177+
if !ok {
178+
return NewTextResult("", fmt.Errorf("missing or invalid scopes in the token")), nil
179+
}
180+
if !slices.Contains(scopes, "mcp:"+ctr.Params.Name) && !slices.Contains(scopes, ctr.Params.Name) {
181+
return NewTextResult("", fmt.Errorf("Authorization failed: Access denied: Tool '%s' requires scope 'mcp:%s' but only scopes %s are available", ctr.Params.Name, ctr.Params.Name, scopes)), nil
182+
}
174183
return next(ctx, ctr)
175184
}
176185
}

0 commit comments

Comments
 (0)