mirror of
https://github.com/alibaba/higress.git
synced 2026-02-27 22:20:57 +08:00
430 lines
12 KiB
Go
430 lines
12 KiB
Go
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestApiKeyAuthentication tests API key authentication forwarding
|
|
func TestApiKeyAuthentication(t *testing.T) {
|
|
server := NewMcpProxyServer("auth-test")
|
|
|
|
// Configure security scheme
|
|
scheme := SecurityScheme{
|
|
ID: "ApiKeyAuth",
|
|
Type: "apiKey",
|
|
In: "header",
|
|
Name: "X-API-Key",
|
|
DefaultCredential: "default-api-key",
|
|
}
|
|
|
|
server.AddSecurityScheme(scheme)
|
|
|
|
// Set server fields directly
|
|
server.SetMcpServerURL("http://secure-backend.example.com/mcp")
|
|
server.SetTimeout(5000)
|
|
|
|
// Create tool with client-to-gateway and gateway-to-backend security
|
|
toolConfig := McpProxyToolConfig{
|
|
Name: "secure_tool",
|
|
Description: "Tool requiring authentication",
|
|
Security: SecurityRequirement{
|
|
ID: "ApiKeyAuth", // Client-to-gateway authentication
|
|
Passthrough: true, // Extract client credential for backend use
|
|
},
|
|
Args: []ToolArg{
|
|
{
|
|
Name: "data",
|
|
Description: "Data parameter",
|
|
Type: "string",
|
|
Required: true,
|
|
},
|
|
},
|
|
OutputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"result": map[string]any{
|
|
"type": "string",
|
|
"description": "The result of the operation",
|
|
},
|
|
},
|
|
},
|
|
RequestTemplate: RequestTemplate{
|
|
Security: SecurityRequirement{
|
|
ID: "ApiKeyAuth", // Gateway-to-backend authentication (same scheme for simplicity)
|
|
},
|
|
},
|
|
}
|
|
|
|
err := server.AddProxyTool(toolConfig)
|
|
require.NoError(t, err)
|
|
|
|
tool, exists := server.GetMCPTools()["secure_tool"]
|
|
require.True(t, exists)
|
|
|
|
params := map[string]interface{}{
|
|
"data": "test data",
|
|
}
|
|
paramsBytes, err := json.Marshal(params)
|
|
require.NoError(t, err)
|
|
|
|
toolInstance := tool.Create(paramsBytes)
|
|
require.NotNil(t, toolInstance)
|
|
|
|
// Authentication is now handled automatically during tool calls
|
|
// The actual authentication flow is tested in integration tests
|
|
}
|
|
|
|
// TestBearerAuthentication tests Bearer token authentication
|
|
func TestBearerAuthentication(t *testing.T) {
|
|
server := NewMcpProxyServer("bearer-auth-test")
|
|
|
|
// Configure Bearer security scheme
|
|
scheme := SecurityScheme{
|
|
ID: "BearerAuth",
|
|
Type: "http",
|
|
Scheme: "bearer",
|
|
}
|
|
|
|
server.AddSecurityScheme(scheme)
|
|
|
|
// Set server fields directly
|
|
server.SetMcpServerURL("https://secure-backend.example.com/mcp")
|
|
server.SetTimeout(8000)
|
|
|
|
// Create tool with Bearer authentication
|
|
// Create tool using only gateway-to-backend authentication (no client auth required)
|
|
toolConfig := McpProxyToolConfig{
|
|
Name: "bearer_tool",
|
|
Description: "Tool with Bearer authentication to backend only",
|
|
Args: []ToolArg{
|
|
{
|
|
Name: "query",
|
|
Description: "Query parameter",
|
|
Type: "string",
|
|
Required: true,
|
|
},
|
|
},
|
|
RequestTemplate: RequestTemplate{
|
|
Security: SecurityRequirement{
|
|
ID: "BearerAuth", // Only gateway-to-backend authentication
|
|
},
|
|
},
|
|
}
|
|
|
|
err := server.AddProxyTool(toolConfig)
|
|
require.NoError(t, err)
|
|
|
|
tool, exists := server.GetMCPTools()["bearer_tool"]
|
|
require.True(t, exists)
|
|
|
|
params := map[string]interface{}{
|
|
"query": "test query",
|
|
}
|
|
paramsBytes, err := json.Marshal(params)
|
|
require.NoError(t, err)
|
|
|
|
toolInstance := tool.Create(paramsBytes)
|
|
require.NotNil(t, toolInstance)
|
|
|
|
// Authentication is now handled automatically during tool calls
|
|
// The actual authentication flow is tested in integration tests
|
|
|
|
// Test backward compatibility: this tool uses RequestTemplate.Security (legacy way)
|
|
// which should still work
|
|
}
|
|
|
|
// TestBasicAuthentication tests Basic authentication
|
|
func TestBasicAuthentication(t *testing.T) {
|
|
server := NewMcpProxyServer("basic-auth-test")
|
|
|
|
// Configure Basic security scheme
|
|
scheme := SecurityScheme{
|
|
ID: "BasicAuth",
|
|
Type: "http",
|
|
Scheme: "basic",
|
|
}
|
|
|
|
server.AddSecurityScheme(scheme)
|
|
|
|
// Test tool call with Basic authentication
|
|
toolConfig := McpProxyToolConfig{
|
|
Name: "basic_tool",
|
|
Description: "Tool with Basic authentication",
|
|
Args: []ToolArg{
|
|
{
|
|
Name: "resource",
|
|
Description: "Resource identifier",
|
|
Type: "string",
|
|
Required: true,
|
|
},
|
|
},
|
|
RequestTemplate: RequestTemplate{
|
|
Security: SecurityRequirement{
|
|
ID: "BasicAuth",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := server.AddProxyTool(toolConfig)
|
|
require.NoError(t, err)
|
|
|
|
tool, exists := server.GetMCPTools()["basic_tool"]
|
|
require.True(t, exists)
|
|
|
|
params := map[string]interface{}{
|
|
"resource": "test-resource",
|
|
}
|
|
paramsBytes, err := json.Marshal(params)
|
|
require.NoError(t, err)
|
|
|
|
toolInstance := tool.Create(paramsBytes)
|
|
require.NotNil(t, toolInstance)
|
|
|
|
// Authentication is now handled automatically during tool calls
|
|
// The actual authentication flow is tested in integration tests
|
|
|
|
// Test OutputSchema functionality (only for tools that have it configured)
|
|
if toolWithOutputSchema, ok := tool.(ToolWithOutputSchema); ok {
|
|
outputSchema := toolWithOutputSchema.OutputSchema()
|
|
if outputSchema != nil {
|
|
// Only validate if outputSchema is configured
|
|
assert.Equal(t, "object", outputSchema["type"])
|
|
properties, hasProperties := outputSchema["properties"].(map[string]any)
|
|
require.True(t, hasProperties)
|
|
resultSchema, hasResult := properties["result"].(map[string]any)
|
|
require.True(t, hasResult)
|
|
assert.Equal(t, "string", resultSchema["type"])
|
|
assert.Equal(t, "The result of the operation", resultSchema["description"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMultipleSecuritySchemes tests multiple security schemes in one server
|
|
func TestMultipleSecuritySchemes(t *testing.T) {
|
|
server := NewMcpProxyServer("multi-auth-test")
|
|
|
|
// Add multiple security schemes
|
|
schemes := []SecurityScheme{
|
|
{
|
|
ID: "ApiKeyAuth",
|
|
Type: "apiKey",
|
|
In: "header",
|
|
Name: "X-API-Key",
|
|
},
|
|
{
|
|
ID: "BearerAuth",
|
|
Type: "http",
|
|
Scheme: "bearer",
|
|
},
|
|
}
|
|
|
|
for _, scheme := range schemes {
|
|
server.AddSecurityScheme(scheme)
|
|
}
|
|
|
|
// Test that both schemes are available
|
|
for _, scheme := range schemes {
|
|
retrievedScheme, exists := server.GetSecurityScheme(scheme.ID)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, scheme.ID, retrievedScheme.ID)
|
|
assert.Equal(t, scheme.Type, retrievedScheme.Type)
|
|
}
|
|
}
|
|
|
|
// ProxyAuthContext, RequestTemplate, SecurityConfig and authentication methods
|
|
// are now implemented in proxy_server.go
|
|
|
|
// TestToolsListAuthentication tests authentication configuration for tools/list requests
|
|
func TestToolsListAuthentication(t *testing.T) {
|
|
server := NewMcpProxyServer("test-server")
|
|
|
|
// Add a security scheme for global authentication
|
|
scheme := SecurityScheme{
|
|
ID: "GlobalAuth",
|
|
Type: "apiKey",
|
|
In: "header",
|
|
Name: "X-API-Key",
|
|
DefaultCredential: "default-global-key",
|
|
}
|
|
server.AddSecurityScheme(scheme)
|
|
|
|
// Test that we can retrieve the security scheme
|
|
retrievedScheme, exists := server.GetSecurityScheme("GlobalAuth")
|
|
assert.True(t, exists)
|
|
assert.Equal(t, "GlobalAuth", retrievedScheme.ID)
|
|
assert.Equal(t, "apiKey", retrievedScheme.Type)
|
|
assert.Equal(t, "header", retrievedScheme.In)
|
|
assert.Equal(t, "X-API-Key", retrievedScheme.Name)
|
|
|
|
// Test setting default security directly on server
|
|
defaultDownstreamSecurity := SecurityRequirement{
|
|
ID: "GlobalAuth",
|
|
Passthrough: true,
|
|
}
|
|
defaultUpstreamSecurity := SecurityRequirement{
|
|
ID: "GlobalAuth",
|
|
}
|
|
|
|
server.SetDefaultDownstreamSecurity(defaultDownstreamSecurity)
|
|
server.SetDefaultUpstreamSecurity(defaultUpstreamSecurity)
|
|
|
|
// Verify default security settings
|
|
retrievedDownstream := server.GetDefaultDownstreamSecurity()
|
|
assert.Equal(t, "GlobalAuth", retrievedDownstream.ID)
|
|
assert.True(t, retrievedDownstream.Passthrough)
|
|
|
|
retrievedUpstream := server.GetDefaultUpstreamSecurity()
|
|
assert.Equal(t, "GlobalAuth", retrievedUpstream.ID)
|
|
|
|
t.Logf("Tools/list authentication configuration test completed successfully")
|
|
}
|
|
|
|
// TestDefaultSecurityFallback tests the fallback mechanism from tool-level to default security
|
|
func TestDefaultSecurityFallback(t *testing.T) {
|
|
server := NewMcpProxyServer("test-server")
|
|
|
|
// Add security schemes
|
|
defaultScheme := SecurityScheme{
|
|
ID: "DefaultAuth",
|
|
Type: "apiKey",
|
|
In: "header",
|
|
Name: "X-Default-Key",
|
|
DefaultCredential: "default-key",
|
|
}
|
|
toolScheme := SecurityScheme{
|
|
ID: "ToolAuth",
|
|
Type: "apiKey",
|
|
In: "header",
|
|
Name: "X-Tool-Key",
|
|
DefaultCredential: "tool-key",
|
|
}
|
|
server.AddSecurityScheme(defaultScheme)
|
|
server.AddSecurityScheme(toolScheme)
|
|
|
|
// Test tool configuration with tool-level security (should use tool-level, not default)
|
|
toolConfigWithSecurity := McpProxyToolConfig{
|
|
Name: "secure_tool",
|
|
Description: "Tool with its own security",
|
|
Security: SecurityRequirement{
|
|
ID: "ToolAuth",
|
|
Passthrough: true,
|
|
},
|
|
RequestTemplate: RequestTemplate{
|
|
Security: SecurityRequirement{
|
|
ID: "ToolAuth",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test tool configuration without tool-level security (should fallback to default)
|
|
toolConfigWithoutSecurity := McpProxyToolConfig{
|
|
Name: "fallback_tool",
|
|
Description: "Tool that falls back to default security",
|
|
// No Security field configured, should use default
|
|
RequestTemplate: RequestTemplate{
|
|
// No Security field configured, should use default
|
|
},
|
|
}
|
|
|
|
// Set default security directly on server
|
|
server.SetDefaultDownstreamSecurity(SecurityRequirement{
|
|
ID: "DefaultAuth",
|
|
Passthrough: false,
|
|
})
|
|
server.SetDefaultUpstreamSecurity(SecurityRequirement{
|
|
ID: "DefaultAuth",
|
|
})
|
|
|
|
// Set server configuration directly
|
|
server.SetMcpServerURL("http://backend.example.com")
|
|
server.SetTimeout(5000)
|
|
|
|
// Add tools to server
|
|
err := server.AddProxyTool(toolConfigWithSecurity)
|
|
assert.NoError(t, err)
|
|
err = server.AddProxyTool(toolConfigWithoutSecurity)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify tools were added
|
|
tools := server.GetMCPTools()
|
|
assert.Contains(t, tools, "secure_tool")
|
|
assert.Contains(t, tools, "fallback_tool")
|
|
|
|
t.Logf("Default security fallback test completed successfully")
|
|
}
|
|
|
|
// TestURLModificationInAuthentication tests that authentication can modify the URL (e.g., adding query parameters)
|
|
func TestURLModificationInAuthentication(t *testing.T) {
|
|
server := NewMcpProxyServer("test-server")
|
|
|
|
// Add a security scheme that adds parameters to query (apiKey in query)
|
|
scheme := SecurityScheme{
|
|
ID: "QueryApiKey",
|
|
Type: "apiKey",
|
|
In: "query",
|
|
Name: "api_key",
|
|
DefaultCredential: "test-key-123",
|
|
}
|
|
server.AddSecurityScheme(scheme)
|
|
|
|
// Verify the security scheme was added correctly
|
|
retrievedScheme, exists := server.GetSecurityScheme("QueryApiKey")
|
|
assert.True(t, exists)
|
|
assert.Equal(t, "apiKey", retrievedScheme.Type)
|
|
assert.Equal(t, "query", retrievedScheme.In)
|
|
assert.Equal(t, "api_key", retrievedScheme.Name)
|
|
|
|
t.Logf("URL modification authentication configuration test completed successfully")
|
|
}
|
|
|
|
// TestProxyServerFields tests the server-level field setting and getting
|
|
func TestProxyServerFields(t *testing.T) {
|
|
server := NewMcpProxyServer("test-server")
|
|
|
|
// Test mcpServerURL
|
|
testURL := "http://mcp.example.com:8080/mcp"
|
|
server.SetMcpServerURL(testURL)
|
|
assert.Equal(t, testURL, server.GetMcpServerURL())
|
|
|
|
// Test timeout
|
|
testTimeout := 10000
|
|
server.SetTimeout(testTimeout)
|
|
assert.Equal(t, testTimeout, server.GetTimeout())
|
|
|
|
// Test default security settings
|
|
downstreamSec := SecurityRequirement{
|
|
ID: "test-downstream",
|
|
Passthrough: true,
|
|
}
|
|
upstreamSec := SecurityRequirement{
|
|
ID: "test-upstream",
|
|
}
|
|
|
|
server.SetDefaultDownstreamSecurity(downstreamSec)
|
|
server.SetDefaultUpstreamSecurity(upstreamSec)
|
|
|
|
assert.Equal(t, "test-downstream", server.GetDefaultDownstreamSecurity().ID)
|
|
assert.True(t, server.GetDefaultDownstreamSecurity().Passthrough)
|
|
assert.Equal(t, "test-upstream", server.GetDefaultUpstreamSecurity().ID)
|
|
|
|
t.Logf("Proxy server fields test completed successfully")
|
|
}
|