mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(auth): introduce scoped based authorization
Signed-off-by: Arda Güçlü <aguclu@redhat.com>
This commit is contained in:
@@ -88,6 +88,7 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
|
||||
// Scopes are likely to be used for authorization.
|
||||
scopes := claims.GetScopes()
|
||||
klog.V(2).Infof("JWT token validated - Scopes: %v", scopes)
|
||||
r = r.WithContext(context.WithValue(r.Context(), mcp.TokenScopesContextKey, scopes))
|
||||
|
||||
// Now, there are a couple of options:
|
||||
// 1. If there is no authorization url configured for this MCP Server,
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/containers/kubernetes-mcp-server/pkg/config"
|
||||
@@ -61,7 +62,7 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat
|
||||
response := map[string]interface{}{
|
||||
"authorization_servers": authServers,
|
||||
"authorization_server": authServers[0],
|
||||
"scopes_supported": []string{},
|
||||
"scopes_supported": mcpServer.GetEnabledTools(),
|
||||
"bearer_methods_supported": []string{"header"},
|
||||
}
|
||||
|
||||
|
||||
@@ -227,18 +227,6 @@ func (m *MCPServerOptions) Validate() error {
|
||||
klog.Warningf("authorization-url is using http://, this is not recommended production use")
|
||||
}
|
||||
}
|
||||
if m.StaticConfig.ServerURL != "" {
|
||||
u, err := url.Parse(m.StaticConfig.ServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Scheme != "https" && u.Scheme != "http" {
|
||||
return fmt.Errorf("--server-url must be a valid URL")
|
||||
}
|
||||
if u.Scheme == "http" {
|
||||
klog.Warningf("server-url is using http://, this is not recommended production use")
|
||||
}
|
||||
}
|
||||
if m.StaticConfig.JwksURL != "" {
|
||||
u, err := url.Parse(m.StaticConfig.JwksURL)
|
||||
if err != nil {
|
||||
|
||||
@@ -255,28 +255,3 @@ func TestAuthorizationURL(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerURL(t *testing.T) {
|
||||
t.Run("invalid server-url without protocol", func(t *testing.T) {
|
||||
ioStreams, _ := testStream()
|
||||
rootCmd := NewMCPServer(ioStreams)
|
||||
rootCmd.SetArgs([]string{"--version", "--require-oauth", "--port=8080", "--server-url", "example.com:8080", "--authorization-url", "https://example.com/auth"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid server-url without protocol, got nil")
|
||||
}
|
||||
expected := "--server-url must be a valid URL"
|
||||
if !strings.Contains(err.Error(), expected) {
|
||||
t.Fatalf("Expected error to contain %s, got %s", expected, err.Error())
|
||||
}
|
||||
})
|
||||
t.Run("valid server-url with https", func(t *testing.T) {
|
||||
ioStreams, _ := testStream()
|
||||
rootCmd := NewMCPServer(ioStreams)
|
||||
rootCmd.SetArgs([]string{"--version", "--require-oauth", "--port=8080", "--server-url", "https://example.com:8080", "--authorization-url", "https://example.com/auth"})
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error for valid https server-url, got %s", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"github.com/containers/kubernetes-mcp-server/pkg/version"
|
||||
)
|
||||
|
||||
const TokenScopesContextKey = "TokenScopesContextKey"
|
||||
|
||||
type Configuration struct {
|
||||
Profile Profile
|
||||
ListOutput output.Output
|
||||
@@ -45,20 +47,29 @@ func (c *Configuration) isToolApplicable(tool server.ServerTool) bool {
|
||||
type Server struct {
|
||||
configuration *Configuration
|
||||
server *server.MCPServer
|
||||
enabledTools []string
|
||||
k *internalk8s.Manager
|
||||
}
|
||||
|
||||
func NewServer(configuration Configuration) (*Server, error) {
|
||||
s := &Server{
|
||||
configuration: &configuration,
|
||||
server: server.NewMCPServer(
|
||||
version.BinaryName,
|
||||
version.Version,
|
||||
var serverOptions []server.ServerOption
|
||||
serverOptions = append(serverOptions,
|
||||
server.WithResourceCapabilities(true, true),
|
||||
server.WithPromptCapabilities(true),
|
||||
server.WithToolCapabilities(true),
|
||||
server.WithLogging(),
|
||||
server.WithToolHandlerMiddleware(toolCallLoggingMiddleware),
|
||||
)
|
||||
if configuration.StaticConfig.RequireOAuth {
|
||||
serverOptions = append(serverOptions, server.WithToolHandlerMiddleware(toolScopedAuthorizationMiddleware))
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
configuration: &configuration,
|
||||
server: server.NewMCPServer(
|
||||
version.BinaryName,
|
||||
version.Version,
|
||||
serverOptions...,
|
||||
),
|
||||
}
|
||||
if err := s.reloadKubernetesClient(); err != nil {
|
||||
@@ -81,6 +92,7 @@ func (s *Server) reloadKubernetesClient() error {
|
||||
continue
|
||||
}
|
||||
applicableTools = append(applicableTools, tool)
|
||||
s.enabledTools = append(s.enabledTools, tool.Tool.Name)
|
||||
}
|
||||
s.server.SetTools(applicableTools...)
|
||||
return nil
|
||||
@@ -125,6 +137,10 @@ func (s *Server) GetKubernetesAPIServerHost() string {
|
||||
return s.k.GetAPIServerHost()
|
||||
}
|
||||
|
||||
func (s *Server) GetEnabledTools() []string {
|
||||
return s.enabledTools
|
||||
}
|
||||
|
||||
func (s *Server) Close() {
|
||||
if s.k != nil {
|
||||
s.k.Close()
|
||||
@@ -181,3 +197,16 @@ func toolCallLoggingMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFu
|
||||
return next(ctx, ctr)
|
||||
}
|
||||
}
|
||||
|
||||
func toolScopedAuthorizationMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
|
||||
return func(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
scopes, ok := ctx.Value(TokenScopesContextKey).([]string)
|
||||
if !ok {
|
||||
return NewTextResult("", fmt.Errorf("Authorization failed: Access denied: Tool '%s' requires scope 'mcp:%s' but no scope is available", ctr.Params.Name, ctr.Params.Name)), nil
|
||||
}
|
||||
if !slices.Contains(scopes, "mcp:"+ctr.Params.Name) && !slices.Contains(scopes, ctr.Params.Name) {
|
||||
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
|
||||
}
|
||||
return next(ctx, ctr)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user