mcp-proxy-adapter 6.0.0__py3-none-any.whl → 6.0.1__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/__main__.py +27 -7
- mcp_proxy_adapter/api/app.py +209 -79
- mcp_proxy_adapter/api/handlers.py +16 -5
- mcp_proxy_adapter/api/middleware/__init__.py +14 -9
- 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/protocol_middleware.py +84 -18
- mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -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/health_command.py +1 -1
- 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 +234 -351
- mcp_proxy_adapter/commands/token_management_command.py +1 -1
- mcp_proxy_adapter/config.py +323 -40
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/certificate_utils.py +291 -73
- mcp_proxy_adapter/core/client.py +574 -0
- mcp_proxy_adapter/core/client_manager.py +284 -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 +169 -10
- 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 +286 -0
- mcp_proxy_adapter/core/server_adapter.py +282 -0
- mcp_proxy_adapter/core/server_engine.py +270 -0
- mcp_proxy_adapter/core/ssl_utils.py +13 -12
- mcp_proxy_adapter/core/transport_manager.py +5 -5
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/examples/__init__.py +13 -4
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
- mcp_proxy_adapter/examples/debug_request_state.py +112 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
- mcp_proxy_adapter/examples/demo_client.py +275 -0
- mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
- mcp_proxy_adapter/examples/generate_certificates.py +177 -0
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
- mcp_proxy_adapter/examples/run_example.py +59 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
- mcp_proxy_adapter/examples/run_security_tests.py +544 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
- mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/security_test_client.py +782 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
- mcp_proxy_adapter/examples/test_config.py +148 -0
- mcp_proxy_adapter/examples/test_config_generator.py +86 -0
- mcp_proxy_adapter/examples/test_examples.py +281 -0
- mcp_proxy_adapter/examples/universal_client.py +620 -0
- mcp_proxy_adapter/main.py +66 -148
- mcp_proxy_adapter/utils/config_generator.py +1008 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
- mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
- mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.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/README.md +0 -124
- 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.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -1,305 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
MTLS Middleware Adapter for backward compatibility.
|
3
|
-
|
4
|
-
This module provides an adapter that maintains the same interface as MTLSMiddleware
|
5
|
-
while using the new SecurityMiddleware internally.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import logging
|
9
|
-
from typing import Dict, List, Optional, Any, Callable, Awaitable
|
10
|
-
from cryptography import x509
|
11
|
-
from cryptography.hazmat.primitives import serialization
|
12
|
-
|
13
|
-
from fastapi import Request, Response
|
14
|
-
from starlette.responses import JSONResponse
|
15
|
-
|
16
|
-
from mcp_proxy_adapter.core.logging import logger
|
17
|
-
from mcp_proxy_adapter.core.auth_validator import AuthValidator
|
18
|
-
from mcp_proxy_adapter.core.role_utils import RoleUtils
|
19
|
-
from mcp_proxy_adapter.core.certificate_utils import CertificateUtils
|
20
|
-
from .base import BaseMiddleware
|
21
|
-
from .security import SecurityMiddleware
|
22
|
-
|
23
|
-
|
24
|
-
class MTLSMiddlewareAdapter(BaseMiddleware):
|
25
|
-
"""
|
26
|
-
Adapter for MTLSMiddleware that uses SecurityMiddleware internally.
|
27
|
-
|
28
|
-
Maintains the same interface as the original MTLSMiddleware for backward compatibility.
|
29
|
-
"""
|
30
|
-
|
31
|
-
def __init__(self, app, mtls_config: Dict[str, Any]):
|
32
|
-
"""
|
33
|
-
Initialize mTLS middleware adapter.
|
34
|
-
|
35
|
-
Args:
|
36
|
-
app: FastAPI application
|
37
|
-
mtls_config: mTLS configuration dictionary
|
38
|
-
"""
|
39
|
-
super().__init__(app)
|
40
|
-
|
41
|
-
# Store original configuration for backward compatibility
|
42
|
-
self.mtls_config = mtls_config
|
43
|
-
self.auth_validator = AuthValidator()
|
44
|
-
self.role_utils = RoleUtils()
|
45
|
-
self.certificate_utils = CertificateUtils()
|
46
|
-
|
47
|
-
# Extract configuration
|
48
|
-
self.enabled = mtls_config.get("enabled", False)
|
49
|
-
self.ca_cert_path = mtls_config.get("ca_cert")
|
50
|
-
self.verify_client = mtls_config.get("verify_client", True)
|
51
|
-
self.client_cert_required = mtls_config.get("client_cert_required", True)
|
52
|
-
self.allowed_roles = mtls_config.get("allowed_roles", [])
|
53
|
-
self.require_roles = mtls_config.get("require_roles", False)
|
54
|
-
|
55
|
-
# Create internal security middleware
|
56
|
-
self.security_middleware = self._create_security_middleware()
|
57
|
-
|
58
|
-
logger.info(f"MTLSMiddlewareAdapter initialized: enabled={self.enabled}, "
|
59
|
-
f"verify_client={self.verify_client}, "
|
60
|
-
f"client_cert_required={self.client_cert_required}")
|
61
|
-
|
62
|
-
def _create_security_middleware(self) -> SecurityMiddleware:
|
63
|
-
"""
|
64
|
-
Create internal SecurityMiddleware with MTLSMiddleware configuration.
|
65
|
-
|
66
|
-
Returns:
|
67
|
-
SecurityMiddleware instance
|
68
|
-
"""
|
69
|
-
# Convert MTLSMiddleware config to SecurityMiddleware config
|
70
|
-
security_config = {
|
71
|
-
"security": {
|
72
|
-
"enabled": self.enabled,
|
73
|
-
"auth": {
|
74
|
-
"enabled": False
|
75
|
-
},
|
76
|
-
"ssl": {
|
77
|
-
"enabled": self.enabled,
|
78
|
-
"cert_file": None,
|
79
|
-
"key_file": None,
|
80
|
-
"ca_cert": self.ca_cert_path,
|
81
|
-
"min_tls_version": "TLSv1.2",
|
82
|
-
"verify_client": self.verify_client,
|
83
|
-
"client_cert_required": self.client_cert_required
|
84
|
-
},
|
85
|
-
"permissions": {
|
86
|
-
"enabled": self.require_roles,
|
87
|
-
"roles_file": None,
|
88
|
-
"default_role": "user",
|
89
|
-
"deny_by_default": True
|
90
|
-
},
|
91
|
-
"rate_limit": {
|
92
|
-
"enabled": False
|
93
|
-
}
|
94
|
-
}
|
95
|
-
}
|
96
|
-
|
97
|
-
return SecurityMiddleware(self.app, security_config)
|
98
|
-
|
99
|
-
async def before_request(self, request: Request) -> None:
|
100
|
-
"""
|
101
|
-
Process request before calling the main handler.
|
102
|
-
|
103
|
-
Args:
|
104
|
-
request: FastAPI request object
|
105
|
-
"""
|
106
|
-
if not self.enabled:
|
107
|
-
return
|
108
|
-
|
109
|
-
try:
|
110
|
-
# Use SecurityMiddleware for validation
|
111
|
-
await self.security_middleware.before_request(request)
|
112
|
-
|
113
|
-
# Additional MTLS-specific processing
|
114
|
-
client_cert = self._extract_client_certificate(request)
|
115
|
-
if client_cert:
|
116
|
-
# Store certificate and roles in request state for backward compatibility
|
117
|
-
request.state.client_certificate = client_cert
|
118
|
-
request.state.client_roles = self._extract_roles_from_certificate(client_cert)
|
119
|
-
request.state.client_common_name = self._get_common_name(client_cert)
|
120
|
-
|
121
|
-
logger.debug(f"mTLS authentication successful for {request.state.client_common_name} "
|
122
|
-
f"with roles: {request.state.client_roles}")
|
123
|
-
|
124
|
-
except Exception as e:
|
125
|
-
logger.error(f"mTLS authentication failed: {e}")
|
126
|
-
raise
|
127
|
-
|
128
|
-
def _extract_client_certificate(self, request: Request) -> Optional[x509.Certificate]:
|
129
|
-
"""
|
130
|
-
Extract client certificate from request.
|
131
|
-
|
132
|
-
Args:
|
133
|
-
request: FastAPI request object
|
134
|
-
|
135
|
-
Returns:
|
136
|
-
Client certificate or None
|
137
|
-
"""
|
138
|
-
# Check for certificate in request headers
|
139
|
-
cert_header = request.headers.get("X-Client-Cert")
|
140
|
-
if cert_header:
|
141
|
-
try:
|
142
|
-
cert_data = cert_header.encode('utf-8')
|
143
|
-
return x509.load_pem_x509_certificate(cert_data)
|
144
|
-
except Exception as e:
|
145
|
-
logger.warning(f"Failed to parse certificate from header: {e}")
|
146
|
-
|
147
|
-
# Check for certificate in request state (from SSL context)
|
148
|
-
if hasattr(request, 'client') and hasattr(request.client, 'get_extra_info'):
|
149
|
-
cert = request.client.get_extra_info('ssl_object')
|
150
|
-
if cert:
|
151
|
-
return cert
|
152
|
-
|
153
|
-
return None
|
154
|
-
|
155
|
-
def _validate_client_certificate(self, cert: x509.Certificate) -> bool:
|
156
|
-
"""
|
157
|
-
Validate client certificate.
|
158
|
-
|
159
|
-
Args:
|
160
|
-
cert: Client certificate
|
161
|
-
|
162
|
-
Returns:
|
163
|
-
True if valid, False otherwise
|
164
|
-
"""
|
165
|
-
try:
|
166
|
-
# Basic validation
|
167
|
-
if not self.certificate_utils.is_certificate_valid(cert):
|
168
|
-
return False
|
169
|
-
|
170
|
-
# CA validation if CA cert is provided
|
171
|
-
if self.ca_cert_path:
|
172
|
-
return self.certificate_utils.validate_certificate_chain(cert, self.ca_cert_path)
|
173
|
-
|
174
|
-
return True
|
175
|
-
|
176
|
-
except Exception as e:
|
177
|
-
logger.error(f"Certificate validation failed: {e}")
|
178
|
-
return False
|
179
|
-
|
180
|
-
def _extract_roles_from_certificate(self, cert: x509.Certificate) -> List[str]:
|
181
|
-
"""
|
182
|
-
Extract roles from client certificate.
|
183
|
-
|
184
|
-
Args:
|
185
|
-
cert: Client certificate
|
186
|
-
|
187
|
-
Returns:
|
188
|
-
List of roles
|
189
|
-
"""
|
190
|
-
try:
|
191
|
-
# Extract from subject alternative names
|
192
|
-
roles = []
|
193
|
-
|
194
|
-
# Check for roles in SAN
|
195
|
-
san = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
196
|
-
if san:
|
197
|
-
for name in san.value:
|
198
|
-
if isinstance(name, x509.DNSName):
|
199
|
-
if name.value.startswith("role="):
|
200
|
-
role = name.value.split("=", 1)[1]
|
201
|
-
roles.append(role)
|
202
|
-
|
203
|
-
# Check for roles in subject
|
204
|
-
subject = cert.subject
|
205
|
-
for attr in subject:
|
206
|
-
if attr.oid.dotted_string == "2.5.4.3": # Common Name
|
207
|
-
if attr.value.startswith("role="):
|
208
|
-
role = attr.value.split("=", 1)[1]
|
209
|
-
roles.append(role)
|
210
|
-
|
211
|
-
# Check allowed roles if specified
|
212
|
-
if self.allowed_roles:
|
213
|
-
roles = [role for role in roles if role in self.allowed_roles]
|
214
|
-
|
215
|
-
return roles
|
216
|
-
|
217
|
-
except Exception as e:
|
218
|
-
logger.error(f"Failed to extract roles from certificate: {e}")
|
219
|
-
return []
|
220
|
-
|
221
|
-
def _get_common_name(self, cert: x509.Certificate) -> str:
|
222
|
-
"""
|
223
|
-
Get common name from certificate.
|
224
|
-
|
225
|
-
Args:
|
226
|
-
cert: Client certificate
|
227
|
-
|
228
|
-
Returns:
|
229
|
-
Common name
|
230
|
-
"""
|
231
|
-
try:
|
232
|
-
subject = cert.subject
|
233
|
-
for attr in subject:
|
234
|
-
if attr.oid.dotted_string == "2.5.4.3": # Common Name
|
235
|
-
return attr.value
|
236
|
-
return "unknown"
|
237
|
-
except Exception:
|
238
|
-
return "unknown"
|
239
|
-
|
240
|
-
def _validate_access(self, roles: List[str]) -> bool:
|
241
|
-
"""
|
242
|
-
Validate access based on roles.
|
243
|
-
|
244
|
-
Args:
|
245
|
-
roles: List of client roles
|
246
|
-
|
247
|
-
Returns:
|
248
|
-
True if access is allowed, False otherwise
|
249
|
-
"""
|
250
|
-
if not self.require_roles:
|
251
|
-
return True
|
252
|
-
|
253
|
-
if not roles:
|
254
|
-
return False
|
255
|
-
|
256
|
-
# Check if any role is allowed
|
257
|
-
return any(role in self.allowed_roles for role in roles)
|
258
|
-
|
259
|
-
def get_client_certificate(self, request: Request) -> Optional[x509.Certificate]:
|
260
|
-
"""
|
261
|
-
Get client certificate from request state (backward compatibility).
|
262
|
-
|
263
|
-
Args:
|
264
|
-
request: Request object
|
265
|
-
|
266
|
-
Returns:
|
267
|
-
Client certificate or None
|
268
|
-
"""
|
269
|
-
return getattr(request.state, 'client_certificate', None)
|
270
|
-
|
271
|
-
def get_client_roles(self, request: Request) -> List[str]:
|
272
|
-
"""
|
273
|
-
Get client roles from request state (backward compatibility).
|
274
|
-
|
275
|
-
Args:
|
276
|
-
request: Request object
|
277
|
-
|
278
|
-
Returns:
|
279
|
-
List of client roles
|
280
|
-
"""
|
281
|
-
return getattr(request.state, 'client_roles', [])
|
282
|
-
|
283
|
-
def get_client_common_name(self, request: Request) -> str:
|
284
|
-
"""
|
285
|
-
Get client common name from request state (backward compatibility).
|
286
|
-
|
287
|
-
Args:
|
288
|
-
request: Request object
|
289
|
-
|
290
|
-
Returns:
|
291
|
-
Client common name
|
292
|
-
"""
|
293
|
-
return getattr(request.state, 'client_common_name', 'unknown')
|
294
|
-
|
295
|
-
def is_mtls_authenticated(self, request: Request) -> bool:
|
296
|
-
"""
|
297
|
-
Check if request is mTLS authenticated (backward compatibility).
|
298
|
-
|
299
|
-
Args:
|
300
|
-
request: Request object
|
301
|
-
|
302
|
-
Returns:
|
303
|
-
True if mTLS authenticated, False otherwise
|
304
|
-
"""
|
305
|
-
return hasattr(request.state, 'client_certificate') and request.state.client_certificate is not None
|
@@ -1,296 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
mTLS Middleware
|
3
|
-
|
4
|
-
This module provides middleware for mutual TLS (mTLS) authentication.
|
5
|
-
Extracts and validates client certificates, extracts roles, and validates access.
|
6
|
-
|
7
|
-
Author: MCP Proxy Adapter Team
|
8
|
-
Version: 1.0.0
|
9
|
-
"""
|
10
|
-
|
11
|
-
import logging
|
12
|
-
from typing import Dict, List, Optional, Any
|
13
|
-
from cryptography import x509
|
14
|
-
from cryptography.hazmat.primitives import serialization
|
15
|
-
|
16
|
-
from fastapi import Request, Response
|
17
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
18
|
-
|
19
|
-
from ...core.auth_validator import AuthValidator
|
20
|
-
from ...core.role_utils import RoleUtils
|
21
|
-
from ...core.certificate_utils import CertificateUtils
|
22
|
-
from .base import BaseMiddleware
|
23
|
-
|
24
|
-
logger = logging.getLogger(__name__)
|
25
|
-
|
26
|
-
|
27
|
-
class MTLSMiddleware(BaseMiddleware):
|
28
|
-
"""
|
29
|
-
Middleware for mTLS authentication.
|
30
|
-
|
31
|
-
Extracts client certificates from requests, validates them against CA,
|
32
|
-
extracts roles, and validates access based on configuration.
|
33
|
-
"""
|
34
|
-
|
35
|
-
def __init__(self, app, mtls_config: Dict[str, Any]):
|
36
|
-
"""
|
37
|
-
Initialize mTLS middleware.
|
38
|
-
|
39
|
-
Args:
|
40
|
-
app: FastAPI application
|
41
|
-
mtls_config: mTLS configuration dictionary
|
42
|
-
"""
|
43
|
-
super().__init__(app)
|
44
|
-
self.mtls_config = mtls_config
|
45
|
-
self.auth_validator = AuthValidator()
|
46
|
-
self.role_utils = RoleUtils()
|
47
|
-
self.certificate_utils = CertificateUtils()
|
48
|
-
|
49
|
-
# Extract configuration
|
50
|
-
self.enabled = mtls_config.get("enabled", False)
|
51
|
-
self.ca_cert_path = mtls_config.get("ca_cert")
|
52
|
-
self.verify_client = mtls_config.get("verify_client", True)
|
53
|
-
self.client_cert_required = mtls_config.get("client_cert_required", True)
|
54
|
-
self.allowed_roles = mtls_config.get("allowed_roles", [])
|
55
|
-
self.require_roles = mtls_config.get("require_roles", False)
|
56
|
-
|
57
|
-
logger.info(f"mTLS middleware initialized: enabled={self.enabled}, "
|
58
|
-
f"verify_client={self.verify_client}, "
|
59
|
-
f"client_cert_required={self.client_cert_required}")
|
60
|
-
|
61
|
-
async def before_request(self, request: Request) -> None:
|
62
|
-
"""
|
63
|
-
Process request before calling the main handler.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
request: FastAPI request object
|
67
|
-
"""
|
68
|
-
if not self.enabled:
|
69
|
-
return
|
70
|
-
|
71
|
-
try:
|
72
|
-
# Extract client certificate
|
73
|
-
client_cert = self._extract_client_certificate(request)
|
74
|
-
|
75
|
-
if client_cert is None:
|
76
|
-
if self.client_cert_required:
|
77
|
-
raise ValueError("Client certificate is required but not provided")
|
78
|
-
return
|
79
|
-
|
80
|
-
# Validate client certificate
|
81
|
-
if not self._validate_client_certificate(client_cert):
|
82
|
-
raise ValueError("Client certificate validation failed")
|
83
|
-
|
84
|
-
# Extract roles from certificate
|
85
|
-
roles = self._extract_roles_from_certificate(client_cert)
|
86
|
-
|
87
|
-
# Validate access based on roles
|
88
|
-
if self.require_roles and not self._validate_access(roles):
|
89
|
-
raise ValueError("Access denied: insufficient roles")
|
90
|
-
|
91
|
-
# Store certificate and roles in request state
|
92
|
-
request.state.client_certificate = client_cert
|
93
|
-
request.state.client_roles = roles
|
94
|
-
request.state.client_common_name = self._get_common_name(client_cert)
|
95
|
-
|
96
|
-
logger.debug(f"mTLS authentication successful for {request.state.client_common_name} "
|
97
|
-
f"with roles: {roles}")
|
98
|
-
|
99
|
-
except Exception as e:
|
100
|
-
logger.error(f"mTLS authentication failed: {e}")
|
101
|
-
raise
|
102
|
-
|
103
|
-
def _extract_client_certificate(self, request: Request) -> Optional[x509.Certificate]:
|
104
|
-
"""
|
105
|
-
Extract client certificate from request.
|
106
|
-
|
107
|
-
Args:
|
108
|
-
request: FastAPI request object
|
109
|
-
|
110
|
-
Returns:
|
111
|
-
Client certificate object or None if not found
|
112
|
-
"""
|
113
|
-
try:
|
114
|
-
# Check if client certificate is available in SSL context
|
115
|
-
if hasattr(request, 'scope') and 'ssl' in request.scope:
|
116
|
-
ssl_context = request.scope['ssl']
|
117
|
-
if hasattr(ssl_context, 'getpeercert'):
|
118
|
-
cert_data = ssl_context.getpeercert(binary_form=True)
|
119
|
-
if cert_data:
|
120
|
-
return x509.load_der_x509_certificate(cert_data)
|
121
|
-
|
122
|
-
# Check for certificate in headers (for proxy scenarios)
|
123
|
-
cert_header = request.headers.get('ssl-client-cert')
|
124
|
-
if cert_header:
|
125
|
-
# Remove header prefix if present
|
126
|
-
if cert_header.startswith('-----BEGIN CERTIFICATE-----'):
|
127
|
-
cert_data = cert_header.encode('utf-8')
|
128
|
-
else:
|
129
|
-
# Assume it's base64 encoded
|
130
|
-
import base64
|
131
|
-
cert_data = base64.b64decode(cert_header)
|
132
|
-
|
133
|
-
return x509.load_pem_x509_certificate(cert_data)
|
134
|
-
|
135
|
-
return None
|
136
|
-
|
137
|
-
except Exception as e:
|
138
|
-
logger.error(f"Failed to extract client certificate: {e}")
|
139
|
-
return None
|
140
|
-
|
141
|
-
def _validate_client_certificate(self, cert: x509.Certificate) -> bool:
|
142
|
-
"""
|
143
|
-
Validate client certificate.
|
144
|
-
|
145
|
-
Args:
|
146
|
-
cert: Client certificate object
|
147
|
-
|
148
|
-
Returns:
|
149
|
-
True if certificate is valid, False otherwise
|
150
|
-
"""
|
151
|
-
try:
|
152
|
-
if not self.verify_client:
|
153
|
-
return True
|
154
|
-
|
155
|
-
# Convert certificate to PEM format for validation
|
156
|
-
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
|
157
|
-
|
158
|
-
# Use AuthValidator to validate certificate
|
159
|
-
result = self.auth_validator.validate_certificate_data(cert_pem)
|
160
|
-
if not result.is_valid:
|
161
|
-
logger.warning(f"Certificate validation failed: {result.error_message}")
|
162
|
-
return False
|
163
|
-
|
164
|
-
# Validate certificate chain if CA is provided
|
165
|
-
if self.ca_cert_path and self.ca_cert_path != "None":
|
166
|
-
# Create temporary file for certificate
|
167
|
-
import tempfile
|
168
|
-
import os
|
169
|
-
|
170
|
-
with tempfile.NamedTemporaryFile(mode='wb', suffix='.crt', delete=False) as f:
|
171
|
-
f.write(cert_pem)
|
172
|
-
temp_cert_path = f.name
|
173
|
-
|
174
|
-
try:
|
175
|
-
chain_valid = self.certificate_utils.validate_certificate_chain(
|
176
|
-
temp_cert_path, self.ca_cert_path
|
177
|
-
)
|
178
|
-
if not chain_valid:
|
179
|
-
logger.warning("Certificate chain validation failed")
|
180
|
-
return False
|
181
|
-
finally:
|
182
|
-
os.unlink(temp_cert_path)
|
183
|
-
|
184
|
-
return True
|
185
|
-
|
186
|
-
except Exception as e:
|
187
|
-
logger.error(f"Failed to validate client certificate: {e}")
|
188
|
-
return False
|
189
|
-
|
190
|
-
def _extract_roles_from_certificate(self, cert: x509.Certificate) -> List[str]:
|
191
|
-
"""
|
192
|
-
Extract roles from client certificate.
|
193
|
-
|
194
|
-
Args:
|
195
|
-
cert: Client certificate object
|
196
|
-
|
197
|
-
Returns:
|
198
|
-
List of roles extracted from certificate
|
199
|
-
"""
|
200
|
-
try:
|
201
|
-
return self.certificate_utils.extract_roles_from_certificate_object(cert)
|
202
|
-
except Exception as e:
|
203
|
-
logger.error(f"Failed to extract roles from certificate: {e}")
|
204
|
-
return []
|
205
|
-
|
206
|
-
def _validate_access(self, roles: List[str]) -> bool:
|
207
|
-
"""
|
208
|
-
Validate access based on roles.
|
209
|
-
|
210
|
-
Args:
|
211
|
-
roles: List of roles from client certificate
|
212
|
-
|
213
|
-
Returns:
|
214
|
-
True if access is allowed, False otherwise
|
215
|
-
"""
|
216
|
-
try:
|
217
|
-
if not self.allowed_roles:
|
218
|
-
return True
|
219
|
-
|
220
|
-
if not roles:
|
221
|
-
return False
|
222
|
-
|
223
|
-
# Check if any of the client roles match allowed roles
|
224
|
-
for client_role in roles:
|
225
|
-
for allowed_role in self.allowed_roles:
|
226
|
-
if self.role_utils.compare_roles(client_role, allowed_role):
|
227
|
-
return True
|
228
|
-
|
229
|
-
return False
|
230
|
-
|
231
|
-
except Exception as e:
|
232
|
-
logger.error(f"Failed to validate access: {e}")
|
233
|
-
return False
|
234
|
-
|
235
|
-
def _get_common_name(self, cert: x509.Certificate) -> str:
|
236
|
-
"""
|
237
|
-
Get common name from certificate.
|
238
|
-
|
239
|
-
Args:
|
240
|
-
cert: Certificate object
|
241
|
-
|
242
|
-
Returns:
|
243
|
-
Common name or empty string if not found
|
244
|
-
"""
|
245
|
-
try:
|
246
|
-
for name_attribute in cert.subject:
|
247
|
-
if name_attribute.oid == x509.NameOID.COMMON_NAME:
|
248
|
-
return str(name_attribute.value)
|
249
|
-
return ""
|
250
|
-
except Exception as e:
|
251
|
-
logger.error(f"Failed to get common name: {e}")
|
252
|
-
return ""
|
253
|
-
|
254
|
-
async def handle_error(self, request: Request, exception: Exception) -> Response:
|
255
|
-
"""
|
256
|
-
Handle mTLS authentication errors.
|
257
|
-
|
258
|
-
Args:
|
259
|
-
request: FastAPI request object
|
260
|
-
exception: Exception that occurred
|
261
|
-
|
262
|
-
Returns:
|
263
|
-
Error response
|
264
|
-
"""
|
265
|
-
from fastapi.responses import JSONResponse
|
266
|
-
|
267
|
-
error_message = str(exception)
|
268
|
-
|
269
|
-
if "certificate is required" in error_message.lower():
|
270
|
-
status_code = 401
|
271
|
-
error_code = -32009 # Certificate not found
|
272
|
-
elif "validation failed" in error_message.lower():
|
273
|
-
status_code = 401
|
274
|
-
error_code = -32003 # Certificate validation failed
|
275
|
-
elif "access denied" in error_message.lower():
|
276
|
-
status_code = 403
|
277
|
-
error_code = -32007 # Role validation failed
|
278
|
-
else:
|
279
|
-
status_code = 500
|
280
|
-
error_code = -32603 # Internal error
|
281
|
-
|
282
|
-
return JSONResponse(
|
283
|
-
status_code=status_code,
|
284
|
-
content={
|
285
|
-
"jsonrpc": "2.0",
|
286
|
-
"error": {
|
287
|
-
"code": error_code,
|
288
|
-
"message": error_message,
|
289
|
-
"data": {
|
290
|
-
"validation_type": "mtls",
|
291
|
-
"request_id": getattr(request.state, 'request_id', None)
|
292
|
-
}
|
293
|
-
},
|
294
|
-
"id": None
|
295
|
-
}
|
296
|
-
)
|