mcp-proxy-adapter 4.1.1__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 (253) hide show
  1. mcp_proxy_adapter/__main__.py +12 -0
  2. mcp_proxy_adapter/api/app.py +254 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +36 -30
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +7 -0
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +159 -2
  41. mcp_proxy_adapter/core/app_factory.py +326 -0
  42. mcp_proxy_adapter/core/auth_validator.py +606 -0
  43. mcp_proxy_adapter/core/certificate_utils.py +827 -0
  44. mcp_proxy_adapter/core/client_security.py +384 -0
  45. mcp_proxy_adapter/core/config_converter.py +405 -0
  46. mcp_proxy_adapter/core/config_validator.py +218 -0
  47. mcp_proxy_adapter/core/logging.py +19 -3
  48. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  49. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  50. mcp_proxy_adapter/core/protocol_manager.py +235 -0
  51. mcp_proxy_adapter/core/proxy_client.py +602 -0
  52. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  53. mcp_proxy_adapter/core/role_utils.py +426 -0
  54. mcp_proxy_adapter/core/security_adapter.py +370 -0
  55. mcp_proxy_adapter/core/security_factory.py +239 -0
  56. mcp_proxy_adapter/core/security_integration.py +277 -0
  57. mcp_proxy_adapter/core/server_adapter.py +345 -0
  58. mcp_proxy_adapter/core/server_engine.py +364 -0
  59. mcp_proxy_adapter/core/settings.py +1 -0
  60. mcp_proxy_adapter/core/ssl_utils.py +233 -0
  61. mcp_proxy_adapter/core/transport_manager.py +292 -0
  62. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  63. mcp_proxy_adapter/custom_openapi.py +22 -11
  64. mcp_proxy_adapter/examples/README.md +230 -97
  65. mcp_proxy_adapter/examples/README_EN.md +258 -0
  66. mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
  67. mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
  68. mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
  69. mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
  70. mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
  71. mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
  72. mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
  73. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
  74. mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
  75. mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
  76. mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
  77. mcp_proxy_adapter/examples/cert_config.json +9 -0
  78. mcp_proxy_adapter/examples/certs/admin.crt +32 -0
  79. mcp_proxy_adapter/examples/certs/admin.key +52 -0
  80. mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
  81. mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
  82. mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
  83. mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
  84. mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
  85. mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
  86. mcp_proxy_adapter/examples/certs/client.crt +32 -0
  87. mcp_proxy_adapter/examples/certs/client.key +52 -0
  88. mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
  89. mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
  90. mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
  91. mcp_proxy_adapter/examples/certs/client_user.key +52 -0
  92. mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
  93. mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
  94. mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
  95. mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
  96. mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
  97. mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
  98. mcp_proxy_adapter/examples/certs/readonly.key +52 -0
  99. mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
  100. mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
  101. mcp_proxy_adapter/examples/certs/server.crt +32 -0
  102. mcp_proxy_adapter/examples/certs/server.key +52 -0
  103. mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
  104. mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
  105. mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
  106. mcp_proxy_adapter/examples/certs/user.crt +32 -0
  107. mcp_proxy_adapter/examples/certs/user.key +52 -0
  108. mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
  109. mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
  110. mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
  111. mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
  112. mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
  113. mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
  114. mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
  115. mcp_proxy_adapter/examples/commands/__init__.py +1 -0
  116. mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
  117. mcp_proxy_adapter/examples/debug_request_state.py +144 -0
  118. mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
  119. mcp_proxy_adapter/examples/demo_client.py +341 -0
  120. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
  121. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
  122. mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
  123. mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
  124. mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
  125. mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
  126. mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
  127. mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
  128. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
  129. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
  130. mcp_proxy_adapter/examples/full_application/main.py +138 -0
  131. mcp_proxy_adapter/examples/full_application/roles.json +21 -0
  132. mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
  133. mcp_proxy_adapter/examples/generate_certificates.py +121 -0
  134. mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
  135. mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
  136. mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
  137. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
  138. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
  139. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
  140. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
  141. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
  142. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
  143. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
  144. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
  145. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
  146. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
  147. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
  148. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
  149. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
  150. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
  151. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
  152. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
  153. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
  154. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
  155. mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
  156. mcp_proxy_adapter/examples/roles.json +38 -0
  157. mcp_proxy_adapter/examples/run_example.py +81 -0
  158. mcp_proxy_adapter/examples/run_security_tests.py +326 -0
  159. mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
  160. mcp_proxy_adapter/examples/security_test_client.py +743 -0
  161. mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
  162. mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
  163. mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
  164. mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
  165. mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
  166. mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
  167. mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
  168. mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
  169. mcp_proxy_adapter/examples/test_examples.py +344 -0
  170. mcp_proxy_adapter/examples/universal_client.py +628 -0
  171. mcp_proxy_adapter/main.py +186 -0
  172. mcp_proxy_adapter/utils/config_generator.py +639 -0
  173. mcp_proxy_adapter/version.py +2 -1
  174. mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
  175. mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
  176. mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
  177. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
  178. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  179. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  180. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  181. mcp_proxy_adapter/examples/__init__.py +0 -7
  182. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  183. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  184. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  185. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  186. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  187. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  188. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  189. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  190. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  191. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  192. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  193. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  194. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  195. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  196. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  197. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  198. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  199. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  200. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  201. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  202. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  203. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  204. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  205. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  206. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  207. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  208. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  209. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  210. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  211. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  212. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  213. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  214. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  215. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  216. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  217. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  218. mcp_proxy_adapter/tests/__init__.py +0 -0
  219. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  220. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  221. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  222. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  223. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  224. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  225. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  226. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  227. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  228. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  229. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  230. mcp_proxy_adapter/tests/conftest.py +0 -131
  231. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  232. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  233. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  234. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  235. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  236. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  237. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  238. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  239. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  240. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  241. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  242. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  243. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  244. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  245. mcp_proxy_adapter/tests/test_config.py +0 -127
  246. mcp_proxy_adapter/tests/test_utils.py +0 -65
  247. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  248. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  249. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  250. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  251. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  252. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
  253. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,579 @@
