mcp-proxy-adapter 6.0.0__py3-none-any.whl → 6.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_proxy_adapter/api/app.py +174 -80
- mcp_proxy_adapter/api/handlers.py +16 -5
- mcp_proxy_adapter/api/middleware/__init__.py +7 -2
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/factory.py +36 -12
- mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
- mcp_proxy_adapter/commands/__init__.py +7 -1
- mcp_proxy_adapter/commands/base.py +7 -4
- mcp_proxy_adapter/commands/builtin_commands.py +8 -2
- mcp_proxy_adapter/commands/command_registry.py +8 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +2 -2
- mcp_proxy_adapter/commands/token_management_command.py +1 -1
- mcp_proxy_adapter/config.py +81 -21
- mcp_proxy_adapter/core/app_factory.py +326 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/logging.py +8 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +9 -0
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +299 -47
- mcp_proxy_adapter/core/security_adapter.py +12 -15
- mcp_proxy_adapter/core/security_integration.py +277 -0
- mcp_proxy_adapter/core/server_adapter.py +345 -0
- mcp_proxy_adapter/core/server_engine.py +364 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/examples/README.md +230 -97
- mcp_proxy_adapter/examples/README_EN.md +258 -0
- mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
- mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
- mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
- mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
- mcp_proxy_adapter/examples/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/admin.key +52 -0
- mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
- mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
- mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/client.crt +32 -0
- mcp_proxy_adapter/examples/certs/client.key +52 -0
- mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
- mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_user.key +52 -0
- mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
- mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
- mcp_proxy_adapter/examples/certs/readonly.key +52 -0
- mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/server.crt +32 -0
- mcp_proxy_adapter/examples/certs/server.key +52 -0
- mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
- mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
- mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
- mcp_proxy_adapter/examples/certs/user.crt +32 -0
- mcp_proxy_adapter/examples/certs/user.key +52 -0
- mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
- mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
- mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
- mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
- mcp_proxy_adapter/examples/commands/__init__.py +1 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
- mcp_proxy_adapter/examples/debug_request_state.py +144 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
- mcp_proxy_adapter/examples/demo_client.py +341 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
- mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
- mcp_proxy_adapter/examples/full_application/main.py +138 -0
- mcp_proxy_adapter/examples/full_application/roles.json +21 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
- mcp_proxy_adapter/examples/generate_certificates.py +121 -0
- mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
- mcp_proxy_adapter/examples/roles.json +38 -0
- mcp_proxy_adapter/examples/run_example.py +81 -0
- mcp_proxy_adapter/examples/run_security_tests.py +326 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
- mcp_proxy_adapter/examples/security_test_client.py +743 -0
- mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
- mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
- mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
- mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
- mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
- mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
- mcp_proxy_adapter/examples/test_examples.py +344 -0
- mcp_proxy_adapter/examples/universal_client.py +628 -0
- mcp_proxy_adapter/main.py +21 -10
- mcp_proxy_adapter/utils/config_generator.py +639 -0
- mcp_proxy_adapter/version.py +2 -1
- mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
- mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
- mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
- mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
- mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
- mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
- mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
- mcp_proxy_adapter/api/middleware/security.py +0 -376
- mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
- mcp_proxy_adapter/examples/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
- mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
- mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -114
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
- mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
- mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
- mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
- mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
- mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
- mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
- mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
- mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
- mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
- mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
- mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/schemas/roles_schema.json +0 -162
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -270
- mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
- mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
"""
|
2
|
+
Client Security Module
|
3
|
+
|
4
|
+
This module provides client-side security integration for MCP Proxy Adapter,
|
5
|
+
using mcp_security_framework utilities for secure connections to servers.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
"""
|
10
|
+
|
11
|
+
import logging
|
12
|
+
import ssl
|
13
|
+
from typing import Dict, Any, Optional, List, Tuple
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
# Import framework utilities
|
17
|
+
try:
|
18
|
+
from mcp_security_framework.utils.crypto_utils import (
|
19
|
+
generate_api_key, create_jwt_token, verify_jwt_token
|
20
|
+
)
|
21
|
+
from mcp_security_framework.utils.cert_utils import (
|
22
|
+
parse_certificate, extract_roles_from_certificate,
|
23
|
+
validate_certificate_chain, validate_certificate_format
|
24
|
+
)
|
25
|
+
from mcp_security_framework.core.ssl_manager import SSLManager
|
26
|
+
from mcp_security_framework.schemas.config import SSLConfig, AuthConfig
|
27
|
+
from mcp_security_framework.schemas.models import AuthResult, ValidationResult
|
28
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
29
|
+
except ImportError:
|
30
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
31
|
+
SSLManager = None
|
32
|
+
SSLConfig = None
|
33
|
+
AuthConfig = None
|
34
|
+
AuthResult = None
|
35
|
+
ValidationResult = None
|
36
|
+
|
37
|
+
from mcp_proxy_adapter.core.logging import logger
|
38
|
+
|
39
|
+
|
40
|
+
class ClientSecurityManager:
|
41
|
+
"""
|
42
|
+
Client-side security manager for MCP Proxy Adapter.
|
43
|
+
|
44
|
+
Provides secure client connections using mcp_security_framework utilities.
|
45
|
+
Handles authentication, certificate management, and SSL/TLS for client connections.
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self, config: Dict[str, Any]):
|
49
|
+
"""
|
50
|
+
Initialize client security manager.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
config: Security configuration
|
54
|
+
"""
|
55
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
56
|
+
raise ImportError("mcp_security_framework is not available")
|
57
|
+
|
58
|
+
self.config = config
|
59
|
+
self.security_config = config.get("security", {})
|
60
|
+
|
61
|
+
# Initialize SSL manager if needed
|
62
|
+
self.ssl_manager = None
|
63
|
+
if self.security_config.get("ssl", {}).get("enabled", False):
|
64
|
+
ssl_config = self._create_ssl_config()
|
65
|
+
self.ssl_manager = SSLManager(ssl_config)
|
66
|
+
|
67
|
+
logger.info("Client security manager initialized")
|
68
|
+
|
69
|
+
def _create_ssl_config(self) -> SSLConfig:
|
70
|
+
"""Create SSL configuration for client connections."""
|
71
|
+
ssl_section = self.security_config.get("ssl", {})
|
72
|
+
|
73
|
+
return SSLConfig(
|
74
|
+
enabled=ssl_section.get("enabled", False),
|
75
|
+
cert_file=ssl_section.get("client_cert_file"),
|
76
|
+
key_file=ssl_section.get("client_key_file"),
|
77
|
+
ca_cert_file=ssl_section.get("ca_cert_file"),
|
78
|
+
verify_mode=ssl_section.get("verify_mode", "CERT_REQUIRED"),
|
79
|
+
min_tls_version=ssl_section.get("min_tls_version", "TLSv1.2"),
|
80
|
+
check_hostname=ssl_section.get("check_hostname", True),
|
81
|
+
check_expiry=ssl_section.get("check_expiry", True)
|
82
|
+
)
|
83
|
+
|
84
|
+
def create_client_ssl_context(self, server_hostname: Optional[str] = None) -> Optional[ssl.SSLContext]:
|
85
|
+
"""
|
86
|
+
Create SSL context for client connections.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
server_hostname: Server hostname for SNI
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
SSL context or None if SSL not enabled
|
93
|
+
"""
|
94
|
+
if not self.ssl_manager:
|
95
|
+
return None
|
96
|
+
|
97
|
+
try:
|
98
|
+
# Create client SSL context
|
99
|
+
context = self.ssl_manager.create_client_context()
|
100
|
+
|
101
|
+
if server_hostname:
|
102
|
+
# Set server hostname for SNI
|
103
|
+
context.check_hostname = True
|
104
|
+
context.verify_mode = ssl.CERT_REQUIRED
|
105
|
+
|
106
|
+
logger.info(f"Client SSL context created for {server_hostname or 'unknown server'}")
|
107
|
+
return context
|
108
|
+
|
109
|
+
except Exception as e:
|
110
|
+
logger.error(f"Failed to create client SSL context: {e}")
|
111
|
+
return None
|
112
|
+
|
113
|
+
def generate_client_api_key(self, user_id: str, prefix: str = "mcp_proxy") -> str:
|
114
|
+
"""
|
115
|
+
Generate API key for client authentication.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
user_id: User identifier
|
119
|
+
prefix: Key prefix
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Generated API key
|
123
|
+
"""
|
124
|
+
try:
|
125
|
+
api_key = generate_api_key(prefix=prefix)
|
126
|
+
logger.info(f"Generated API key for user: {user_id}")
|
127
|
+
return api_key
|
128
|
+
except Exception as e:
|
129
|
+
logger.error(f"Failed to generate API key: {e}")
|
130
|
+
raise
|
131
|
+
|
132
|
+
def create_client_jwt_token(self, user_id: str, roles: List[str],
|
133
|
+
secret: str, algorithm: str = "HS256",
|
134
|
+
expiry_hours: int = 24) -> str:
|
135
|
+
"""
|
136
|
+
Create JWT token for client authentication.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
user_id: User identifier
|
140
|
+
roles: User roles
|
141
|
+
secret: JWT secret
|
142
|
+
algorithm: JWT algorithm
|
143
|
+
expiry_hours: Token expiry in hours
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
JWT token
|
147
|
+
"""
|
148
|
+
try:
|
149
|
+
payload = {
|
150
|
+
"user_id": user_id,
|
151
|
+
"roles": roles,
|
152
|
+
"type": "client_proxy"
|
153
|
+
}
|
154
|
+
|
155
|
+
token = create_jwt_token(
|
156
|
+
payload=payload,
|
157
|
+
secret=secret,
|
158
|
+
algorithm=algorithm,
|
159
|
+
expiry_hours=expiry_hours
|
160
|
+
)
|
161
|
+
|
162
|
+
logger.info(f"Created JWT token for user: {user_id}")
|
163
|
+
return token
|
164
|
+
|
165
|
+
except Exception as e:
|
166
|
+
logger.error(f"Failed to create JWT token: {e}")
|
167
|
+
raise
|
168
|
+
|
169
|
+
def validate_server_certificate(self, cert_path: str, ca_cert_path: Optional[str] = None) -> bool:
|
170
|
+
"""
|
171
|
+
Validate server certificate before connection.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
cert_path: Path to server certificate
|
175
|
+
ca_cert_path: Path to CA certificate
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
True if certificate is valid
|
179
|
+
"""
|
180
|
+
try:
|
181
|
+
# Validate certificate format
|
182
|
+
if not validate_certificate_format(cert_path):
|
183
|
+
logger.error(f"Invalid certificate format: {cert_path}")
|
184
|
+
return False
|
185
|
+
|
186
|
+
# Validate certificate chain if CA provided
|
187
|
+
if ca_cert_path:
|
188
|
+
if not validate_certificate_chain(cert_path, ca_cert_path):
|
189
|
+
logger.error(f"Invalid certificate chain: {cert_path}")
|
190
|
+
return False
|
191
|
+
|
192
|
+
# Parse certificate and check basic properties
|
193
|
+
cert_info = parse_certificate(cert_path)
|
194
|
+
if not cert_info:
|
195
|
+
logger.error(f"Failed to parse certificate: {cert_path}")
|
196
|
+
return False
|
197
|
+
|
198
|
+
logger.info(f"Server certificate validated: {cert_path}")
|
199
|
+
return True
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
logger.error(f"Failed to validate server certificate: {e}")
|
203
|
+
return False
|
204
|
+
|
205
|
+
def extract_server_roles(self, cert_path: str) -> List[str]:
|
206
|
+
"""
|
207
|
+
Extract roles from server certificate.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
cert_path: Path to server certificate
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
List of roles extracted from certificate
|
214
|
+
"""
|
215
|
+
try:
|
216
|
+
roles = extract_roles_from_certificate(cert_path)
|
217
|
+
logger.info(f"Extracted roles from server certificate: {roles}")
|
218
|
+
return roles
|
219
|
+
except Exception as e:
|
220
|
+
logger.error(f"Failed to extract roles from certificate: {e}")
|
221
|
+
return []
|
222
|
+
|
223
|
+
def get_client_auth_headers(self, auth_method: str = "api_key", **kwargs) -> Dict[str, str]:
|
224
|
+
"""
|
225
|
+
Get authentication headers for client requests.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
auth_method: Authentication method (api_key, jwt, certificate)
|
229
|
+
**kwargs: Additional parameters
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
Dictionary of authentication headers
|
233
|
+
"""
|
234
|
+
headers = {}
|
235
|
+
|
236
|
+
try:
|
237
|
+
if auth_method == "api_key":
|
238
|
+
api_key = kwargs.get("api_key")
|
239
|
+
if api_key:
|
240
|
+
headers["X-API-Key"] = api_key
|
241
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
242
|
+
|
243
|
+
elif auth_method == "jwt":
|
244
|
+
token = kwargs.get("token")
|
245
|
+
if token:
|
246
|
+
headers["Authorization"] = f"Bearer {token}"
|
247
|
+
|
248
|
+
elif auth_method == "certificate":
|
249
|
+
# Certificate authentication is handled at SSL level
|
250
|
+
headers["X-Auth-Method"] = "certificate"
|
251
|
+
|
252
|
+
# Add common proxy headers
|
253
|
+
headers["X-Proxy-Type"] = "mcp_proxy_adapter"
|
254
|
+
headers["X-Client-Type"] = "proxy_client"
|
255
|
+
|
256
|
+
logger.debug(f"Created auth headers for method: {auth_method}")
|
257
|
+
return headers
|
258
|
+
|
259
|
+
except Exception as e:
|
260
|
+
logger.error(f"Failed to create auth headers: {e}")
|
261
|
+
return {}
|
262
|
+
|
263
|
+
def prepare_client_connection(self, server_config: Dict[str, Any]) -> Tuple[Optional[ssl.SSLContext], Dict[str, str]]:
|
264
|
+
"""
|
265
|
+
Prepare secure client connection to server.
|
266
|
+
|
267
|
+
Args:
|
268
|
+
server_config: Server connection configuration
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
Tuple of (SSL context, auth headers)
|
272
|
+
"""
|
273
|
+
ssl_context = None
|
274
|
+
auth_headers = {}
|
275
|
+
|
276
|
+
try:
|
277
|
+
# Create SSL context if needed
|
278
|
+
if server_config.get("ssl", False):
|
279
|
+
server_hostname = server_config.get("hostname")
|
280
|
+
ssl_context = self.create_client_ssl_context(server_hostname)
|
281
|
+
|
282
|
+
# Create authentication headers
|
283
|
+
auth_method = server_config.get("auth_method", "api_key")
|
284
|
+
auth_headers = self.get_client_auth_headers(
|
285
|
+
auth_method=auth_method,
|
286
|
+
api_key=server_config.get("api_key"),
|
287
|
+
token=server_config.get("token")
|
288
|
+
)
|
289
|
+
|
290
|
+
logger.info(f"Prepared client connection for {server_config.get('hostname', 'unknown')}")
|
291
|
+
return ssl_context, auth_headers
|
292
|
+
|
293
|
+
except Exception as e:
|
294
|
+
logger.error(f"Failed to prepare client connection: {e}")
|
295
|
+
return None, {}
|
296
|
+
|
297
|
+
def validate_server_response(self, response_headers: Dict[str, str]) -> bool:
|
298
|
+
"""
|
299
|
+
Validate server response for security compliance.
|
300
|
+
|
301
|
+
Args:
|
302
|
+
response_headers: Server response headers
|
303
|
+
|
304
|
+
Returns:
|
305
|
+
True if response is valid
|
306
|
+
"""
|
307
|
+
try:
|
308
|
+
# Check for required security headers
|
309
|
+
required_headers = ["Content-Type"]
|
310
|
+
for header in required_headers:
|
311
|
+
if header not in response_headers:
|
312
|
+
logger.warning(f"Missing required header: {header}")
|
313
|
+
|
314
|
+
# Check for security headers
|
315
|
+
security_headers = ["X-Frame-Options", "X-Content-Type-Options"]
|
316
|
+
for header in security_headers:
|
317
|
+
if header in response_headers:
|
318
|
+
logger.debug(f"Found security header: {header}")
|
319
|
+
|
320
|
+
# Validate content type
|
321
|
+
content_type = response_headers.get("Content-Type", "")
|
322
|
+
if "application/json" not in content_type:
|
323
|
+
logger.warning(f"Unexpected content type: {content_type}")
|
324
|
+
|
325
|
+
return True
|
326
|
+
|
327
|
+
except Exception as e:
|
328
|
+
logger.error(f"Failed to validate server response: {e}")
|
329
|
+
return False
|
330
|
+
|
331
|
+
def get_client_certificate_info(self) -> Optional[Dict[str, Any]]:
|
332
|
+
"""
|
333
|
+
Get information about client certificate.
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
Certificate information or None
|
337
|
+
"""
|
338
|
+
try:
|
339
|
+
ssl_config = self.security_config.get("ssl", {})
|
340
|
+
cert_path = ssl_config.get("client_cert_file")
|
341
|
+
|
342
|
+
if not cert_path or not Path(cert_path).exists():
|
343
|
+
return None
|
344
|
+
|
345
|
+
cert_info = parse_certificate(cert_path)
|
346
|
+
if cert_info:
|
347
|
+
roles = extract_roles_from_certificate(cert_path)
|
348
|
+
cert_info["roles"] = roles
|
349
|
+
return cert_info
|
350
|
+
|
351
|
+
return None
|
352
|
+
|
353
|
+
except Exception as e:
|
354
|
+
logger.error(f"Failed to get client certificate info: {e}")
|
355
|
+
return None
|
356
|
+
|
357
|
+
def is_ssl_enabled(self) -> bool:
|
358
|
+
"""Check if SSL is enabled for client connections."""
|
359
|
+
return self.security_config.get("ssl", {}).get("enabled", False)
|
360
|
+
|
361
|
+
def get_supported_auth_methods(self) -> List[str]:
|
362
|
+
"""Get list of supported authentication methods."""
|
363
|
+
return ["api_key", "jwt", "certificate"]
|
364
|
+
|
365
|
+
|
366
|
+
# Factory function for easy integration
|
367
|
+
def create_client_security_manager(config: Dict[str, Any]) -> Optional[ClientSecurityManager]:
|
368
|
+
"""
|
369
|
+
Create client security manager instance.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
config: Configuration dictionary
|
373
|
+
|
374
|
+
Returns:
|
375
|
+
ClientSecurityManager instance or None if framework not available
|
376
|
+
"""
|
377
|
+
try:
|
378
|
+
return ClientSecurityManager(config)
|
379
|
+
except ImportError:
|
380
|
+
logger.warning("mcp_security_framework not available, client security disabled")
|
381
|
+
return None
|
382
|
+
except Exception as e:
|
383
|
+
logger.error(f"Failed to create client security manager: {e}")
|
384
|
+
return None
|
@@ -230,17 +230,22 @@ def setup_logging(
|
|
230
230
|
return logger
|
231
231
|
|
232
232
|
|
233
|
-
def _parse_file_size(size_str
|
233
|
+
def _parse_file_size(size_str) -> int:
|
234
234
|
"""
|
235
235
|
Parse file size string to bytes.
|
236
236
|
|
237
237
|
Args:
|
238
|
-
size_str: Size string (e.g., "10MB", "1GB", "100KB")
|
238
|
+
size_str: Size string (e.g., "10MB", "1GB", "100KB") or int
|
239
239
|
|
240
240
|
Returns:
|
241
241
|
Size in bytes
|
242
242
|
"""
|
243
|
-
|
243
|
+
# If it's already an int, return it
|
244
|
+
if isinstance(size_str, int):
|
245
|
+
return size_str
|
246
|
+
|
247
|
+
# Convert to string and parse
|
248
|
+
size_str = str(size_str).upper()
|
244
249
|
if size_str.endswith("KB"):
|
245
250
|
return int(size_str[:-2]) * 1024
|
246
251
|
elif size_str.endswith("MB"):
|
@@ -0,0 +1,156 @@
|
|
1
|
+
"""
|
2
|
+
Custom ASGI application for mTLS support.
|
3
|
+
|
4
|
+
This module provides a custom ASGI application that properly handles
|
5
|
+
client certificates in mTLS connections.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import ssl
|
9
|
+
import logging
|
10
|
+
from typing import Dict, Any, Optional
|
11
|
+
from starlette.applications import Starlette
|
12
|
+
from starlette.requests import Request
|
13
|
+
from starlette.responses import Response
|
14
|
+
from starlette.types import ASGIApp, Receive, Send, Scope
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class MTLSASGIApp:
|
20
|
+
"""
|
21
|
+
Custom ASGI application that properly handles mTLS client certificates.
|
22
|
+
|
23
|
+
This wrapper ensures that client certificates are properly extracted
|
24
|
+
and made available to the FastAPI application.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, app: ASGIApp, ssl_config: Dict[str, Any]):
|
28
|
+
"""
|
29
|
+
Initialize MTLS ASGI application.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
app: The underlying ASGI application (FastAPI)
|
33
|
+
ssl_config: SSL configuration for mTLS
|
34
|
+
"""
|
35
|
+
self.app = app
|
36
|
+
self.ssl_config = ssl_config
|
37
|
+
self.verify_client = ssl_config.get("verify_client", False)
|
38
|
+
self.client_cert_required = ssl_config.get("client_cert_required", False)
|
39
|
+
|
40
|
+
logger.info(f"MTLS ASGI app initialized: verify_client={self.verify_client}, "
|
41
|
+
f"client_cert_required={self.client_cert_required}")
|
42
|
+
|
43
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
44
|
+
"""
|
45
|
+
Handle ASGI request with mTLS support.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
scope: ASGI scope
|
49
|
+
receive: ASGI receive callable
|
50
|
+
send: ASGI send callable
|
51
|
+
"""
|
52
|
+
try:
|
53
|
+
# Extract client certificate from SSL context
|
54
|
+
if scope["type"] == "http" and "ssl" in scope:
|
55
|
+
client_cert = self._extract_client_certificate(scope)
|
56
|
+
if client_cert:
|
57
|
+
# Store certificate in scope for middleware access
|
58
|
+
scope["client_certificate"] = client_cert
|
59
|
+
logger.debug(f"Client certificate extracted: {client_cert.get('subject', {})}")
|
60
|
+
elif self.client_cert_required:
|
61
|
+
logger.warning("Client certificate required but not provided")
|
62
|
+
# Return 401 Unauthorized
|
63
|
+
await self._send_unauthorized_response(send)
|
64
|
+
return
|
65
|
+
|
66
|
+
# Call the underlying application
|
67
|
+
await self.app(scope, receive, send)
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
logger.error(f"Error in MTLS ASGI app: {e}")
|
71
|
+
await self._send_error_response(send, str(e))
|
72
|
+
|
73
|
+
def _extract_client_certificate(self, scope: Scope) -> Optional[Dict[str, Any]]:
|
74
|
+
"""
|
75
|
+
Extract client certificate from SSL context.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
scope: ASGI scope
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
Client certificate data or None
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
ssl_context = scope.get("ssl")
|
85
|
+
if not ssl_context:
|
86
|
+
return None
|
87
|
+
|
88
|
+
# Get peer certificate
|
89
|
+
cert = ssl_context.getpeercert()
|
90
|
+
if cert:
|
91
|
+
return cert
|
92
|
+
|
93
|
+
return None
|
94
|
+
|
95
|
+
except Exception as e:
|
96
|
+
logger.error(f"Failed to extract client certificate: {e}")
|
97
|
+
return None
|
98
|
+
|
99
|
+
async def _send_unauthorized_response(self, send: Send) -> None:
|
100
|
+
"""
|
101
|
+
Send 401 Unauthorized response.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
send: ASGI send callable
|
105
|
+
"""
|
106
|
+
response = {
|
107
|
+
"type": "http.response.start",
|
108
|
+
"status": 401,
|
109
|
+
"headers": [
|
110
|
+
(b"content-type", b"application/json"),
|
111
|
+
(b"content-length", b"163"),
|
112
|
+
],
|
113
|
+
}
|
114
|
+
await send(response)
|
115
|
+
|
116
|
+
body = b'{"jsonrpc": "2.0", "error": {"code": -32001, "message": "Unauthorized: Client certificate required"}, "id": null}'
|
117
|
+
await send({"type": "http.response.body", "body": body})
|
118
|
+
|
119
|
+
async def _send_error_response(self, send: Send, error_message: str) -> None:
|
120
|
+
"""
|
121
|
+
Send error response.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
send: ASGI send callable
|
125
|
+
error_message: Error message
|
126
|
+
"""
|
127
|
+
response = {
|
128
|
+
"type": "http.response.start",
|
129
|
+
"status": 500,
|
130
|
+
"headers": [
|
131
|
+
(b"content-type", b"application/json"),
|
132
|
+
],
|
133
|
+
}
|
134
|
+
await send(response)
|
135
|
+
|
136
|
+
body = f'{{"jsonrpc": "2.0", "error": {{"code": -32603, "message": "Internal error: {error_message}"}}, "id": null}}'.encode()
|
137
|
+
await send({"type": "http.response.body", "body": body})
|
138
|
+
|
139
|
+
|
140
|
+
def create_mtls_asgi_app(app: ASGIApp, ssl_config: Dict[str, Any]) -> ASGIApp:
|
141
|
+
"""
|
142
|
+
Create MTLS-enabled ASGI application.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
app: The underlying ASGI application (FastAPI)
|
146
|
+
ssl_config: SSL configuration for mTLS
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
MTLS-enabled ASGI application
|
150
|
+
"""
|
151
|
+
if ssl_config.get("mode") == "mtls" or ssl_config.get("verify_client", False):
|
152
|
+
logger.info("Creating MTLS-enabled ASGI application")
|
153
|
+
return MTLSASGIApp(app, ssl_config)
|
154
|
+
else:
|
155
|
+
logger.info("Creating standard ASGI application (no mTLS)")
|
156
|
+
return app
|