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.
Files changed (259) hide show
  1. mcp_proxy_adapter/api/app.py +174 -80
  2. mcp_proxy_adapter/api/handlers.py +16 -5
  3. mcp_proxy_adapter/api/middleware/__init__.py +7 -2
  4. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  5. mcp_proxy_adapter/api/middleware/factory.py +36 -12
  6. mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
  7. mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
  8. mcp_proxy_adapter/commands/__init__.py +7 -1
  9. mcp_proxy_adapter/commands/base.py +7 -4
  10. mcp_proxy_adapter/commands/builtin_commands.py +8 -2
  11. mcp_proxy_adapter/commands/command_registry.py +8 -0
  12. mcp_proxy_adapter/commands/echo_command.py +81 -0
  13. mcp_proxy_adapter/commands/help_command.py +21 -14
  14. mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
  15. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  16. mcp_proxy_adapter/commands/security_command.py +488 -0
  17. mcp_proxy_adapter/commands/ssl_setup_command.py +2 -2
  18. mcp_proxy_adapter/commands/token_management_command.py +1 -1
  19. mcp_proxy_adapter/config.py +81 -21
  20. mcp_proxy_adapter/core/app_factory.py +326 -0
  21. mcp_proxy_adapter/core/client_security.py +384 -0
  22. mcp_proxy_adapter/core/logging.py +8 -3
  23. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  24. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  25. mcp_proxy_adapter/core/protocol_manager.py +9 -0
  26. mcp_proxy_adapter/core/proxy_client.py +602 -0
  27. mcp_proxy_adapter/core/proxy_registration.py +299 -47
  28. mcp_proxy_adapter/core/security_adapter.py +12 -15
  29. mcp_proxy_adapter/core/security_integration.py +277 -0
  30. mcp_proxy_adapter/core/server_adapter.py +345 -0
  31. mcp_proxy_adapter/core/server_engine.py +364 -0
  32. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  33. mcp_proxy_adapter/examples/README.md +230 -97
  34. mcp_proxy_adapter/examples/README_EN.md +258 -0
  35. mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
  36. mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
  37. mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
  38. mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
  39. mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
  40. mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
  41. mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
  42. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
  43. mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
  44. mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
  45. mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
  46. mcp_proxy_adapter/examples/cert_config.json +9 -0
  47. mcp_proxy_adapter/examples/certs/admin.crt +32 -0
  48. mcp_proxy_adapter/examples/certs/admin.key +52 -0
  49. mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
  50. mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
  51. mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
  52. mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
  53. mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
  54. mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
  55. mcp_proxy_adapter/examples/certs/client.crt +32 -0
  56. mcp_proxy_adapter/examples/certs/client.key +52 -0
  57. mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
  58. mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
  59. mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
  60. mcp_proxy_adapter/examples/certs/client_user.key +52 -0
  61. mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
  62. mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
  63. mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
  64. mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
  65. mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
  66. mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
  67. mcp_proxy_adapter/examples/certs/readonly.key +52 -0
  68. mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
  69. mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
  70. mcp_proxy_adapter/examples/certs/server.crt +32 -0
  71. mcp_proxy_adapter/examples/certs/server.key +52 -0
  72. mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
  73. mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
  74. mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
  75. mcp_proxy_adapter/examples/certs/user.crt +32 -0
  76. mcp_proxy_adapter/examples/certs/user.key +52 -0
  77. mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
  78. mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
  79. mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
  80. mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
  81. mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
  82. mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
  83. mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
  84. mcp_proxy_adapter/examples/commands/__init__.py +1 -0
  85. mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
  86. mcp_proxy_adapter/examples/debug_request_state.py +144 -0
  87. mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
  88. mcp_proxy_adapter/examples/demo_client.py +341 -0
  89. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
  90. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
  91. mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
  92. mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
  93. mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
  94. mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
  95. mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
  96. mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
  97. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
  98. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
  99. mcp_proxy_adapter/examples/full_application/main.py +138 -0
  100. mcp_proxy_adapter/examples/full_application/roles.json +21 -0
  101. mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
  102. mcp_proxy_adapter/examples/generate_certificates.py +121 -0
  103. mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
  104. mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
  105. mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
  106. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
  107. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
  108. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
  109. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
  110. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
  111. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
  112. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
  113. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
  114. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
  115. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
  116. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
  117. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
  118. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
  119. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
  120. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
  121. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
  122. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
  123. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
  124. mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
  125. mcp_proxy_adapter/examples/roles.json +38 -0
  126. mcp_proxy_adapter/examples/run_example.py +81 -0
  127. mcp_proxy_adapter/examples/run_security_tests.py +326 -0
  128. mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
  129. mcp_proxy_adapter/examples/security_test_client.py +743 -0
  130. mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
  131. mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
  132. mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
  133. mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
  134. mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
  135. mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
  136. mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
  137. mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
  138. mcp_proxy_adapter/examples/test_examples.py +344 -0
  139. mcp_proxy_adapter/examples/universal_client.py +628 -0
  140. mcp_proxy_adapter/main.py +21 -10
  141. mcp_proxy_adapter/utils/config_generator.py +639 -0
  142. mcp_proxy_adapter/version.py +2 -1
  143. mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
  144. mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
  145. mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
  146. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
  147. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  148. mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
  149. mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
  150. mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
  151. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  152. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
  153. mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
  154. mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
  155. mcp_proxy_adapter/api/middleware/security.py +0 -376
  156. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
  157. mcp_proxy_adapter/examples/__init__.py +0 -7
  158. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  159. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  160. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  161. mcp_proxy_adapter/examples/basic_server/config.json +0 -70
  162. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
  163. mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
  164. mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
  165. mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
  166. mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
  167. mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
  168. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  169. mcp_proxy_adapter/examples/basic_server/server.py +0 -114
  170. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  171. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  172. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
  173. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  174. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  175. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  176. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
  177. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
  178. mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
  179. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
  180. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
  181. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
  182. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
  183. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
  184. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
  185. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  186. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  187. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  188. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  189. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  190. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  191. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  192. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
  193. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
  194. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
  195. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  196. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  197. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
  198. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  199. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
  200. mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
  201. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
  202. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
  203. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
  204. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  205. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
  206. mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
  207. mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
  208. mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
  209. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
  210. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  211. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  212. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  213. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  214. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  215. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  216. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  217. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  218. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  219. mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
  220. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
  221. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  222. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  223. mcp_proxy_adapter/schemas/roles_schema.json +0 -162
  224. mcp_proxy_adapter/tests/__init__.py +0 -0
  225. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  226. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  227. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  228. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  229. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  230. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  231. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  232. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  233. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  234. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  235. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  236. mcp_proxy_adapter/tests/conftest.py +0 -131
  237. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  238. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  239. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  240. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  241. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  242. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  243. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  244. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  245. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  246. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  247. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  248. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  249. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  250. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  251. mcp_proxy_adapter/tests/test_config.py +0 -127
  252. mcp_proxy_adapter/tests/test_utils.py +0 -65
  253. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  254. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  255. mcp_proxy_adapter/tests/unit/test_config.py +0 -270
  256. mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
  257. mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
  258. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
  259. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/top_level.txt +0 -0