1
+ """
2
+ Unified Configuration Adapter for mcp_security_framework integration.
3
+
4
+ Author: Vasiliy Zdanovskiy
5
+ email: vasilyvz@gmail.com
6
+
7
+ This module provides a unified adapter for converting mcp_proxy_adapter configuration
8
+ to SecurityConfig format used by mcp_security_framework.
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ from typing import Dict, Any, Optional, List, Union
14
+ from pathlib import Path
15
+ from dataclasses import dataclass
16
+
17
+ # Import mcp_security_framework components
18
+ try:
19
+ from mcp_security_framework import SecurityConfig
20
+ from mcp_security_framework.schemas.config import (
21
+ AuthConfig, SSLConfig, PermissionConfig, RateLimitConfig
22
+ )
23
+ SECURITY_FRAMEWORK_AVAILABLE = True
24
+ except ImportError:
25
+ SECURITY_FRAMEWORK_AVAILABLE = False
26
+ SecurityConfig = None
27
+ AuthConfig = None
28
+ SSLConfig = None
29
+ PermissionConfig = None
30
+ RateLimitConfig = None
31
+
32
+ from mcp_proxy_adapter.core.logging import logger
33
+
34
+
35
+ @dataclass
36
+ class ValidationResult:
37
+ """Result of configuration validation."""
38
+
39
+ is_valid: bool
40
+ errors: List[str]
41
+ warnings: List[str]
42
+ details: Dict[str, Any]
43
+
44
+ def __post_init__(self):
45
+ """Initialize with empty lists if None."""
46
+ if self.errors is None:
47
+ self.errors = []
48
+ if self.warnings is None:
49
+ self.warnings = []
50
+ if self.details is None:
51
+ self.details = {}
52
+
53
+
54
+ class UnifiedConfigAdapter:
55
+ """
56
+ Unified adapter for converting mcp_proxy_adapter configuration to SecurityConfig.
57
+
58
+ This adapter handles:
59
+ - Legacy configuration format compatibility
60
+ - Configuration validation
61
+ - Conversion to mcp_security_framework format
62
+ - Default value management
63
+ """
64
+
65
+ def __init__(self):
66
+ """Initialize the unified configuration adapter."""
67
+ self.validation_errors = []
68
+ self.validation_warnings = []
69
+
70
+ def convert_to_security_config(self, config: Dict[str, Any]) -> Optional[SecurityConfig]:
71
+ """
72
+ Convert mcp_proxy_adapter configuration to SecurityConfig.
73
+
74
+ Args:
75
+ config: mcp_proxy_adapter configuration dictionary
76
+
77
+ Returns:
78
+ SecurityConfig instance or None if conversion failed
79
+ """
80
+ if not SECURITY_FRAMEWORK_AVAILABLE:
81
+ logger.error("mcp_security_framework not available, cannot convert configuration")
82
+ return None
83
+
84
+ try:
85
+ # Validate configuration first
86
+ validation_result = self.validate_configuration(config)
87
+ if not validation_result.is_valid:
88
+ logger.error(f"Configuration validation failed: {validation_result.errors}")
89
+ return None
90
+
91
+ # Convert configuration sections
92
+ auth_config = self._convert_auth_config(config)
93
+ ssl_config = self._convert_ssl_config(config)
94
+ permission_config = self._convert_permission_config(config)
95
+ rate_limit_config = self._convert_rate_limit_config(config)
96
+
97
+ # Create SecurityConfig
98
+ security_config = SecurityConfig(
99
+ auth=auth_config,
100
+ ssl=ssl_config,
101
+ permissions=permission_config,
102
+ rate_limit=rate_limit_config
103
+ )
104
+
105
+ logger.info("Configuration successfully converted to SecurityConfig")
106
+ return security_config
107
+
108
+ except Exception as e:
109
+ logger.error(f"Failed to convert configuration: {e}")
110
+ return None
111
+
112
+ def validate_configuration(self, config: Dict[str, Any]) -> ValidationResult:
113
+ """
114
+ Validate configuration before conversion.
115
+
116
+ Args:
117
+ config: Configuration dictionary to validate
118
+
119
+ Returns:
120
+ ValidationResult with validation details
121
+ """
122
+ self.validation_errors = []
123
+ self.validation_warnings = []
124
+
125
+ # Debug: Check SSL config at start of validation
126
+ if "security" in config:
127
+ ssl_config = config["security"].get("ssl", {})
128
+ print(f"🔍 Debug: SSL config at start of validation: enabled={ssl_config.get('enabled', False)}")
129
+
130
+ # Debug: Check if root ssl section exists
131
+ if "ssl" in config:
132
+ print(f"🔍 Debug: Root SSL section found: enabled={config['ssl'].get('enabled', False)}")
133
+
134
+ # Check if config is a dictionary
135
+ if not isinstance(config, dict):
136
+ return ValidationResult(
137
+ is_valid=False,
138
+ errors=["Configuration must be a dictionary"],
139
+ warnings=[],
140
+ details={}
141
+ )
142
+
143
+ # Validate security section
144
+ self._validate_security_section(config)
145
+
146
+ # Validate legacy sections for compatibility
147
+ self._validate_legacy_sections(config)
148
+
149
+ # Check for conflicts
150
+ self._check_configuration_conflicts(config)
151
+
152
+ # Validate individual sections
153
+ self._validate_auth_section(config)
154
+ self._validate_ssl_section(config)
155
+ self._validate_permissions_section(config)
156
+ self._validate_rate_limit_section(config)
157
+
158
+ return ValidationResult(
159
+ is_valid=len(self.validation_errors) == 0,
160
+ errors=self.validation_errors.copy(),
161
+ warnings=self.validation_warnings.copy(),
162
+ details={
163
+ "has_security_section": "security" in config,
164
+ "has_legacy_sections": any(key in config for key in ["ssl", "roles", "auth_enabled"]),
165
+ "total_errors": len(self.validation_errors),
166
+ "total_warnings": len(self.validation_warnings)
167
+ }
168
+ )
169
+
170
+ def _validate_security_section(self, config: Dict[str, Any]):
171
+ """Validate security section."""
172
+ security_config = config.get("security", {})
173
+
174
+ if not isinstance(security_config, dict):
175
+ self.validation_errors.append("Security section must be a dictionary")
176
+ return
177
+
178
+ # Check for unknown keys in security section
179
+ known_keys = {"enabled", "auth", "ssl", "permissions", "rate_limit", "public_paths"}
180
+ unknown_keys = set(security_config.keys()) - known_keys
181
+
182
+ if unknown_keys:
183
+ self.validation_warnings.append(f"Unknown keys in security section: {unknown_keys}")
184
+
185
+ def _validate_legacy_sections(self, config: Dict[str, Any]):
186
+ """Validate legacy configuration sections."""
187
+ legacy_sections = ["ssl", "roles", "auth_enabled", "rate_limit_enabled"]
188
+
189
+ for section in legacy_sections:
190
+ if section in config:
191
+ self.validation_warnings.append(f"Legacy section '{section}' found, consider migrating to security section")
192
+
193
+ def _check_configuration_conflicts(self, config: Dict[str, Any]):
194
+ """Check for configuration conflicts."""
195
+ security_config = config.get("security", {})
196
+
197
+ # Check for SSL configuration conflicts
198
+ if "ssl" in config and "ssl" in security_config:
199
+ self.validation_warnings.append("SSL configuration found in both root and security sections")
200
+
201
+ # Check for auth configuration conflicts
202
+ if "auth_enabled" in config and "auth" in security_config:
203
+ self.validation_warnings.append("Auth configuration found in both root and security sections")
204
+
205
+ def _validate_auth_section(self, config: Dict[str, Any]):
206
+ """Validate authentication section."""
207
+ auth_config = self._get_auth_config(config)
208
+
209
+ if not isinstance(auth_config, dict):
210
+ self.validation_errors.append("Auth configuration must be a dictionary")
211
+ return
212
+
213
+ # Validate auth methods
214
+ methods = auth_config.get("methods", [])
215
+ if not isinstance(methods, list):
216
+ self.validation_errors.append("Auth methods must be a list")
217
+ else:
218
+ valid_methods = {"api_key", "jwt", "certificate"}
219
+ invalid_methods = set(methods) - valid_methods
220
+ if invalid_methods:
221
+ self.validation_errors.append(f"Invalid auth methods: {invalid_methods}")
222
+
223
+ # Validate API keys
224
+ api_keys = auth_config.get("api_keys", {})
225
+ if not isinstance(api_keys, dict):
226
+ self.validation_errors.append("API keys must be a dictionary")
227
+
228
+ # Validate JWT configuration
229
+ if "jwt" in methods:
230
+ jwt_secret = auth_config.get("jwt_secret", "")
231
+ if not jwt_secret:
232
+ self.validation_warnings.append("JWT secret is empty or not set")
233
+
234
+ def _validate_ssl_section(self, config: Dict[str, Any]):
235
+ """Validate SSL section."""
236
+ ssl_config = self._get_ssl_config(config)
237
+
238
+ if not isinstance(ssl_config, dict):
239
+ self.validation_errors.append("SSL configuration must be a dictionary")
240
+ return
241
+
242
+ # Validate certificate files
243
+ if ssl_config.get("enabled", False):
244
+ cert_file = ssl_config.get("cert_file")
245
+ key_file = ssl_config.get("key_file")
246
+
247
+ print(f"🔍 Debug: _validate_ssl_section: cert_file={cert_file}")
248
+ print(f"🔍 Debug: _validate_ssl_section: key_file={key_file}")
249
+ print(f"🔍 Debug: _validate_ssl_section: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}")
250
+ print(f"🔍 Debug: _validate_ssl_section: key_file exists={Path(key_file).exists() if key_file else 'None'}")
251
+
252
+ if cert_file and not Path(cert_file).exists():
253
+ self.validation_warnings.append(f"SSL certificate file not found: {cert_file}")
254
+
255
+ if key_file and not Path(key_file).exists():
256
+ self.validation_warnings.append(f"SSL key file not found: {key_file}")
257
+
258
+ def _validate_permissions_section(self, config: Dict[str, Any]):
259
+ """Validate permissions section."""
260
+ permissions_config = self._get_permissions_config(config)
261
+
262
+ if not isinstance(permissions_config, dict):
263
+ self.validation_errors.append("Permissions configuration must be a dictionary")
264
+ return
265
+
266
+ # Validate roles file
267
+ if permissions_config.get("enabled", False):
268
+ roles_file = permissions_config.get("roles_file")
269
+ if roles_file and not Path(roles_file).exists():
270
+ self.validation_warnings.append(f"Roles file not found: {roles_file}")
271
+
272
+ def _validate_rate_limit_section(self, config: Dict[str, Any]):
273
+ """Validate rate limit section."""
274
+ rate_limit_config = self._get_rate_limit_config(config)
275
+
276
+ if not isinstance(rate_limit_config, dict):
277
+ self.validation_errors.append("Rate limit configuration must be a dictionary")
278
+ return
279
+
280
+ # Validate rate limit values only if enabled
281
+ if rate_limit_config.get("enabled", False):
282
+ requests_per_minute = rate_limit_config.get("requests_per_minute", rate_limit_config.get("default_requests_per_minute", 0))
283
+ if requests_per_minute <= 0:
284
+ self.validation_errors.append("requests_per_minute must be greater than 0 when rate limiting is enabled")
285
+
286
+ def _convert_auth_config(self, config: Dict[str, Any]) -> AuthConfig:
287
+ """Convert authentication configuration."""
288
+ auth_config = self._get_auth_config(config)
289
+
290
+ # Get authentication methods
291
+ methods = auth_config.get("methods", ["api_key"])
292
+
293
+ # Get API keys from multiple sources
294
+ api_keys = auth_config.get("api_keys", {})
295
+ if not api_keys:
296
+ # Try legacy SSL config
297
+ legacy_ssl = config.get("ssl", {})
298
+ api_keys = legacy_ssl.get("api_keys", {})
299
+
300
+ return AuthConfig(
301
+ enabled=auth_config.get("enabled", True),
302
+ methods=methods,
303
+ api_keys=api_keys,
304
+ jwt_secret=auth_config.get("jwt_secret", ""),
305
+ jwt_algorithm=auth_config.get("jwt_algorithm", "HS256"),
306
+ jwt_expiration=auth_config.get("jwt_expiration", 3600)
307
+ )
308
+
309
+ def _convert_ssl_config(self, config: Dict[str, Any]) -> SSLConfig:
310
+ """Convert SSL configuration."""
311
+ ssl_config = self._get_ssl_config(config)
312
+
313
+ return SSLConfig(
314
+ enabled=ssl_config.get("enabled", False),
315
+ cert_file=ssl_config.get("cert_file"),
316
+ key_file=ssl_config.get("key_file"),
317
+ ca_cert=ssl_config.get("ca_cert"),
318
+ min_tls_version=ssl_config.get("min_tls_version", "TLSv1.2"),
319
+ verify_client=ssl_config.get("verify_client", False),
320
+ client_cert_required=ssl_config.get("client_cert_required", False),
321
+ cipher_suites=ssl_config.get("cipher_suites", [])
322
+ )
323
+
324
+ def _convert_permission_config(self, config: Dict[str, Any]) -> PermissionConfig:
325
+ """Convert permissions configuration."""
326
+ permissions_config = self._get_permissions_config(config)
327
+
328
+ return PermissionConfig(
329
+ enabled=permissions_config.get("enabled", True),
330
+ roles_file=permissions_config.get("roles_file", "roles.json"),
331
+ default_role=permissions_config.get("default_role", "user"),
332
+ deny_by_default=permissions_config.get("deny_by_default", True),
333
+ role_mappings=permissions_config.get("role_mappings", {})
334
+ )
335
+
336
+ def _convert_rate_limit_config(self, config: Dict[str, Any]) -> RateLimitConfig:
337
+ """Convert rate limit configuration."""
338
+ rate_limit_config = self._get_rate_limit_config(config)
339
+
340
+ return RateLimitConfig(
341
+ enabled=rate_limit_config.get("enabled", False),
342
+ requests_per_minute=rate_limit_config.get("requests_per_minute", 60),
343
+ requests_per_hour=rate_limit_config.get("requests_per_hour", 1000),
344
+ requests_per_day=rate_limit_config.get("requests_per_day", 10000),
345
+ burst_limit=rate_limit_config.get("burst_limit", 10),
346
+ by_ip=rate_limit_config.get("by_ip", True),
347
+ by_user=rate_limit_config.get("by_user", True),
348
+ by_endpoint=rate_limit_config.get("by_endpoint", False),
349
+ exempt_roles=rate_limit_config.get("exempt_roles", []),
350
+ exempt_endpoints=rate_limit_config.get("exempt_endpoints", [])
351
+ )
352
+
353
+ def _get_auth_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
354
+ """Get authentication configuration from config."""
355
+ security_config = config.get("security", {})
356
+
357
+ # Ensure security_config is a dictionary
358
+ if not isinstance(security_config, dict):
359
+ return {}
360
+
361
+ auth_config = security_config.get("auth", {})
362
+
363
+ # Handle legacy auth_enabled flag
364
+ if config.get("auth_enabled") is not None:
365
+ auth_config["enabled"] = config["auth_enabled"]
366
+
367
+ return auth_config
368
+
369
+ def _get_ssl_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
370
+ """Get SSL configuration from config."""
371
+ security_config = config.get("security", {})
372
+
373
+ # Ensure security_config is a dictionary
374
+ if not isinstance(security_config, dict):
375
+ return {}
376
+
377
+ ssl_config = security_config.get("ssl", {})
378
+
379
+ # Debug: Check SSL config before merging
380
+ print(f"🔍 Debug: _get_ssl_config: security.ssl key_file={ssl_config.get('key_file')}")
381
+
382
+ # Merge with legacy SSL config, but prioritize security.ssl over legacy ssl
383
+ legacy_ssl = config.get("ssl", {})
384
+ if legacy_ssl:
385
+ print(f"🔍 Debug: _get_ssl_config: legacy ssl key_file={legacy_ssl.get('key_file')}")
386
+ # Only merge legacy config if security.ssl is not enabled or missing
387
+ if not ssl_config.get("enabled", False):
388
+ ssl_config.update(legacy_ssl)
389
+ else:
390
+ # If security.ssl is enabled, only merge non-conflicting fields
391
+ for key, value in legacy_ssl.items():
392
+ if key not in ssl_config or ssl_config[key] is None:
393
+ ssl_config[key] = value
394
+
395
+ print(f"🔍 Debug: _get_ssl_config: final key_file={ssl_config.get('key_file')}")
396
+ return ssl_config
397
+
398
+ def _get_permissions_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
399
+ """Get permissions configuration from config."""
400
+ security_config = config.get("security", {})
401
+
402
+ # Ensure security_config is a dictionary
403
+ if not isinstance(security_config, dict):
404
+ return {}
405
+
406
+ permissions_config = security_config.get("permissions", {})
407
+
408
+ # Merge with legacy roles config
409
+ legacy_roles = config.get("roles", {})
410
+ if legacy_roles:
411
+ permissions_config.update(legacy_roles)
412
+
413
+ return permissions_config
414
+
415
+ def _get_rate_limit_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
416
+ """Get rate limit configuration from config."""
417
+ security_config = config.get("security", {})
418
+
419
+ # Ensure security_config is a dictionary
420
+ if not isinstance(security_config, dict):
421
+ return {}
422
+
423
+ rate_limit_config = security_config.get("rate_limit", {})
424
+
425
+ # Handle legacy rate_limit_enabled flag
426
+ if config.get("rate_limit_enabled") is not None:
427
+ rate_limit_config["enabled"] = config["rate_limit_enabled"]
428
+
429
+ return rate_limit_config
430
+
431
+ def get_public_paths(self, config: Dict[str, Any]) -> List[str]:
432
+ """
433
+ Get public paths from configuration.
434
+
435
+ Args:
436
+ config: Configuration dictionary
437
+
438
+ Returns:
439
+ List of public paths
440
+ """
441
+ security_config = config.get("security", {})
442
+
443
+ # Ensure security_config is a dictionary
444
+ if not isinstance(security_config, dict):
445
+ return [
446
+ "/health",
447
+ "/docs",
448
+ "/redoc",
449
+ "/openapi.json",
450
+ "/favicon.ico"
451
+ ]
452
+
453
+ public_paths = security_config.get("public_paths", [])
454
+
455
+ # Add default public paths if none specified
456
+ if not public_paths:
457
+ public_paths = [
458
+ "/health",
459
+ "/docs",
460
+ "/redoc",
461
+ "/openapi.json",
462
+ "/favicon.ico"
463
+ ]
464
+
465
+ return public_paths
466
+
467
+ def get_security_enabled(self, config: Dict[str, Any]) -> bool:
468
+ """
469
+ Check if security is enabled in configuration.
470
+
471
+ Args:
472
+ config: Configuration dictionary
473
+
474
+ Returns:
475
+ True if security is enabled
476
+ """
477
+ security_config = config.get("security", {})
478
+
479
+ # Ensure security_config is a dictionary
480
+ if not isinstance(security_config, dict):
481
+ return True # Default to enabled for safety
482
+
483
+ return security_config.get("enabled", True)
484
+
485
+ def migrate_legacy_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
486
+ """
487
+ Migrate legacy configuration to new format.
488
+
489
+ Args:
490
+ config: Legacy configuration dictionary
491
+
492
+ Returns:
493
+ Migrated configuration dictionary
494
+ """
495
+ migrated_config = config.copy()
496
+
497
+ # Create security section if it doesn't exist
498
+ if "security" not in migrated_config:
499
+ migrated_config["security"] = {}
500
+
501
+ security_config = migrated_config["security"]
502
+
503
+ # Migrate SSL configuration
504
+ if "ssl" in migrated_config and "ssl" not in security_config:
505
+ security_config["ssl"] = migrated_config["ssl"]
506
+ # Don't remove legacy SSL yet for backward compatibility
507
+
508
+ # Migrate roles configuration
509
+ if "roles" in migrated_config and "permissions" not in security_config:
510
+ security_config["permissions"] = migrated_config["roles"]
511
+ # Don't remove legacy roles yet for backward compatibility
512
+
513
+ # Migrate auth_enabled flag
514
+ if "auth_enabled" in migrated_config and "auth" not in security_config:
515
+ security_config["auth"] = {"enabled": migrated_config["auth_enabled"]}
516
+
517
+ # Migrate rate_limit_enabled flag
518
+ if "rate_limit_enabled" in migrated_config and "rate_limit" not in security_config:
519
+ security_config["rate_limit"] = {"enabled": migrated_config["rate_limit_enabled"]}
520
+
521
+ logger.info("Legacy configuration migrated to new format")
522
+ return migrated_config
523
+
524
+ def get_default_config(self) -> Dict[str, Any]:
525
+ """
526
+ Get default configuration.
527
+
528
+ Returns:
529
+ Default configuration dictionary
530
+ """
531
+ return {
532
+ "security": {
533
+ "enabled": True,
534
+ "auth": {
535
+ "enabled": True,
536
+ "methods": ["api_key"],
537
+ "api_keys": {},
538
+ "jwt_secret": "",
539
+ "jwt_algorithm": "HS256",
540
+ "jwt_expiration": 3600
541
+ },
542
+ "ssl": {
543
+ "enabled": False,
544
+ "cert_file": None,
545
+ "key_file": None,
546
+ "ca_cert": None,
547
+ "min_tls_version": "TLSv1.2",
548
+ "verify_client": False,
549
+ "client_cert_required": False,
550
+ "cipher_suites": []
551
+ },
552
+ "permissions": {
553
+ "enabled": True,
554
+ "roles_file": "roles.json",
555
+ "default_role": "user",
556
+ "deny_by_default": True,
557
+ "role_mappings": {}
558
+ },
559
+ "rate_limit": {
560
+ "enabled": True,
561
+ "requests_per_minute": 60,
562
+ "requests_per_hour": 1000,
563
+ "requests_per_day": 10000,
564
+ "burst_limit": 10,
565
+ "by_ip": True,
566
+ "by_user": True,
567
+ "by_endpoint": False,
568
+ "exempt_roles": [],
569
+ "exempt_endpoints": []
570
+ },
571
+ "public_paths": [
572
+ "/health",
573
+ "/docs",
574
+ "/redoc",
575
+ "/openapi.json",
576
+ "/favicon.ico"
577
+ ]
578
+ }
579
+ }
@@ -96,14 +96,25 @@ class CustomOpenAPIGenerator:
96
96
  Returns:
97
97
  Dict containing the parameter schema.
98
98
  """
