Skip to content

Commit 1aa0176

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

File tree

4 files changed

+74
-20
lines changed

4 files changed

+74
-20
lines changed

pkg/http/authorization.go

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
11
package http
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"fmt"
8+
"io"
69
"net/http"
10+
"slices"
711
"strings"
812

9-
"github.com/coreos/go-oidc/v3/oidc"
10-
"github.com/go-jose/go-jose/v4"
11-
"github.com/go-jose/go-jose/v4/jwt"
1213
"k8s.io/klog/v2"
1314

1415
"github.com/containers/kubernetes-mcp-server/pkg/mcp"
16+
"github.com/coreos/go-oidc/v3/oidc"
17+
"github.com/go-jose/go-jose/v4"
18+
"github.com/go-jose/go-jose/v4/jwt"
1519
)
1620

1721
const (
1822
Audience = "kubernetes-mcp-server"
1923
)
2024

25+
// MCPRequest represents the structure of an MCP request
26+
type MCPRequest struct {
27+
Method string `json:"method"`
28+
Params struct {
29+
Name string `json:"name"`
30+
Arguments map[string]interface{} `json:"arguments"`
31+
} `json:"params"`
32+
}
33+
2134
// AuthorizationMiddleware validates the OAuth flow using Kubernetes TokenReview API
2235
func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *oidc.Provider, mcpServer *mcp.Server) func(http.Handler) http.Handler {
2336
return func(next http.Handler) http.Handler {
@@ -88,6 +101,18 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
88101
// Scopes are likely to be used for authorization.
89102
scopes := claims.GetScopes()
90103
klog.V(2).Infof("JWT token validated - Scopes: %v", scopes)
104+
if len(scopes) == 0 {
105+
http.Error(w, "InsufficientScopeError", http.StatusForbidden)
106+
return
107+
}
108+
109+
if (r.URL.Path == mcpEndpoint || r.URL.Path == sseMessageEndpoint) && r.Method == "POST" {
110+
if err := validateMCPToolCallScopes(r, scopes, mcpServer.GetEnabledTools()); err != nil {
111+
klog.V(1).Infof("Authorization failed for MCP tool call: %s %s from %s, error: %v", r.Method, r.URL.Path, r.RemoteAddr, err)
112+
http.Error(w, "InsufficientScopeError", http.StatusForbidden)
113+
return
114+
}
115+
}
91116

92117
// Now, there are a couple of options:
93118
// 1. If there is no authorization url configured for this MCP Server,
@@ -101,7 +126,7 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
101126
// 2. b. If this is not the only token in the headers, the token in here is used
102127
// only for authentication and authorization. Therefore, we need to send TokenReview request
103128
// 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)
129+
/*_, _, err = mcpServer.VerifyTokenAPIServer(r.Context(), token, audience)
105130
if err != nil {
106131
klog.V(1).Infof("Authentication failed - token validation error: %s %s from %s, error: %v", r.Method, r.URL.Path, r.RemoteAddr, err)
107132
@@ -112,13 +137,46 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
112137
}
113138
http.Error(w, "Unauthorized: Invalid token", http.StatusUnauthorized)
114139
return
115-
}
140+
}*/
116141

117142
next.ServeHTTP(w, r)
118143
})
119144
}
120145
}
121146

147+
func validateMCPToolCallScopes(r *http.Request, tokenScopes, enabledScopes []string) error {
148+
body, err := io.ReadAll(r.Body)
149+
if err != nil {
150+
return fmt.Errorf("failed to read request body: %w", err)
151+
}
152+
153+
r.Body = io.NopCloser(bytes.NewReader(body))
154+
155+
var mcpReq MCPRequest
156+
if err := json.Unmarshal(body, &mcpReq); err != nil {
157+
return fmt.Errorf("failed to parse MCP request: %w", err)
158+
}
159+
160+
if mcpReq.Method != "tools/call" {
161+
return nil
162+
}
163+
164+
toolName := mcpReq.Params.Name
165+
if toolName == "" {
166+
return fmt.Errorf("tool name is empty")
167+
}
168+
169+
if slices.Contains(enabledScopes, toolName) {
170+
scopedToolName := "mcp:" + toolName
171+
if !slices.Contains(tokenScopes, scopedToolName) {
172+
return fmt.Errorf("tool '%s' not allowed in scopes %v", scopedToolName, tokenScopes)
173+
}
174+
klog.V(2).Infof("MCP resource tool call authorized - tool '%s' found in scopes", scopedToolName)
175+
}
176+
177+
return nil
178+
}
179+
122180
var allSignatureAlgorithms = []jose.SignatureAlgorithm{
123181
jose.EdDSA,
124182
jose.HS256,

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/mcp/mcp.go

Lines changed: 8 additions & 1 deletion
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()

0 commit comments

Comments
 (0)