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.
Files changed (212) hide show
  1. mcp_proxy_adapter/__main__.py +27 -7
  2. mcp_proxy_adapter/api/app.py +209 -79
  3. mcp_proxy_adapter/api/handlers.py +16 -5
  4. mcp_proxy_adapter/api/middleware/__init__.py +14 -9
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/factory.py +36 -12
  7. mcp_proxy_adapter/api/middleware/protocol_middleware.py +84 -18
  8. mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
  9. mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
  10. mcp_proxy_adapter/commands/__init__.py +7 -1
  11. mcp_proxy_adapter/commands/base.py +7 -4
  12. mcp_proxy_adapter/commands/builtin_commands.py +8 -2
  13. mcp_proxy_adapter/commands/command_registry.py +8 -0
  14. mcp_proxy_adapter/commands/echo_command.py +81 -0
  15. mcp_proxy_adapter/commands/health_command.py +1 -1
  16. mcp_proxy_adapter/commands/help_command.py +21 -14
  17. mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
  18. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  19. mcp_proxy_adapter/commands/security_command.py +488 -0
  20. mcp_proxy_adapter/commands/ssl_setup_command.py +234 -351
  21. mcp_proxy_adapter/commands/token_management_command.py +1 -1
  22. mcp_proxy_adapter/config.py +323 -40
  23. mcp_proxy_adapter/core/app_factory.py +410 -0
  24. mcp_proxy_adapter/core/app_runner.py +272 -0
  25. mcp_proxy_adapter/core/certificate_utils.py +291 -73
  26. mcp_proxy_adapter/core/client.py +574 -0
  27. mcp_proxy_adapter/core/client_manager.py +284 -0
  28. mcp_proxy_adapter/core/client_security.py +384 -0
  29. mcp_proxy_adapter/core/logging.py +8 -3
  30. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  31. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  32. mcp_proxy_adapter/core/protocol_manager.py +169 -10
  33. mcp_proxy_adapter/core/proxy_client.py +602 -0
  34. mcp_proxy_adapter/core/proxy_registration.py +299 -47
  35. mcp_proxy_adapter/core/security_adapter.py +12 -15
  36. mcp_proxy_adapter/core/security_integration.py +286 -0
  37. mcp_proxy_adapter/core/server_adapter.py +282 -0
  38. mcp_proxy_adapter/core/server_engine.py +270 -0
  39. mcp_proxy_adapter/core/ssl_utils.py +13 -12
  40. mcp_proxy_adapter/core/transport_manager.py +5 -5
  41. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  42. mcp_proxy_adapter/examples/__init__.py +13 -4
  43. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  44. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  45. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  46. mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
  47. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  48. mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
  49. mcp_proxy_adapter/examples/debug_request_state.py +112 -0
  50. mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
  51. mcp_proxy_adapter/examples/demo_client.py +275 -0
  52. mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
  53. mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
  54. mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
  55. mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
  56. mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
  57. mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
  58. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
  59. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  60. mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
  61. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
  62. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  63. mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
  64. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
  65. mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
  66. mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
  67. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
  68. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  69. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
  70. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
  71. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  72. mcp_proxy_adapter/examples/full_application/main.py +173 -0
  73. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
  74. mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
  75. mcp_proxy_adapter/examples/generate_certificates.py +177 -0
  76. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
  77. mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
  78. mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
  79. mcp_proxy_adapter/examples/run_example.py +59 -0
  80. mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
  81. mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
  82. mcp_proxy_adapter/examples/run_security_tests.py +544 -0
  83. mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
  84. mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
  85. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
  86. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
  87. mcp_proxy_adapter/examples/security_test_client.py +782 -0
  88. mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
  89. mcp_proxy_adapter/examples/test_config.py +148 -0
  90. mcp_proxy_adapter/examples/test_config_generator.py +86 -0
  91. mcp_proxy_adapter/examples/test_examples.py +281 -0
  92. mcp_proxy_adapter/examples/universal_client.py +620 -0
  93. mcp_proxy_adapter/main.py +66 -148
  94. mcp_proxy_adapter/utils/config_generator.py +1008 -0
  95. mcp_proxy_adapter/version.py +5 -2
  96. mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
  97. mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
  98. mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
  99. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
  100. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  101. mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
  102. mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
  103. mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
  104. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  105. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
  106. mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
  107. mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
  108. mcp_proxy_adapter/api/middleware/security.py +0 -376
  109. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
  110. mcp_proxy_adapter/examples/README.md +0 -124
  111. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  112. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  113. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  114. mcp_proxy_adapter/examples/basic_server/config.json +0 -70
  115. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
  116. mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
  117. mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
  118. mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
  119. mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
  120. mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
  121. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  122. mcp_proxy_adapter/examples/basic_server/server.py +0 -114
  123. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  124. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  125. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
  126. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  127. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  128. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  129. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
  130. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
  131. mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
  132. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
  133. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
  134. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
  135. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
  136. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
  137. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
  138. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  139. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  140. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  141. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  142. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  143. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  144. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  145. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
  146. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
  147. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
  148. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  149. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  150. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
  151. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  152. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
  153. mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
  154. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
  155. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
  156. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
  157. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  158. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
  159. mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
  160. mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
  161. mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
  162. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
  163. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  164. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  165. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  166. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  167. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  168. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  169. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  170. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  171. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  172. mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
  173. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
  174. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  175. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  176. mcp_proxy_adapter/schemas/roles_schema.json +0 -162
  177. mcp_proxy_adapter/tests/__init__.py +0 -0
  178. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  179. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  180. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  181. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  182. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  183. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  184. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  185. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  186. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  187. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  188. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  189. mcp_proxy_adapter/tests/conftest.py +0 -131
  190. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  191. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  192. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  193. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  194. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  195. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  196. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  197. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  198. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  199. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  200. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  201. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  202. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  203. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  204. mcp_proxy_adapter/tests/test_config.py +0 -127
  205. mcp_proxy_adapter/tests/test_utils.py +0 -65
  206. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  207. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  208. mcp_proxy_adapter/tests/unit/test_config.py +0 -270
  209. mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
  210. mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
  211. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
  212. {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
- )