99
- # Get command schema
100
- cmd_schema = cmd_class.get_schema()
101
-
102
- # Add title and description
103
- cmd_schema["title"] = f"Parameters for {cmd_class.name}"
104
- cmd_schema["description"] = f"Parameters for the {cmd_class.name} command"
105
-
106
- return cmd_schema
99
+ try:
100
+ # Get command schema
101
+ cmd_schema = cmd_class.get_schema()
102
+
103
+ # Add title and description
104
+ cmd_schema["title"] = f"Parameters for {cmd_class.name}"
105
+ cmd_schema["description"] = f"Parameters for the {cmd_class.name} command"
106
+
107
+ return cmd_schema
108
+ except Exception as e:
109
+ # Return default schema if command schema generation fails
110
+ logger.warning(f"Failed to get schema for command {cmd_class.name}: {e}")
111
+ return {
112
+ "type": "object",
113
+ "title": f"Parameters for {cmd_class.name}",
114
+ "description": f"Parameters for the {cmd_class.name} command (schema generation failed)",
115
+ "properties": {},
116
+ "additionalProperties": True
117
+ }
107
118
 
108
119
  def generate(self, title: Optional[str] = None, description: Optional[str] = None, version: Optional[str] = None) -> Dict[str, Any]:
109
120
  """
@@ -262,7 +273,7 @@ class CustomOpenAPIGenerator:
262
273
  # Add commands to schema
263
274
  self._add_commands_to_schema(schema)
264
275
 
265
- logger.info(f"Generated OpenAPI schema with {len(registry.get_all_commands())} commands")
276
+ logger.debug(f"Generated OpenAPI schema with {len(registry.get_all_commands())} commands")
266
277
 
267
278
  return schema
268
279
 
@@ -358,9 +369,9 @@ def custom_openapi_with_fallback(app: FastAPI) -> Dict[str, Any]:
358
369
  # Use the first registered generator
359
370
  generator_name = list(_openapi_generators.keys())[0]
360
371
  generator_func = _openapi_generators[generator_name]
361
- logger.info(f"Using custom OpenAPI generator: {generator_name}")
372
+ logger.debug(f"Using custom OpenAPI generator: {generator_name}")
362
373
  return generator_func(app)
363
374
 
364
375
  # Fall back to default generator
365
- logger.info("Using default OpenAPI generator")
376
+ logger.debug("Using default OpenAPI generator")
366
377
  return custom_openapi(app)