Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions internal/azureclient/token_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package azureclient

import (
"context"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)

type StaticTokenCredential struct {
token string
}

func NewStaticTokenCredential(token string) *StaticTokenCredential {
return &StaticTokenCredential{
token: token,
}
}

func (c *StaticTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
expiresOn := time.Now().Add(1 * time.Hour)
return azcore.AccessToken{
Token: c.token,
ExpiresOn: expiresOn,
}, nil
}
3 changes: 2 additions & 1 deletion internal/components/fleet/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ func NewClient() (*Client, error) {
k8sExecutor := kubectl.NewExecutor()

// Wrap it using the adapter to work with aks-mcp config
wrappedExecutor := k8s.WrapK8sExecutor(k8sExecutor)
// Fleet operations don't support token-only authentication mode yet (always use local kubeconfig)
wrappedExecutor := k8s.WrapK8sExecutor(k8sExecutor, false)

return &Client{
executor: wrappedExecutor,
Expand Down
20 changes: 20 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ type ConfigData struct {
// Default is false (use new unified tools)
// This flag is provided for backward compatibility and may be removed in future versions
UseLegacyTools bool

// TokenAuthOnly enables token-only authentication mode for tools that support it
// When enabled, supported tools (e.g., kubectl) are executed via Azure AKS RunCommand API using user-provided tokens
// When disabled (default), tools are executed locally with default authentication (e.g., kubeconfig for Kubernetes tools)
TokenAuthOnly bool
}

// NewConfig creates and returns a new configuration instance
Expand All @@ -90,6 +95,7 @@ func NewConfig() *ConfigData {
AllowNamespaces: "",
LogLevel: "info",
UseLegacyTools: os.Getenv("USE_LEGACY_TOOLS") == "true",
TokenAuthOnly: false,
}
}

Expand Down Expand Up @@ -125,6 +131,10 @@ func (cfg *ConfigData) ParseFlags() {
flag.StringVar(&cfg.AllowNamespaces, "allow-namespaces", "",
"Comma-separated list of allowed Kubernetes namespaces (empty means all namespaces)")

// Token-only authentication configuration
flag.BoolVar(&cfg.TokenAuthOnly, "token-auth-only", false,
"Enable token-only authentication mode for supported tools (e.g., kubectl uses Azure AKS RunCommand API with user-provided tokens instead of local kubeconfig)")

// Logging settings
flag.StringVar(&cfg.LogLevel, "log-level", "info", "Log level (debug, info, warn, error)")

Expand Down Expand Up @@ -268,6 +278,16 @@ func (cfg *ConfigData) ValidateConfig() error {
return fmt.Errorf("OAuth authentication is not supported with stdio transport per MCP specification")
}

// Validate token-only authentication + stdio transport compatibility
if cfg.TokenAuthOnly && cfg.Transport == "stdio" {
return fmt.Errorf("token-only authentication mode (--token-auth-only) is not supported with stdio transport, use sse or streamable-http instead")
}

// Validate token-only authentication + legacy tools compatibility
if cfg.TokenAuthOnly && cfg.UseLegacyTools {
return fmt.Errorf("token-only authentication mode (--token-auth-only) requires unified tools and is not compatible with legacy tools (USE_LEGACY_TOOLS=true)")
}

return nil
}

Expand Down
256 changes: 256 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,259 @@ func setEnv(t *testing.T, key, value string) {
func unsetEnv(t *testing.T, key string) {
t.Setenv(key, "")
}

func TestValidateConfig_OAuthWithStdio(t *testing.T) {
cfg := NewConfig()
cfg.OAuthConfig.Enabled = true
cfg.Transport = "stdio"

err := cfg.ValidateConfig()
if err == nil {
t.Fatal("Expected error when OAuth is enabled with stdio transport, got nil")
}

expectedMsg := "OAuth authentication is not supported with stdio transport per MCP specification"
if err.Error() != expectedMsg {
t.Errorf("Expected error '%s', got '%s'", expectedMsg, err.Error())
}
}

func TestValidateConfig_OAuthWithSSE(t *testing.T) {
cfg := NewConfig()
cfg.OAuthConfig.Enabled = true
cfg.Transport = "sse"

err := cfg.ValidateConfig()
if err != nil {
t.Errorf("Expected no error for OAuth with SSE transport, got: %v", err)
}
}

func TestValidateConfig_OAuthWithStreamableHTTP(t *testing.T) {
cfg := NewConfig()
cfg.OAuthConfig.Enabled = true
cfg.Transport = "streamable-http"

err := cfg.ValidateConfig()
if err != nil {
t.Errorf("Expected no error for OAuth with streamable-http transport, got: %v", err)
}
}

func TestValidateConfig_TokenAuthOnlyWithLegacyTools(t *testing.T) {
cfg := NewConfig()
cfg.TokenAuthOnly = true
cfg.UseLegacyTools = true
cfg.Transport = "sse"

err := cfg.ValidateConfig()
if err == nil {
t.Fatal("Expected error when token-only authentication is enabled with legacy tools, got nil")
}

expectedMsg := "token-only authentication mode (--token-auth-only) requires unified tools and is not compatible with legacy tools (USE_LEGACY_TOOLS=true)"
if err.Error() != expectedMsg {
t.Errorf("Expected error '%s', got '%s'", expectedMsg, err.Error())
}
}

func TestValidateConfig_TokenAuthOnlyWithStdio(t *testing.T) {
cfg := NewConfig()
cfg.TokenAuthOnly = true
cfg.Transport = "stdio"

err := cfg.ValidateConfig()
if err == nil {
t.Fatal("Expected error when token-only authentication is enabled with stdio transport, got nil")
}

expectedMsg := "token-only authentication mode (--token-auth-only) is not supported with stdio transport, use sse or streamable-http instead"
if err.Error() != expectedMsg {
t.Errorf("Expected error '%s', got '%s'", expectedMsg, err.Error())
}
}

func TestValidateConfig_TokenAuthOnlyWithSSE(t *testing.T) {
cfg := NewConfig()
cfg.TokenAuthOnly = true
cfg.Transport = "sse"
cfg.UseLegacyTools = false

err := cfg.ValidateConfig()
if err != nil {
t.Errorf("Expected no error for token-only authentication with SSE transport, got: %v", err)
}
}

func TestValidateConfig_TokenAuthOnlyWithStreamableHTTP(t *testing.T) {
cfg := NewConfig()
cfg.TokenAuthOnly = true
cfg.Transport = "streamable-http"
cfg.UseLegacyTools = false

err := cfg.ValidateConfig()
if err != nil {
t.Errorf("Expected no error for token-only authentication with streamable-http transport, got: %v", err)
}
}

func TestValidateConfig_TokenAuthOnlyWithUnifiedTools(t *testing.T) {
cfg := NewConfig()
cfg.TokenAuthOnly = true
cfg.UseLegacyTools = false
cfg.Transport = "sse"

err := cfg.ValidateConfig()
if err != nil {
t.Errorf("Expected no error for token-only authentication with unified tools, got: %v", err)
}
}

func TestValidateConfig_LegacyToolsWithoutTokenAuthOnly(t *testing.T) {
cfg := NewConfig()
cfg.TokenAuthOnly = false
cfg.UseLegacyTools = true

err := cfg.ValidateConfig()
if err != nil {
t.Errorf("Expected no error for legacy tools without token-only authentication, got: %v", err)
}
}

func TestValidateConfig_ValidCombinations(t *testing.T) {
tests := []struct {
name string
oauthEnabled bool
transport string
tokenAuthOnly bool
useLegacyTools bool
wantErr bool
}{
{
name: "OAuth disabled with stdio",
oauthEnabled: false,
transport: "stdio",
tokenAuthOnly: false,
useLegacyTools: false,
wantErr: false,
},
{
name: "OAuth enabled with SSE",
oauthEnabled: true,
transport: "sse",
tokenAuthOnly: false,
useLegacyTools: false,
wantErr: false,
},
{
name: "OAuth enabled with streamable-http",
oauthEnabled: true,
transport: "streamable-http",
tokenAuthOnly: false,
useLegacyTools: false,
wantErr: false,
},
{
name: "Token-only authentication with unified tools",
oauthEnabled: false,
transport: "sse",
tokenAuthOnly: true,
useLegacyTools: false,
wantErr: false,
},
{
name: "Single cluster with legacy tools",
oauthEnabled: false,
transport: "stdio",
tokenAuthOnly: false,
useLegacyTools: true,
wantErr: false,
},
{
name: "All features compatible",
oauthEnabled: true,
transport: "sse",
tokenAuthOnly: true,
useLegacyTools: false,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
cfg.OAuthConfig.Enabled = tt.oauthEnabled
cfg.Transport = tt.transport
cfg.TokenAuthOnly = tt.tokenAuthOnly
cfg.UseLegacyTools = tt.useLegacyTools

err := cfg.ValidateConfig()
if (err != nil) != tt.wantErr {
t.Errorf("ValidateConfig() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestValidateConfig_InvalidCombinations(t *testing.T) {
tests := []struct {
name string
oauthEnabled bool
transport string
tokenAuthOnly bool
useLegacyTools bool
expectedErrMsg string
}{
{
name: "OAuth with stdio",
oauthEnabled: true,
transport: "stdio",
tokenAuthOnly: false,
useLegacyTools: false,
expectedErrMsg: "OAuth authentication is not supported with stdio transport",
},
{
name: "Token-only authentication with stdio",
oauthEnabled: false,
transport: "stdio",
tokenAuthOnly: true,
useLegacyTools: false,
expectedErrMsg: "token-only authentication mode (--token-auth-only) is not supported with stdio transport",
},
{
name: "Token-only authentication with legacy tools",
oauthEnabled: false,
transport: "sse",
tokenAuthOnly: true,
useLegacyTools: true,
expectedErrMsg: "token-only authentication mode (--token-auth-only) requires unified tools",
},
{
name: "All invalid combinations",
oauthEnabled: true,
transport: "stdio",
tokenAuthOnly: true,
useLegacyTools: true,
expectedErrMsg: "OAuth authentication is not supported with stdio transport",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
cfg.OAuthConfig.Enabled = tt.oauthEnabled
cfg.Transport = tt.transport
cfg.TokenAuthOnly = tt.tokenAuthOnly
cfg.UseLegacyTools = tt.useLegacyTools

err := cfg.ValidateConfig()
if err == nil {
t.Fatal("Expected error, got nil")
}

if !contains(err.Error(), tt.expectedErrMsg) {
t.Errorf("Expected error containing '%s', got '%s'", tt.expectedErrMsg, err.Error())
}
})
}
}
8 changes: 8 additions & 0 deletions internal/ctx/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ctx

type ContextKey string

// AzureTokenKey is the context key for storing Azure tokens extracted from HTTP headers.
// This is the name of the HTTP header, not a hardcoded credential.
// #nosec G101
const AzureTokenKey ContextKey = "X-Azure-Token"
18 changes: 14 additions & 4 deletions internal/k8s/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,29 @@ func ConvertConfig(cfg *config.ConfigData) *k8sconfig.ConfigData {

// WrapK8sExecutor makes an mcp-kubernetes CommandExecutor
// compatible with the aks-mcp tools.CommandExecutor interface.
func WrapK8sExecutor(k8sExecutor k8stools.CommandExecutor) tools.CommandExecutor {
return &executorAdapter{k8sExecutor: k8sExecutor}
func WrapK8sExecutor(k8sExecutor k8stools.CommandExecutor, tokenAuthOnly bool) tools.CommandExecutor {
return &executorAdapter{
k8sExecutor: k8sExecutor,
runCommandExecutor: NewRunCommandExecutor(),
tokenAuthOnly: tokenAuthOnly,
}
}

// executorAdapter bridges aks-mcp execution to mcp-kubernetes.
// Unexported; behavior is defined by the wrapped executor.
type executorAdapter struct {
k8sExecutor k8stools.CommandExecutor
k8sExecutor k8stools.CommandExecutor
runCommandExecutor *RunCommandExecutor
tokenAuthOnly bool
}

// Execute adapts aks-mcp execution by converting its config
// and delegating to the wrapped mcp-kubernetes executor.
// and delegating to the wrapped mcp-kubernetes executor or RunCommand executor.
func (a *executorAdapter) Execute(ctx context.Context, params map[string]interface{}, cfg *config.ConfigData) (string, error) {
if a.tokenAuthOnly {
k8sCfg := ConvertConfig(cfg)
return a.runCommandExecutor.Execute(ctx, params, k8sCfg)
}
k8sCfg := ConvertConfig(cfg)
return a.k8sExecutor.Execute(ctx, params, k8sCfg)
}
Loading
Loading