@@ -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
- )
@@ -1,152 +0,0 @@
1
- """
2
- Middleware for rate limiting.
3
- """
4
-
5
- import time
6
- from typing import Dict, List, Callable, Awaitable
7
- from collections import defaultdict
8
-
9
- from fastapi import Request, Response
10
- from starlette.responses import JSONResponse
11
-
12
- from mcp_proxy_adapter.core.logging import logger
13
- from .base import BaseMiddleware
14
-
15
- class RateLimitMiddleware(BaseMiddleware):
16
- """
17
- Middleware for limiting request rate.
18
- """
19
-
20
- def __init__(self, app, rate_limit: int = 100, time_window: int = 60,
21
- by_ip: bool = True, by_user: bool = True,
22
- public_paths: List[str] = None):
23
- """
24
- Initializes middleware for rate limiting.
25
-
26
- Args:
27
- app: FastAPI application
28
- rate_limit: Maximum number of requests in the specified time period
29
- time_window: Time period in seconds
30
- by_ip: Limit requests by IP address
31
- by_user: Limit requests by user
32
- public_paths: List of paths for which rate limiting is not applied
33
- """
34
- super().__init__(app)
35
- self.rate_limit = rate_limit
36
- self.time_window = time_window
37
- self.by_ip = by_ip
38
- self.by_user = by_user
39
- self.public_paths = public_paths or [
40
- "/docs",
41
- "/redoc",
42
- "/openapi.json",
43
- "/health"
44
- ]
45
-
46
- # Storage for requests by IP
47
- self.ip_requests = defaultdict(list)
48
-
49
- # Storage for requests by user
50
- self.user_requests = defaultdict(list)
51
-
52
- async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
53
- """
54
- Processes request and checks rate limit.
55
-
56
- Args:
57
- request: Request.
58
- call_next: Next handler.
59
-
60
- Returns:
61
- Response.
62
- """
63
- # Check if path is public
64
- path = request.url.path
65
- if self._is_public_path(path):
66
- # If path is public, skip rate limiting
67
- return await call_next(request)
68
-
69
- # Current time
70
- current_time = time.time()
71
-
72
- # Get client IP address
73
- client_ip = request.client.host if request.client else "unknown"
74
-
75
- # Get user from request state (if any)
76
- username = getattr(request.state, "username", None)
77
-
78
- # Check limit by IP
79
- if self.by_ip and client_ip != "unknown":
80
- # Clean old requests
81
- self._clean_old_requests(self.ip_requests[client_ip], current_time)
82
-
83
- # Check number of requests
84
- if len(self.ip_requests[client_ip]) >= self.rate_limit:
85
- logger.warning(f"Rate limit exceeded for IP: {client_ip} | Path: {path}")
86
- return self._create_error_response("Rate limit exceeded", 429)
87
-
88
- # Add current request
89
- self.ip_requests[client_ip].append(current_time)
90
-
91
- # Check limit by user
92
- if self.by_user and username:
93
- # Clean old requests
94
- self._clean_old_requests(self.user_requests[username], current_time)
95
-
96
- # Check number of requests
97
- if len(self.user_requests[username]) >= self.rate_limit:
98
- logger.warning(f"Rate limit exceeded for user: {username} | Path: {path}")
99
- return self._create_error_response("Rate limit exceeded", 429)
100
-
101
- # Add current request
102
- self.user_requests[username].append(current_time)
103
-
104
- # Call the next middleware or main handler
105
- return await call_next(request)
106
-
107
- def _clean_old_requests(self, requests: List[float], current_time: float) -> None:
108
- """
109
- Cleans old requests that are outside the time window.
110
-
111
- Args:
112
- requests: List of request timestamps.
113
- current_time: Current time.
114
- """
115
- min_time = current_time - self.time_window
116
- while requests and requests[0] < min_time:
117
- requests.pop(0)
118
-
119
- def _is_public_path(self, path: str) -> bool:
120
- """
121
- Checks if the path is public.
122
-
123
- Args:
124
- path: Path to check.
125
-
126
- Returns:
127
- True if path is public, False otherwise.
128
- """
129
- return any(path.startswith(public_path) for public_path in self.public_paths)
130
-
131
- def _create_error_response(self, message: str, status_code: int) -> Response:
132
- """
133
- Creates error response in JSON-RPC format.
134
-
135
- Args:
136
- message: Error message.
137
- status_code: HTTP status code.
138
-
139
- Returns:
140
- JSON response with error.
141
- """
142
- return JSONResponse(
143
- status_code=status_code,
144
- content={
145
- "jsonrpc": "2.0",
146
- "error": {
147
- "code": -32000,
148
- "message": message
149
- },
150
- "id": None
151
- }
152
- )
@@ -1,241 +0,0 @@
1
- """
2
- Rate Limit Middleware Adapter for backward compatibility.
3
-
4
- This module provides an adapter that maintains the same interface as RateLimitMiddleware
5
- while using the new SecurityMiddleware internally.
6
- """
7
-
8
- import time
9
- from typing import Dict, List, Callable, Awaitable, Any
10
- from collections import defaultdict
11
-
12
- from fastapi import Request, Response
13
- from starlette.responses import JSONResponse
14
-
15
- from mcp_proxy_adapter.core.logging import logger
16
- from .base import BaseMiddleware
17
- from .security import SecurityMiddleware
18
-
19
-
20
- class RateLimitMiddlewareAdapter(BaseMiddleware):
21
- """
22
- Adapter for RateLimitMiddleware that uses SecurityMiddleware internally.
23
-
24
- Maintains the same interface as the original RateLimitMiddleware for backward compatibility.
25
- """
26
-
27
- def __init__(self, app, rate_limit: int = 100, time_window: int = 60,
28
- by_ip: bool = True, by_user: bool = True,
29
- public_paths: List[str] = None):
30
- """
31
- Initialize rate limit middleware adapter.
32
-
33
- Args:
34
- app: FastAPI application
35
- rate_limit: Maximum number of requests in the specified time period
36
- time_window: Time period in seconds
37
- by_ip: Limit requests by IP address
38
- by_user: Limit requests by user
39
- public_paths: List of paths for which rate limiting is not applied
40
- """
41
- super().__init__(app)
42
-
43
- # Store original parameters for backward compatibility
44
- self.rate_limit = rate_limit
45
- self.time_window = time_window
46
- self.by_ip = by_ip
47
- self.by_user = by_user
48
- self.public_paths = public_paths or [
49
- "/docs",
50
- "/redoc",
51
- "/openapi.json",
52
- "/health"
53
- ]
54
-
55
- # Legacy storage for backward compatibility
56
- self.ip_requests = defaultdict(list)
57
- self.user_requests = defaultdict(list)
58
-
59
- # Create internal security middleware
60
- self.security_middleware = self._create_security_middleware()
61
-
62
- logger.info(f"RateLimitMiddlewareAdapter initialized: rate_limit={rate_limit}, "
63
- f"time_window={time_window}, by_ip={by_ip}, by_user={by_user}")
64
-
65
- def _create_security_middleware(self) -> SecurityMiddleware:
66
- """
67
- Create internal SecurityMiddleware with RateLimitMiddleware configuration.
68
-
69
- Returns:
70
- SecurityMiddleware instance
71
- """
72
- # Convert RateLimitMiddleware config to SecurityMiddleware config
73
- security_config = {
74
- "security": {
75
- "enabled": True,
76
- "auth": {
77
- "enabled": False
78
- },
79
- "ssl": {
80
- "enabled": False
81
- },
82
- "permissions": {
83
- "enabled": False
84
- },
85
- "rate_limit": {
86
- "enabled": True,
87
- "requests_per_minute": self.rate_limit,
88
- "requests_per_hour": self.rate_limit * 60,
89
- "burst_limit": self.rate_limit // 10,
90
- "by_ip": self.by_ip,
91
- "by_user": self.by_user
92
- },
93
- "public_paths": self.public_paths
94
- }
95
- }
96
-
97
- return SecurityMiddleware(self.app, security_config)
98
-
99
- async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
100
- """
101
- Process request using internal SecurityMiddleware with legacy fallback.
102
-
103
- Args:
104
- request: Request object
105
- call_next: Next handler
106
-
107
- Returns:
108
- Response object
109
- """
110
- # Check if path is public
111
- path = request.url.path
112
- if self._is_public_path(path):
113
- return await call_next(request)
114
-
115
- # Try to use SecurityMiddleware first
116
- try:
117
- await self.security_middleware.before_request(request)
118
- return await call_next(request)
119
-
120
- except Exception as e:
121
- # Fallback to legacy rate limiting if SecurityMiddleware fails
122
- logger.warning(f"SecurityMiddleware rate limiting failed, using legacy fallback: {e}")
123
- return await self._legacy_rate_limit_check(request, call_next)
124
-
125
- async def _legacy_rate_limit_check(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
126
- """
127
- Legacy rate limiting implementation as fallback.
128
-
129
- Args:
130
- request: Request object
131
- call_next: Next handler
132
-
133
- Returns:
134
- Response object
135
- """
136
- current_time = time.time()
137
- client_ip = request.client.host if request.client else "unknown"
138
- username = getattr(request.state, "username", None)
139
-
140
- # Check limit by IP
141
- if self.by_ip and client_ip != "unknown":
142
- self._clean_old_requests(self.ip_requests[client_ip], current_time)
143
-
144
- if len(self.ip_requests[client_ip]) >= self.rate_limit:
145
- logger.warning(f"Rate limit exceeded for IP: {client_ip}")
146
- return self._create_error_response("Rate limit exceeded", 429)
147
-
148
- self.ip_requests[client_ip].append(current_time)
149
-
150
- # Check limit by user
151
- if self.by_user and username:
152
- self._clean_old_requests(self.user_requests[username], current_time)
153
-
154
- if len(self.user_requests[username]) >= self.rate_limit:
155
- logger.warning(f"Rate limit exceeded for user: {username}")
156
- return self._create_error_response("Rate limit exceeded", 429)
157
-
158
- self.user_requests[username].append(current_time)
159
-
160
- return await call_next(request)
161
-
162
- def _clean_old_requests(self, requests_list: List[float], current_time: float) -> None:
163
- """
164
- Remove old requests from the list.
165
-
166
- Args:
167
- requests_list: List of request timestamps
168
- current_time: Current time
169
- """
170
- cutoff_time = current_time - self.time_window
171
- requests_list[:] = [req_time for req_time in requests_list if req_time > cutoff_time]
172
-
173
- def _is_public_path(self, path: str) -> bool:
174
- """
175
- Check if the path is public (doesn't require rate limiting).
176
-
177
- Args:
178
- path: Request path
179
-
180
- Returns:
181
- True if path is public, False otherwise
182
- """
183
- return any(path.startswith(public_path) for public_path in self.public_paths)
184
-
185
- def _create_error_response(self, message: str, status_code: int) -> JSONResponse:
186
- """
187
- Create error response in RateLimitMiddleware format.
188
-
189
- Args:
190
- message: Error message
191
- status_code: HTTP status code
192
-
193
- Returns:
194
- JSONResponse with error
195
- """
196
- return JSONResponse(
197
- status_code=status_code,
198
- content={
199
- "jsonrpc": "2.0",
200
- "error": {
201
- "code": -32008 if status_code == 429 else -32603,
202
- "message": message,
203
- "data": {
204
- "rate_limit": self.rate_limit,
205
- "time_window": self.time_window,
206
- "status_code": status_code
207
- }
208
- },
209
- "id": None
210
- }
211
- )
212
-
213
- def get_rate_limit_info(self, request: Request) -> Dict[str, Any]:
214
- """
215
- Get rate limit information for the request (backward compatibility).
216
-
217
- Args:
218
- request: Request object
219
-
220
- Returns:
221
- Dictionary with rate limit information
222
- """
223
- client_ip = request.client.host if request.client else "unknown"
224
- username = getattr(request.state, "username", None)
225
-
226
- info = {
227
- "rate_limit": self.rate_limit,
228
- "time_window": self.time_window,
229
- "by_ip": self.by_ip,
230
- "by_user": self.by_user
231
- }
232
-
233
- if self.by_ip and client_ip != "unknown":
234
- info["ip_requests"] = len(self.ip_requests[client_ip])
235
- info["ip_remaining"] = max(0, self.rate_limit - len(self.ip_requests[client_ip]))
236
-
237
- if self.by_user and username:
238
- info["user_requests"] = len(self.user_requests[username])
239
- info["user_remaining"] = max(0, self.rate_limit - len(self.user_requests[username]))
240
-
241
- return info