mcp-proxy-adapter 6.9.28__py3-none-any.whl → 6.9.29__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.

Potentially problematic release.


This version of mcp-proxy-adapter might be problematic. Click here for more details.

Files changed (212) hide show
  1. mcp_proxy_adapter/__init__.py +10 -0
  2. mcp_proxy_adapter/__main__.py +8 -21
  3. mcp_proxy_adapter/api/app.py +10 -913
  4. mcp_proxy_adapter/api/core/__init__.py +18 -0
  5. mcp_proxy_adapter/api/core/app_factory.py +243 -0
  6. mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
  7. mcp_proxy_adapter/api/core/registration_manager.py +166 -0
  8. mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
  9. mcp_proxy_adapter/api/handlers.py +78 -199
  10. mcp_proxy_adapter/api/middleware/__init__.py +1 -44
  11. mcp_proxy_adapter/api/middleware/base.py +0 -42
  12. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
  13. mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
  14. mcp_proxy_adapter/api/middleware/factory.py +0 -94
  15. mcp_proxy_adapter/api/middleware/logging.py +0 -112
  16. mcp_proxy_adapter/api/middleware/performance.py +0 -35
  17. mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
  18. mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
  19. mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
  20. mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
  21. mcp_proxy_adapter/api/openapi/__init__.py +21 -0
  22. mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
  23. mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
  24. mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
  25. mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
  26. mcp_proxy_adapter/api/schemas.py +0 -61
  27. mcp_proxy_adapter/api/tool_integration.py +0 -117
  28. mcp_proxy_adapter/api/tools.py +0 -46
  29. mcp_proxy_adapter/cli/__init__.py +12 -0
  30. mcp_proxy_adapter/cli/commands/__init__.py +15 -0
  31. mcp_proxy_adapter/cli/commands/client.py +100 -0
  32. mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
  33. mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
  34. mcp_proxy_adapter/cli/commands/generate.py +259 -0
  35. mcp_proxy_adapter/cli/commands/server.py +174 -0
  36. mcp_proxy_adapter/cli/commands/sets.py +128 -0
  37. mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
  38. mcp_proxy_adapter/cli/examples/__init__.py +8 -0
  39. mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
  40. mcp_proxy_adapter/cli/examples/https_token.py +96 -0
  41. mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
  42. mcp_proxy_adapter/cli/main.py +63 -0
  43. mcp_proxy_adapter/cli/parser.py +324 -0
  44. mcp_proxy_adapter/cli/validators.py +231 -0
  45. mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
  46. mcp_proxy_adapter/client/proxy.py +45 -0
  47. mcp_proxy_adapter/commands/__init__.py +44 -28
  48. mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
  49. mcp_proxy_adapter/commands/base.py +19 -43
  50. mcp_proxy_adapter/commands/builtin_commands.py +0 -75
  51. mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
  52. mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
  53. mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
  54. mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
  55. mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
  56. mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
  57. mcp_proxy_adapter/commands/catalog_manager.py +58 -928
  58. mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
  59. mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
  60. mcp_proxy_adapter/commands/command_registry.py +172 -904
  61. mcp_proxy_adapter/commands/config_command.py +0 -28
  62. mcp_proxy_adapter/commands/dependency_container.py +1 -70
  63. mcp_proxy_adapter/commands/dependency_manager.py +0 -128
  64. mcp_proxy_adapter/commands/echo_command.py +0 -34
  65. mcp_proxy_adapter/commands/health_command.py +0 -3
  66. mcp_proxy_adapter/commands/help_command.py +0 -159
  67. mcp_proxy_adapter/commands/hooks.py +0 -137
  68. mcp_proxy_adapter/commands/key_management_command.py +0 -25
  69. mcp_proxy_adapter/commands/load_command.py +7 -78
  70. mcp_proxy_adapter/commands/plugins_command.py +0 -16
  71. mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
  72. mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
  73. mcp_proxy_adapter/commands/queue_commands.py +750 -0
  74. mcp_proxy_adapter/commands/registration_status_command.py +0 -43
  75. mcp_proxy_adapter/commands/registry/__init__.py +18 -0
  76. mcp_proxy_adapter/commands/registry/command_info.py +103 -0
  77. mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
  78. mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
  79. mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
  80. mcp_proxy_adapter/commands/reload_command.py +0 -80
  81. mcp_proxy_adapter/commands/result.py +25 -77
  82. mcp_proxy_adapter/commands/role_test_command.py +0 -44
  83. mcp_proxy_adapter/commands/roles_management_command.py +0 -199
  84. mcp_proxy_adapter/commands/security_command.py +0 -30
  85. mcp_proxy_adapter/commands/settings_command.py +0 -68
  86. mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
  87. mcp_proxy_adapter/commands/token_management_command.py +0 -1
  88. mcp_proxy_adapter/commands/transport_management_command.py +0 -20
  89. mcp_proxy_adapter/commands/unload_command.py +0 -71
  90. mcp_proxy_adapter/config.py +15 -626
  91. mcp_proxy_adapter/core/__init__.py +5 -39
  92. mcp_proxy_adapter/core/app_factory.py +14 -36
  93. mcp_proxy_adapter/core/app_runner.py +0 -27
  94. mcp_proxy_adapter/core/auth_validator.py +1 -93
  95. mcp_proxy_adapter/core/certificate/__init__.py +20 -0
  96. mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
  97. mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
  98. mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
  99. mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
  100. mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
  101. mcp_proxy_adapter/core/certificate_utils.py +64 -903
  102. mcp_proxy_adapter/core/client.py +0 -6
  103. mcp_proxy_adapter/core/client_manager.py +0 -19
  104. mcp_proxy_adapter/core/client_security.py +0 -2
  105. mcp_proxy_adapter/core/config/__init__.py +18 -0
  106. mcp_proxy_adapter/core/config/config.py +195 -0
  107. mcp_proxy_adapter/core/config/config_factory.py +22 -0
  108. mcp_proxy_adapter/core/config/config_loader.py +66 -0
  109. mcp_proxy_adapter/core/config/feature_manager.py +31 -0
  110. mcp_proxy_adapter/core/config/simple_config.py +112 -0
  111. mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
  112. mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
  113. mcp_proxy_adapter/core/config_converter.py +0 -186
  114. mcp_proxy_adapter/core/config_validator.py +96 -1238
  115. mcp_proxy_adapter/core/errors.py +7 -42
  116. mcp_proxy_adapter/core/job_manager.py +54 -0
  117. mcp_proxy_adapter/core/logging.py +2 -22
  118. mcp_proxy_adapter/core/mtls_asgi.py +0 -20
  119. mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
  120. mcp_proxy_adapter/core/mtls_proxy.py +0 -80
  121. mcp_proxy_adapter/core/mtls_server.py +3 -173
  122. mcp_proxy_adapter/core/protocol_manager.py +1 -191
  123. mcp_proxy_adapter/core/proxy/__init__.py +22 -0
  124. mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
  125. mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
  126. mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
  127. mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
  128. mcp_proxy_adapter/core/proxy_client.py +0 -1
  129. mcp_proxy_adapter/core/proxy_registration.py +36 -913
  130. mcp_proxy_adapter/core/role_utils.py +0 -308
  131. mcp_proxy_adapter/core/security_adapter.py +1 -36
  132. mcp_proxy_adapter/core/security_factory.py +1 -150
  133. mcp_proxy_adapter/core/security_integration.py +0 -33
  134. mcp_proxy_adapter/core/server_adapter.py +1 -40
  135. mcp_proxy_adapter/core/server_engine.py +2 -173
  136. mcp_proxy_adapter/core/settings.py +0 -127
  137. mcp_proxy_adapter/core/signal_handler.py +0 -65
  138. mcp_proxy_adapter/core/ssl_utils.py +19 -137
  139. mcp_proxy_adapter/core/transport_manager.py +0 -151
  140. mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
  141. mcp_proxy_adapter/core/utils.py +1 -182
  142. mcp_proxy_adapter/core/validation/__init__.py +21 -0
  143. mcp_proxy_adapter/core/validation/config_validator.py +211 -0
  144. mcp_proxy_adapter/core/validation/file_validator.py +73 -0
  145. mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
  146. mcp_proxy_adapter/core/validation/security_validator.py +58 -0
  147. mcp_proxy_adapter/core/validation/validation_result.py +27 -0
  148. mcp_proxy_adapter/custom_openapi.py +33 -652
  149. mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
  150. mcp_proxy_adapter/examples/check_config.py +0 -2
  151. mcp_proxy_adapter/examples/client_usage_example.py +164 -0
  152. mcp_proxy_adapter/examples/config_builder.py +13 -2
  153. mcp_proxy_adapter/examples/config_cli.py +0 -1
  154. mcp_proxy_adapter/examples/create_test_configs.py +0 -46
  155. mcp_proxy_adapter/examples/debug_request_state.py +0 -1
  156. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
  157. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
  158. mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
  159. mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
  160. mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
  161. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
  162. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
  163. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
  164. mcp_proxy_adapter/examples/full_application/main.py +186 -150
  165. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
  166. mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
  167. mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
  168. mcp_proxy_adapter/examples/generate_config.py +65 -11
  169. mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
  170. mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
  171. mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
  172. mcp_proxy_adapter/examples/queue_server_example.py +85 -0
  173. mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
  174. mcp_proxy_adapter/examples/required_certificates.py +0 -2
  175. mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
  176. mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
  177. mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
  178. mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
  179. mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
  180. mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
  181. mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
  182. mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
  183. mcp_proxy_adapter/examples/security_test_client.py +24 -1075
  184. mcp_proxy_adapter/examples/setup/__init__.py +24 -0
  185. mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
  186. mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
  187. mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
  188. mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
  189. mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
  190. mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
  191. mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
  192. mcp_proxy_adapter/examples/test_config.py +0 -3
  193. mcp_proxy_adapter/examples/test_config_builder.py +25 -405
  194. mcp_proxy_adapter/examples/test_examples.py +0 -1
  195. mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
  196. mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
  197. mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
  198. mcp_proxy_adapter/examples/universal_client.py +0 -6
  199. mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
  200. mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
  201. mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
  202. mcp_proxy_adapter/integrations/__init__.py +25 -0
  203. mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
  204. mcp_proxy_adapter/main.py +70 -62
  205. mcp_proxy_adapter/openapi.py +0 -22
  206. mcp_proxy_adapter/version.py +1 -1
  207. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/METADATA +2 -1
  208. mcp_proxy_adapter-6.9.29.dist-info/RECORD +235 -0
  209. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/entry_points.txt +1 -1
  210. mcp_proxy_adapter-6.9.28.dist-info/RECORD +0 -149
  211. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/WHEEL +0 -0
  212. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/top_level.txt +0 -0
@@ -1,35 +1,20 @@
1
1
  """
2
- Configuration Validator for MCP Proxy Adapter
3
- Validates configuration files and ensures all required settings are present and correct.
4
-
5
2
  Author: Vasiliy Zdanovskiy
6
3
  email: vasilyvz@gmail.com
4
+
5
+ Main configuration validator for MCP Proxy Adapter.
7
6
  """
8
7
 
9
8
  import json
10
- import os
11
9
  import logging
12
- import re
13
10
  from pathlib import Path
14
- from typing import Dict, List, Any, Optional, Tuple, Set
15
- from enum import Enum
16
- from dataclasses import dataclass
17
- from datetime import datetime, timezone
18
- import ssl
19
- import socket
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class ValidationLevel(Enum):
25
- """Validation severity levels."""
26
- ERROR = "error"
27
- WARNING = "warning"
28
- INFO = "info"
11
+ from typing import Dict, List, Any, Optional
29
12
 
13
+ from .file_validator import FileValidator
14
+ from .security_validator import SecurityValidator
15
+ from .protocol_validator import ProtocolValidator
30
16
 
31
- # Import ValidationResult and exceptions from errors to avoid circular imports
32
- from .errors import ValidationResult, MissingConfigKeyError
17
+ logger = logging.getLogger(__name__)
33
18
 
34
19
 
35
20
  class ConfigValidator:
@@ -49,138 +34,12 @@ class ConfigValidator:
49
34
  Initialize configuration validator.
50
35
 
51
36
  Args:
52
- config_path: Path to configuration file for validation
37
+ config_path: Path to configuration file (optional)
53
38
  """
54
39
  self.config_path = config_path
55
40
  self.config_data: Dict[str, Any] = {}
56
41
  self.validation_results: List[ValidationResult] = []
57
-
58
- # Define required sections and their keys
59
- self.required_sections = {
60
- "server": {
61
- "host": str,
62
- "port": int,
63
- "protocol": str,
64
- "debug": bool,
65
- "log_level": str
66
- },
67
- "logging": {
68
- "level": str,
69
- "log_dir": str,
70
- "log_file": str,
71
- "error_log_file": str,
72
- "access_log_file": str,
73
- "max_file_size": (str, int),
74
- "backup_count": int,
75
- "format": str,
76
- "date_format": str,
77
- "console_output": bool,
78
- "file_output": bool
79
- },
80
- "commands": {
81
- "auto_discovery": bool,
82
- "commands_directory": str,
83
- "catalog_directory": str,
84
- "plugin_servers": list,
85
- "auto_install_dependencies": bool,
86
- "enabled_commands": list,
87
- "disabled_commands": list,
88
- "custom_commands_path": str
89
- },
90
- "transport": {
91
- "type": str,
92
- "port": (int, type(None)),
93
- "verify_client": bool,
94
- "chk_hostname": bool
95
- },
96
- "proxy_registration": {
97
- "enabled": bool,
98
- "proxy_url": str,
99
- "server_id": str,
100
- "server_name": str,
101
- "description": str,
102
- "version": str,
103
- "registration_timeout": int,
104
- "retry_attempts": int,
105
- "retry_delay": int,
106
- "auto_register_on_startup": bool,
107
- "auto_unregister_on_shutdown": bool
108
- },
109
- "debug": {
110
- "enabled": bool,
111
- "level": str
112
- },
113
- "security": {
114
- "enabled": bool,
115
- "tokens": dict,
116
- "roles": dict,
117
- "roles_file": (str, type(None))
118
- },
119
- "roles": {
120
- "enabled": bool,
121
- "config_file": (str, type(None)),
122
- "default_policy": dict,
123
- "auto_load": bool,
124
- "validation_enabled": bool
125
- }
126
- }
127
-
128
- # Define feature flags and their dependencies
129
- self.feature_flags = {
130
- "security": {
131
- "enabled_key": "security.enabled",
132
- "dependencies": ["security.tokens", "security.roles"],
133
- "required_files": ["security.roles_file"],
134
- "optional_files": []
135
- },
136
- "roles": {
137
- "enabled_key": "roles.enabled",
138
- "dependencies": ["roles.config_file"],
139
- "required_files": ["roles.config_file"],
140
- "optional_files": []
141
- },
142
- "proxy_registration": {
143
- "enabled_key": "proxy_registration.enabled",
144
- "dependencies": ["proxy_registration.proxy_url"],
145
- "required_files": [],
146
- "optional_files": [
147
- "proxy_registration.certificate.cert_file",
148
- "proxy_registration.certificate.key_file"
149
- ]
150
- },
151
- "ssl": {
152
- "enabled_key": "ssl.enabled",
153
- "dependencies": ["ssl.cert_file", "ssl.key_file"],
154
- "required_files": ["ssl.cert_file", "ssl.key_file"],
155
- "optional_files": ["ssl.ca_cert"]
156
- },
157
- "transport_ssl": {
158
- "enabled_key": "transport.ssl.enabled",
159
- "dependencies": ["transport.ssl.cert_file", "transport.ssl.key_file"],
160
- "required_files": ["transport.ssl.cert_file", "transport.ssl.key_file"],
161
- "optional_files": ["transport.ssl.ca_cert"]
162
- }
163
- }
164
-
165
- # Protocol-specific requirements
166
- self.protocol_requirements = {
167
- "http": {
168
- "ssl_enabled": False,
169
- "client_verification": False,
170
- "required_files": []
171
- },
172
- "https": {
173
- "ssl_enabled": True,
174
- "client_verification": False,
175
- "required_files": ["ssl.cert_file", "ssl.key_file"]
176
- },
177
- "mtls": {
178
- "ssl_enabled": True,
179
- "client_verification": True,
180
- "required_files": ["ssl.cert_file", "ssl.key_file", "ssl.ca_cert"]
181
- }
182
- }
183
-
42
+
184
43
  def load_config(self, config_path: Optional[str] = None) -> None:
185
44
  """
186
45
  Load configuration from file.
@@ -190,1094 +49,128 @@ class ConfigValidator:
190
49
  """
191
50
  if config_path:
192
51
  self.config_path = config_path
193
-
194
- if not self.config_path or not os.path.exists(self.config_path):
195
- self.validation_results.append(ValidationResult(
196
- level="error",
197
- message=f"Configuration file not found: {self.config_path}",
198
- section="config_file"
199
- ))
200
- return
201
-
52
+
53
+ if not self.config_path:
54
+ raise ValueError("No configuration path provided")
55
+
202
56
  try:
203
57
  with open(self.config_path, 'r', encoding='utf-8') as f:
204
58
  self.config_data = json.load(f)
59
+ logger.info(f"Configuration loaded from {self.config_path}")
60
+ except FileNotFoundError:
61
+ raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
205
62
  except json.JSONDecodeError as e:
206
- self.validation_results.append(ValidationResult(
207
- level="error",
208
- message=f"Invalid JSON in configuration file: {e}",
209
- section="config_file"
210
- ))
63
+ raise ValueError(f"Invalid JSON in configuration file: {e}")
211
64
  except Exception as e:
212
- self.validation_results.append(ValidationResult(
213
- level="error",
214
- message=f"Error loading configuration file: {e}",
215
- section="config_file"
216
- ))
217
-
65
+ raise RuntimeError(f"Error loading configuration: {e}")
66
+
218
67
  def validate_config(self, config_data: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
219
68
  """
220
69
  Validate configuration data.
221
70
 
222
71
  Args:
223
- config_data: Configuration data to validate. If None, uses loaded config.
224
-
72
+ config_data: Configuration data to validate (optional)
73
+
225
74
  Returns:
226
75
  List of validation results
227
76
  """
228
77
  if config_data is not None:
229
78
  self.config_data = config_data
230
-
79
+
231
80
  if not self.config_data:
232
- self.validation_results.append(ValidationResult(
233
- level="error",
234
- message="No configuration data to validate",
235
- section="config_data"
236
- ))
237
- return self.validation_results
238
-
239
- # Clear previous results
81
+ raise ValueError("No configuration data to validate")
82
+
240
83
  self.validation_results = []
241
84
 
242
- # Perform all validation checks
243
- self._validate_required_sections()
244
- self._validate_feature_flags()
245
- self._validate_protocol_requirements()
246
- self._validate_file_existence()
247
- self._validate_security_consistency()
248
- self._validate_ssl_configuration()
249
- self._validate_proxy_registration()
250
- self._validate_roles_configuration()
251
- self._validate_uuid_format()
85
+ # Initialize validators
86
+ file_validator = FileValidator(self.config_data)
87
+ security_validator = SecurityValidator(self.config_data)
88
+ protocol_validator = ProtocolValidator(self.config_data)
89
+
90
+ # Run all validations
91
+ self.validation_results.extend(protocol_validator.validate_required_sections())
92
+ self.validation_results.extend(protocol_validator.validate_protocol_requirements())
93
+ self.validation_results.extend(file_validator.validate_file_existence())
94
+ self.validation_results.extend(security_validator.validate_security_consistency())
95
+ self.validation_results.extend(security_validator.validate_ssl_configuration())
96
+ self.validation_results.extend(security_validator.validate_roles_configuration())
97
+ self.validation_results.extend(security_validator.validate_proxy_registration())
98
+
99
+ # Additional validations
252
100
  self._validate_unknown_fields()
101
+ self._validate_uuid_format()
253
102
 
254
103
  return self.validation_results
255
-
104
+
256
105
  def validate_all(self, config_data: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
257
106
  """
258
- Alias for validate_config method for backward compatibility.
107
+ Validate all aspects of the configuration.
259
108
 
260
109
  Args:
261
- config_data: Configuration data to validate. If None, uses loaded config.
262
-
110
+ config_data: Configuration data to validate (optional)
111
+
263
112
  Returns:
264
113
  List of validation results
265
114
  """
266
115
  return self.validate_config(config_data)
267
-
268
- def _validate_required_sections(self) -> None:
269
- """Validate that all required sections and keys are present for enabled features only."""
270
- # Always required sections (core functionality)
271
- always_required = {
272
- "server": self.required_sections["server"],
273
- "logging": self.required_sections["logging"],
274
- "commands": self.required_sections["commands"]
116
+
117
+ def _validate_unknown_fields(self) -> None:
118
+ """Validate for unknown configuration fields."""
119
+ known_sections = {
120
+ "server", "protocols", "security", "ssl", "auth", "roles",
121
+ "logging", "commands", "proxy_registration", "transport"
275
122
  }
276
123
 
277
- # Check always required sections
278
- for section_name, required_keys in always_required.items():
279
- if section_name not in self.config_data:
124
+ for section in self.config_data.keys():
125
+ if section not in known_sections:
280
126
  self.validation_results.append(ValidationResult(
281
- level="error",
282
- message=f"Required section '{section_name}' is missing",
283
- section=section_name
284
- ))
285
- continue
286
-
287
- section_data = self.config_data[section_name]
288
- for key, expected_type in required_keys.items():
289
- if key not in section_data:
290
- self.validation_results.append(ValidationResult(
291
- level="error",
292
- message=f"Required key '{key}' is missing in section '{section_name}'",
293
- section=section_name,
294
- key=key
295
- ))
296
- else:
297
- # Validate type
298
- value = section_data[key]
299
- if isinstance(expected_type, tuple):
300
- if not isinstance(value, expected_type):
301
- expected_names = [t.__name__ for t in expected_type]
302
- self.validation_results.append(ValidationResult(
303
- level="error",
304
- message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {' or '.join(expected_names)}, got {type(value).__name__}",
305
- section=section_name,
306
- key=key
307
- ))
308
- else:
309
- if not isinstance(value, expected_type):
310
- self.validation_results.append(ValidationResult(
311
- level="error",
312
- message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {expected_type.__name__}, got {type(value).__name__}",
313
- section=section_name,
314
- key=key
315
- ))
316
-
317
- # Check conditional sections based on feature flags
318
- protocol = self._get_nested_value_safe("server.protocol", "http")
319
-
320
- for feature_name, feature_config in self.feature_flags.items():
321
- enabled_key = feature_config["enabled_key"]
322
-
323
- # Skip SSL validation for HTTP protocol
324
- if feature_name in ["ssl", "transport_ssl"] and protocol not in ["https", "mtls"]:
325
- continue
326
-
327
- # Only check if the enabled key exists in the configuration
328
- if not self._has_nested_key(enabled_key):
329
- continue
330
-
331
- is_enabled = self._get_nested_value_safe(enabled_key, False)
332
-
333
- if is_enabled and feature_name in self.required_sections:
334
- section_name = feature_name
335
- required_keys = self.required_sections[section_name]
336
-
337
- if section_name not in self.config_data:
338
- self.validation_results.append(ValidationResult(
339
- level="error",
340
- message=f"Required section '{section_name}' is missing for enabled feature",
341
- section=section_name
342
- ))
343
- continue
344
-
345
- section_data = self.config_data[section_name]
346
- for key, expected_type in required_keys.items():
347
- # Check if key allows None (optional)
348
- is_optional = isinstance(expected_type, tuple) and type(None) in expected_type
349
-
350
- if key not in section_data:
351
- # Only report error if key is not optional
352
- if not is_optional:
353
- self.validation_results.append(ValidationResult(
354
- level="error",
355
- message=f"Required key '{key}' is missing in section '{section_name}' for enabled feature",
356
- section=section_name,
357
- key=key
358
- ))
359
- else:
360
- # Validate type
361
- value = section_data[key]
362
- if isinstance(expected_type, tuple):
363
- if not isinstance(value, expected_type):
364
- expected_names = [t.__name__ for t in expected_type]
365
- self.validation_results.append(ValidationResult(
366
- level="error",
367
- message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {' or '.join(expected_names)}, got {type(value).__name__}",
368
- section=section_name,
369
- key=key
370
- ))
371
- else:
372
- if not isinstance(value, expected_type):
373
- self.validation_results.append(ValidationResult(
374
- level="error",
375
- message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {expected_type.__name__}, got {type(value).__name__}",
376
- section=section_name,
377
- key=key
378
- ))
379
-
380
- def _validate_feature_flags(self) -> None:
381
- """Validate feature flags and their dependencies."""
382
- protocol = self._get_nested_value_safe("server.protocol", "http")
383
-
384
- for feature_name, feature_config in self.feature_flags.items():
385
- enabled_key = feature_config["enabled_key"]
386
-
387
- # Skip SSL validation for HTTP protocol
388
- if feature_name in ["ssl", "transport_ssl"] and protocol not in ["https", "mtls"]:
389
- continue
390
-
391
- # Check if the enabled key exists in the configuration
392
- if not self._has_nested_key(enabled_key):
393
- # Skip validation if the feature flag key doesn't exist
394
- continue
395
-
396
- is_enabled = self._get_nested_value_safe(enabled_key, False)
397
-
398
- if is_enabled:
399
- # Check dependencies
400
- for dependency in feature_config["dependencies"]:
401
- if not self._has_nested_key(dependency):
402
- self.validation_results.append(ValidationResult(
403
- level="error",
404
- message=f"Feature '{feature_name}' is enabled but required dependency '{dependency}' is missing",
405
- section=feature_name,
406
- key=dependency
407
- ))
408
-
409
- # Check required files
410
- for file_key in feature_config["required_files"]:
411
- if self._has_nested_key(file_key):
412
- file_path = self._get_nested_value(file_key)
413
- if file_path and not os.path.exists(file_path):
414
- self.validation_results.append(ValidationResult(
415
- level="error",
416
- message=f"Feature '{feature_name}' is enabled but required file '{file_path}' does not exist",
417
- section=feature_name,
418
- key=file_key
419
- ))
420
- else:
421
- # Feature is disabled - check that optional files are not required
422
- for file_key in feature_config["optional_files"]:
423
- if self._has_nested_key(file_key):
424
- file_path = self._get_nested_value(file_key)
425
- if file_path and not os.path.exists(file_path):
426
- self.validation_results.append(ValidationResult(
427
- level="warning",
428
- message=f"Optional file '{file_path}' for disabled feature '{feature_name}' does not exist",
429
- section=feature_name,
430
- key=file_key,
431
- suggestion="This is not an error since the feature is disabled"
432
- ))
433
-
434
- def _validate_protocol_requirements(self) -> None:
435
- """Validate protocol-specific requirements."""
436
- protocol = self._get_nested_value_safe("server.protocol", "http")
437
-
438
- # Check mTLS protocol requirements
439
- if protocol == "mtls":
440
- # mTLS requires HTTPS protocol
441
- if not self._has_nested_key("ssl.enabled"):
442
- self.validation_results.append(ValidationResult(
443
- level="error",
444
- message="mTLS protocol requires SSL configuration",
445
- section="ssl",
446
- key="enabled"
127
+ level="warning",
128
+ message=f"Unknown configuration section: {section}",
129
+ section=section,
130
+ suggestion="Check if this section is needed or if it's a typo"
447
131
  ))
448
- else:
449
- ssl_enabled = self._get_nested_value_safe("ssl.enabled", False)
450
- if not ssl_enabled:
451
- self.validation_results.append(ValidationResult(
452
- level="error",
453
- message="mTLS protocol requires SSL to be enabled",
454
- section="ssl",
455
- key="enabled"
456
- ))
457
-
458
- if protocol not in self.protocol_requirements:
459
- self.validation_results.append(ValidationResult(
460
- level="error",
461
- message=f"Unsupported protocol: {protocol}",
462
- section="server",
463
- key="protocol"
464
- ))
465
- return
466
132
 
467
- requirements = self.protocol_requirements[protocol]
468
-
469
- # Check SSL requirements
470
- if requirements["ssl_enabled"]:
471
- # Only check SSL if ssl section exists
472
- if self._has_nested_key("ssl.enabled"):
473
- ssl_enabled = self._get_nested_value_safe("ssl.enabled", False)
474
- if not ssl_enabled:
475
- self.validation_results.append(ValidationResult(
476
- level="error",
477
- message=f"Protocol '{protocol}' requires SSL to be enabled",
478
- section="ssl",
479
- key="enabled"
480
- ))
481
-
482
- # Check required SSL files
483
- for file_key in requirements["required_files"]:
484
- if self._has_nested_key(file_key):
485
- file_path = self._get_nested_value(file_key)
486
- if not file_path:
487
- self.validation_results.append(ValidationResult(
488
- level="error",
489
- message=f"Protocol '{protocol}' requires {file_key} to be specified",
490
- section="ssl",
491
- key=file_key.split(".")[-1]
492
- ))
493
- elif not os.path.exists(file_path):
494
- self.validation_results.append(ValidationResult(
495
- level="error",
496
- message=f"Protocol '{protocol}' requires file '{file_path}' to exist",
497
- section="ssl",
498
- key=file_key.split(".")[-1]
499
- ))
500
-
501
- # Check client verification requirements
502
- if requirements["client_verification"]:
503
- verify_client = self._get_nested_value_safe("transport.ssl.verify_client", False)
504
- if not verify_client:
505
- self.validation_results.append(ValidationResult(
506
- level="error",
507
- message=f"Protocol '{protocol}' requires client certificate verification",
508
- section="transport.ssl",
509
- key="verify_client"
510
- ))
511
-
512
- def _validate_file_existence(self) -> None:
513
- """Validate that all referenced files exist."""
514
- protocol = self._get_nested_value_safe("server.protocol", "http")
515
-
516
- file_keys = [
517
- "logging.log_dir",
518
- "commands.commands_directory",
519
- "commands.catalog_directory",
520
- "commands.custom_commands_path",
521
- "security.roles_file",
522
- "roles.config_file"
523
- ]
524
-
525
- # Only add SSL-related files if protocol requires SSL
526
- if protocol in ["https", "mtls"]:
527
- file_keys.extend([
528
- "ssl.cert_file",
529
- "ssl.key_file",
530
- "ssl.ca_cert",
531
- "transport.ssl.cert_file",
532
- "transport.ssl.key_file",
533
- "transport.ssl.ca_cert"
534
- ])
535
-
536
- # Only add proxy registration files if proxy registration is enabled
537
- if self._has_nested_key("proxy_registration.enabled"):
538
- proxy_enabled = self._get_nested_value_safe("proxy_registration.enabled", False)
539
- if proxy_enabled:
540
- file_keys.extend([
541
- "proxy_registration.certificate.cert_file",
542
- "proxy_registration.certificate.key_file"
543
- ])
544
-
545
- for file_key in file_keys:
546
- # Skip if the key doesn't exist in the configuration
547
- if not self._has_nested_key(file_key):
548
- continue
549
-
550
- file_path = self._get_nested_value_safe(file_key)
551
- if file_path and not os.path.exists(file_path):
552
- # Check if this is a required file based on enabled features
553
- is_required = self._is_file_required_for_enabled_features(file_key)
554
- level = "error" if is_required else "warning"
555
-
556
- self.validation_results.append(ValidationResult(
557
- level=level,
558
- message=f"Referenced file '{file_path}' does not exist",
559
- section=file_key.split(".")[0],
560
- key=file_key.split(".")[-1],
561
- suggestion="Create the file or update the configuration" if is_required else "This file is optional"
562
- ))
563
-
564
- def _validate_security_consistency(self) -> None:
565
- """Validate security configuration consistency."""
566
- security_enabled = self._get_nested_value_safe("security.enabled", False)
133
+ def _validate_uuid_format(self) -> None:
134
+ """Validate UUID format in configuration."""
135
+ uuid_fields = ["server.server_id", "proxy_registration.server_id"]
567
136
 
568
- if security_enabled:
569
- # Check if authentication is properly configured
570
- tokens = self._get_nested_value_safe("security.tokens", {})
571
- roles = self._get_nested_value_safe("security.roles", {})
572
- roles_file = self._get_nested_value_safe("security.roles_file")
573
-
574
- has_tokens = bool(tokens and any(tokens.values()))
575
- has_roles = bool(roles and any(roles.values()))
576
- has_roles_file = bool(roles_file and os.path.exists(roles_file))
577
-
578
- if not (has_tokens or has_roles or has_roles_file):
579
- self.validation_results.append(ValidationResult(
580
- level="warning",
581
- message="Security is enabled but no authentication methods are configured",
582
- section="security",
583
- suggestion="Configure tokens, roles, or roles_file in the security section"
584
- ))
585
-
586
- # Check roles consistency
587
- if has_roles and has_roles_file:
137
+ for field in uuid_fields:
138
+ value = self._get_nested_value_safe(field)
139
+ if value and not self._is_valid_uuid4(str(value)):
588
140
  self.validation_results.append(ValidationResult(
589
141
  level="warning",
590
- message="Both inline roles and roles_file are configured. roles_file will take precedence",
591
- section="security",
592
- suggestion="Remove either inline roles or roles_file configuration"
593
- ))
594
-
595
- def _validate_proxy_registration(self) -> None:
596
- """Validate proxy registration configuration."""
597
- registration_enabled = self._get_nested_value_safe("proxy_registration.enabled", False)
598
-
599
- if registration_enabled:
600
- if not self._has_nested_key("proxy_registration.proxy_url"):
601
- self.validation_results.append(ValidationResult(
602
- level="error",
603
- message="Proxy registration is enabled but proxy_url is not specified",
604
- section="proxy_registration",
605
- key="proxy_url"
142
+ message=f"Invalid UUID format in {field}: {value}",
143
+ section=field.split(".")[0],
144
+ key=field.split(".")[1],
145
+ suggestion="Use a valid UUID4 format"
606
146
  ))
607
- else:
608
- proxy_url = self._get_nested_value("proxy_registration.proxy_url")
609
- if not proxy_url:
610
- self.validation_results.append(ValidationResult(
611
- level="error",
612
- message="Proxy registration is enabled but proxy_url is not specified",
613
- section="proxy_registration",
614
- key="proxy_url"
615
- ))
616
-
617
- # Check authentication method consistency
618
- auth_method = self._get_nested_value_safe("proxy_registration.auth_method", "none")
619
- if auth_method != "none":
620
- if auth_method == "certificate":
621
- if not self._has_nested_key("proxy_registration.certificate.cert_file"):
622
- self.validation_results.append(ValidationResult(
623
- level="error",
624
- message="Certificate authentication requires cert_file",
625
- section="proxy_registration.certificate",
626
- key="cert_file"
627
- ))
628
- else:
629
- cert_file = self._get_nested_value("proxy_registration.certificate.cert_file")
630
-
631
- if not self._has_nested_key("proxy_registration.certificate.key_file"):
632
- self.validation_results.append(ValidationResult(
633
- level="error",
634
- message="Certificate authentication requires key_file",
635
- section="proxy_registration.certificate",
636
- key="key_file"
637
- ))
638
- else:
639
- key_file = self._get_nested_value("proxy_registration.certificate.key_file")
640
-
641
- if not cert_file or not key_file:
642
- self.validation_results.append(ValidationResult(
643
- level="error",
644
- message="Certificate authentication is enabled but certificate files are not specified",
645
- section="proxy_registration",
646
- key="certificate"
647
- ))
648
- elif auth_method == "token":
649
- if not self._has_nested_key("proxy_registration.token.token"):
650
- self.validation_results.append(ValidationResult(
651
- level="error",
652
- message="Token authentication requires token",
653
- section="proxy_registration.token",
654
- key="token"
655
- ))
656
- else:
657
- token = self._get_nested_value("proxy_registration.token.token")
658
- if not token:
659
- self.validation_results.append(ValidationResult(
660
- level="error",
661
- message="Token authentication is enabled but token is not specified",
662
- section="proxy_registration",
663
- key="token"
664
- ))
665
-
666
- def _validate_ssl_configuration(self) -> None:
667
- """Validate SSL configuration with detailed certificate validation."""
668
- # Only validate SSL if the protocol requires it
669
- protocol = self._get_nested_value_safe("server.protocol", "http")
670
- if protocol not in ["https", "mtls"]:
671
- return
672
147
 
673
- # Only validate SSL if the ssl section exists
674
- if not self._has_nested_key("ssl.enabled"):
675
- return
148
+ def _is_valid_uuid4(self, uuid_str: str) -> bool:
149
+ """Check if string is a valid UUID4."""
150
+ import re
151
+ uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
152
+ return bool(re.match(uuid_pattern, uuid_str, re.IGNORECASE))
676
153
 
677
- ssl_enabled = self._get_nested_value_safe("ssl.enabled", False)
678
-
679
- if ssl_enabled:
680
- # Initialize variables
681
- cert_file = None
682
- key_file = None
683
- ca_cert = None
684
-
685
- if not self._has_nested_key("ssl.cert_file"):
686
- self.validation_results.append(ValidationResult(
687
- level="error",
688
- message="SSL is enabled but cert_file is not specified",
689
- section="ssl",
690
- key="cert_file"
691
- ))
692
- else:
693
- cert_file = self._get_nested_value("ssl.cert_file")
694
-
695
- if not self._has_nested_key("ssl.key_file"):
696
- self.validation_results.append(ValidationResult(
697
- level="error",
698
- message="SSL is enabled but key_file is not specified",
699
- section="ssl",
700
- key="key_file"
701
- ))
702
- else:
703
- key_file = self._get_nested_value("ssl.key_file")
704
-
705
- # CA cert is optional for HTTPS but may be required for mTLS with client verification
706
- if self._has_nested_key("ssl.ca_cert"):
707
- ca_cert = self._get_nested_value_safe("ssl.ca_cert")
708
-
709
- # Check certificate file
710
- if not cert_file:
711
- self.validation_results.append(ValidationResult(
712
- level="error",
713
- message="SSL is enabled but cert_file is not specified",
714
- section="ssl",
715
- key="cert_file"
716
- ))
717
- elif not os.path.exists(cert_file):
718
- self.validation_results.append(ValidationResult(
719
- level="error",
720
- message=f"SSL certificate file '{cert_file}' does not exist",
721
- section="ssl",
722
- key="cert_file"
723
- ))
724
- else:
725
- # Validate certificate file
726
- self._validate_certificate_file(cert_file, "ssl", "cert_file")
727
-
728
- # Check key file
729
- if not key_file:
730
- self.validation_results.append(ValidationResult(
731
- level="error",
732
- message="SSL is enabled but key_file is not specified",
733
- section="ssl",
734
- key="key_file"
735
- ))
736
- elif not os.path.exists(key_file):
737
- self.validation_results.append(ValidationResult(
738
- level="error",
739
- message=f"SSL key file '{key_file}' does not exist",
740
- section="ssl",
741
- key="key_file"
742
- ))
743
- else:
744
- # Validate key file
745
- self._validate_key_file(key_file, "ssl", "key_file")
746
-
747
- # Check CA certificate if specified
748
- if ca_cert:
749
- if not os.path.exists(ca_cert):
750
- self.validation_results.append(ValidationResult(
751
- level="error",
752
- message=f"SSL CA certificate file '{ca_cert}' does not exist",
753
- section="ssl",
754
- key="ca_cert"
755
- ))
756
- else:
757
- # Validate CA certificate
758
- self._validate_ca_certificate_file(ca_cert, "ssl", "ca_cert")
759
-
760
- # Validate certificate-key pair if both exist
761
- if cert_file and key_file and os.path.exists(cert_file) and os.path.exists(key_file):
762
- self._validate_certificate_key_pair(cert_file, key_file, ca_cert, "ssl")
763
-
764
- def _validate_roles_configuration(self) -> None:
765
- """Validate roles configuration."""
766
- roles_enabled = self._get_nested_value_safe("roles.enabled", False)
767
-
768
- if roles_enabled:
769
- if not self._has_nested_key("roles.config_file"):
770
- self.validation_results.append(ValidationResult(
771
- level="error",
772
- message="Roles are enabled but config_file is not specified",
773
- section="roles",
774
- key="config_file"
775
- ))
776
- else:
777
- config_file = self._get_nested_value("roles.config_file")
778
- if not config_file:
779
- self.validation_results.append(ValidationResult(
780
- level="error",
781
- message="Roles are enabled but config_file is not specified",
782
- section="roles",
783
- key="config_file"
784
- ))
785
- elif not os.path.exists(config_file):
786
- self.validation_results.append(ValidationResult(
787
- level="error",
788
- message=f"Roles config file '{config_file}' does not exist",
789
- section="roles",
790
- key="config_file"
791
- ))
792
-
793
- def _get_nested_value(self, key: str) -> Any:
794
- """Get value from nested dictionary using dot notation. Raises exception if key not found."""
795
- keys = key.split(".")
154
+ def _get_nested_value_safe(self, key: str, default: Any = None) -> Any:
155
+ """Safely get a nested value from configuration."""
156
+ keys = key.split('.')
796
157
  value = self.config_data
797
158
 
798
159
  for k in keys:
799
160
  if isinstance(value, dict) and k in value:
800
161
  value = value[k]
801
162
  else:
802
- raise MissingConfigKeyError(k, ".".join(keys[:keys.index(k)]))
803
-
804
- return value
805
-
806
- def _has_nested_key(self, key: str) -> bool:
807
- """Check if nested key exists in configuration."""
808
- keys = key.split(".")
809
- value = self.config_data
163
+ return default
810
164
 
811
- for k in keys:
812
- if isinstance(value, dict) and k in value:
813
- value = value[k]
814
- else:
815
- return False
816
-
817
- return True
818
-
819
- def _get_nested_value_safe(self, key: str, default: Any = None) -> Any:
820
- """Get value from nested dictionary using dot notation with fallback."""
821
- try:
822
- return self._get_nested_value(key)
823
- except MissingConfigKeyError:
824
- return default
825
-
826
- def _is_file_required_for_enabled_features(self, file_key: str) -> bool:
827
- """Check if file is required based on enabled features."""
828
- for feature_name, feature_config in self.feature_flags.items():
829
- enabled_key = feature_config["enabled_key"]
830
- is_enabled = self._get_nested_value_safe(enabled_key, False)
831
-
832
- if is_enabled and file_key in feature_config["required_files"]:
833
- return True
834
-
835
- return False
836
-
837
- def _validate_certificate_file(self, cert_file: str, section: str, key: str) -> None:
838
- """Validate certificate file format and content."""
839
- try:
840
- import cryptography
841
- from cryptography import x509
842
- from cryptography.hazmat.primitives import serialization
843
-
844
- with open(cert_file, 'rb') as f:
845
- cert_data = f.read()
846
-
847
- # Try to parse as PEM
848
- try:
849
- cert = x509.load_pem_x509_certificate(cert_data)
850
- except Exception:
851
- # Try to parse as DER
852
- try:
853
- cert = x509.load_der_x509_certificate(cert_data)
854
- except Exception as e:
855
- self.validation_results.append(ValidationResult(
856
- level="error",
857
- message=f"Certificate file '{cert_file}' is not a valid PEM or DER certificate: {e}",
858
- section=section,
859
- key=key
860
- ))
861
- return
862
-
863
- # Check certificate expiration
864
- now = datetime.now(timezone.utc)
865
- not_after = cert.not_valid_after_utc
866
-
867
- if now > not_after:
868
- self.validation_results.append(ValidationResult(
869
- level="error",
870
- message=f"Certificate '{cert_file}' has expired on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
871
- section=section,
872
- key=key
873
- ))
874
- elif (not_after - now).days < 30:
875
- self.validation_results.append(ValidationResult(
876
- level="warning",
877
- message=f"Certificate '{cert_file}' expires in {(not_after - now).days} days on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
878
- section=section,
879
- key=key,
880
- suggestion="Consider renewing the certificate"
881
- ))
882
-
883
- # Check if certificate is self-signed
884
- issuer = cert.issuer
885
- subject = cert.subject
886
-
887
- if issuer == subject:
888
- self.validation_results.append(ValidationResult(
889
- level="warning",
890
- message=f"Certificate '{cert_file}' is self-signed",
891
- section=section,
892
- key=key,
893
- suggestion="Consider using a certificate from a trusted CA for production"
894
- ))
895
-
896
- # Check certificate key usage
897
- try:
898
- key_usage = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.KEY_USAGE)
899
- if not key_usage.value.digital_signature:
900
- self.validation_results.append(ValidationResult(
901
- level="warning",
902
- message=f"Certificate '{cert_file}' does not have digital signature key usage",
903
- section=section,
904
- key=key,
905
- suggestion="Ensure the certificate supports digital signature for SSL/TLS"
906
- ))
907
- except x509.ExtensionNotFound:
908
- pass # Key usage extension not present, which is sometimes OK
909
-
910
- except ImportError:
911
- # cryptography library not available, do basic validation
912
- self.validation_results.append(ValidationResult(
913
- level="warning",
914
- message=f"Cannot validate certificate '{cert_file}' - cryptography library not available",
915
- section=section,
916
- key=key,
917
- suggestion="Install cryptography library for detailed certificate validation"
918
- ))
919
- except Exception as e:
920
- self.validation_results.append(ValidationResult(
921
- level="error",
922
- message=f"Error validating certificate '{cert_file}': {e}",
923
- section=section,
924
- key=key
925
- ))
926
-
927
- def _validate_key_file(self, key_file: str, section: str, key: str) -> None:
928
- """Validate private key file format and content."""
929
- try:
930
- import cryptography
931
- from cryptography.hazmat.primitives import serialization
932
-
933
- with open(key_file, 'rb') as f:
934
- key_data = f.read()
935
-
936
- # Try to parse as PEM
937
- try:
938
- private_key = serialization.load_pem_private_key(
939
- key_data,
940
- password=None
941
- )
942
- except Exception:
943
- # Try to parse as DER
944
- try:
945
- private_key = serialization.load_der_private_key(
946
- key_data,
947
- password=None
948
- )
949
- except Exception as e:
950
- self.validation_results.append(ValidationResult(
951
- level="error",
952
- message=f"Key file '{key_file}' is not a valid PEM or DER private key: {e}",
953
- section=section,
954
- key=key
955
- ))
956
- return
957
-
958
- # Check key size
959
- if hasattr(private_key, 'key_size'):
960
- if private_key.key_size < 2048:
961
- self.validation_results.append(ValidationResult(
962
- level="warning",
963
- message=f"Private key '{key_file}' has key size {private_key.key_size} bits, which is below recommended 2048 bits",
964
- section=section,
965
- key=key,
966
- suggestion="Consider using a key with at least 2048 bits for better security"
967
- ))
968
-
969
- except ImportError:
970
- # cryptography library not available, do basic validation
971
- self.validation_results.append(ValidationResult(
972
- level="warning",
973
- message=f"Cannot validate private key '{key_file}' - cryptography library not available",
974
- section=section,
975
- key=key,
976
- suggestion="Install cryptography library for detailed key validation"
977
- ))
978
- except Exception as e:
979
- self.validation_results.append(ValidationResult(
980
- level="error",
981
- message=f"Error validating private key '{key_file}': {e}",
982
- section=section,
983
- key=key
984
- ))
985
-
986
- def _validate_ca_certificate_file(self, ca_cert_file: str, section: str, key: str) -> None:
987
- """Validate CA certificate file."""
988
- try:
989
- import cryptography
990
- from cryptography import x509
991
-
992
- with open(ca_cert_file, 'rb') as f:
993
- ca_data = f.read()
994
-
995
- # Try to parse as PEM
996
- try:
997
- ca_cert = x509.load_pem_x509_certificate(ca_data)
998
- except Exception:
999
- # Try to parse as DER
1000
- try:
1001
- ca_cert = x509.load_der_x509_certificate(ca_data)
1002
- except Exception as e:
1003
- self.validation_results.append(ValidationResult(
1004
- level="error",
1005
- message=f"CA certificate file '{ca_cert_file}' is not a valid PEM or DER certificate: {e}",
1006
- section=section,
1007
- key=key
1008
- ))
1009
- return
1010
-
1011
- # Check CA certificate expiration
1012
- now = datetime.now(timezone.utc)
1013
- not_after = ca_cert.not_valid_after.replace(tzinfo=timezone.utc)
1014
-
1015
- if now > not_after:
1016
- self.validation_results.append(ValidationResult(
1017
- level="error",
1018
- message=f"CA certificate '{ca_cert_file}' has expired on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
1019
- section=section,
1020
- key=key
1021
- ))
1022
- elif (not_after - now).days < 30:
1023
- self.validation_results.append(ValidationResult(
1024
- level="warning",
1025
- message=f"CA certificate '{ca_cert_file}' expires in {(not_after - now).days} days on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
1026
- section=section,
1027
- key=key,
1028
- suggestion="Consider renewing the CA certificate"
1029
- ))
1030
-
1031
- # Check if CA certificate has CA basic constraint
1032
- try:
1033
- basic_constraints = ca_cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.BASIC_CONSTRAINTS)
1034
- if not basic_constraints.value.ca:
1035
- self.validation_results.append(ValidationResult(
1036
- level="warning",
1037
- message=f"CA certificate '{ca_cert_file}' does not have CA basic constraint set",
1038
- section=section,
1039
- key=key,
1040
- suggestion="Ensure the certificate is marked as a CA certificate"
1041
- ))
1042
- except x509.ExtensionNotFound:
1043
- self.validation_results.append(ValidationResult(
1044
- level="warning",
1045
- message=f"CA certificate '{ca_cert_file}' does not have basic constraints extension",
1046
- section=section,
1047
- key=key,
1048
- suggestion="Consider using a proper CA certificate with basic constraints"
1049
- ))
1050
-
1051
- except ImportError:
1052
- # cryptography library not available
1053
- self.validation_results.append(ValidationResult(
1054
- level="warning",
1055
- message=f"Cannot validate CA certificate '{ca_cert_file}' - cryptography library not available",
1056
- section=section,
1057
- key=key,
1058
- suggestion="Install cryptography library for detailed CA certificate validation"
1059
- ))
1060
- except Exception as e:
1061
- self.validation_results.append(ValidationResult(
1062
- level="error",
1063
- message=f"Error validating CA certificate '{ca_cert_file}': {e}",
1064
- section=section,
1065
- key=key
1066
- ))
1067
-
1068
- def _validate_certificate_key_pair(self, cert_file: str, key_file: str, ca_cert_file: Optional[str], section: str) -> None:
1069
- """Validate that certificate and key are a matching pair."""
1070
- try:
1071
- import cryptography
1072
- from cryptography import x509
1073
- from cryptography.hazmat.primitives import serialization, hashes
1074
- from cryptography.hazmat.primitives.asymmetric import rsa, padding
1075
-
1076
- # Load certificate
1077
- with open(cert_file, 'rb') as f:
1078
- cert_data = f.read()
1079
-
1080
- try:
1081
- cert = x509.load_pem_x509_certificate(cert_data)
1082
- except Exception:
1083
- cert = x509.load_der_x509_certificate(cert_data)
1084
-
1085
- # Load private key
1086
- with open(key_file, 'rb') as f:
1087
- key_data = f.read()
1088
-
1089
- try:
1090
- private_key = serialization.load_pem_private_key(key_data, password=None)
1091
- except Exception:
1092
- private_key = serialization.load_der_private_key(key_data, password=None)
1093
-
1094
- # Check if certificate public key matches private key
1095
- cert_public_key = cert.public_key()
1096
-
1097
- # For RSA keys, compare modulus
1098
- if isinstance(cert_public_key, rsa.RSAPublicKey) and isinstance(private_key, rsa.RSAPrivateKey):
1099
- if cert_public_key.public_numbers().n != private_key.public_key().public_numbers().n:
1100
- self.validation_results.append(ValidationResult(
1101
- level="error",
1102
- message=f"Certificate '{cert_file}' and private key '{key_file}' do not match",
1103
- section=section,
1104
- key="cert_file",
1105
- suggestion="Ensure the certificate and private key are from the same key pair"
1106
- ))
1107
- return
165
+ return value
1108
166
 
1109
- # If CA certificate is provided, validate certificate chain
1110
- if ca_cert_file and os.path.exists(ca_cert_file):
1111
- self._validate_certificate_chain(cert_file, ca_cert_file, section)
1112
-
1113
- except ImportError:
1114
- # cryptography library not available
1115
- self.validation_results.append(ValidationResult(
1116
- level="warning",
1117
- message=f"Cannot validate certificate-key pair - cryptography library not available",
1118
- section=section,
1119
- key="cert_file",
1120
- suggestion="Install cryptography library for detailed certificate validation"
1121
- ))
1122
- except Exception as e:
1123
- self.validation_results.append(ValidationResult(
1124
- level="error",
1125
- message=f"Error validating certificate-key pair: {e}",
1126
- section=section,
1127
- key="cert_file"
1128
- ))
1129
-
1130
- def _validate_certificate_chain(self, cert_file: str, ca_cert_file: str, section: str) -> None:
1131
- """Validate certificate chain against CA certificate."""
1132
- try:
1133
- import cryptography
1134
- from cryptography import x509
1135
- from cryptography.hazmat.primitives import hashes
1136
- from cryptography.hazmat.primitives.asymmetric import padding
1137
-
1138
- # Load certificate
1139
- with open(cert_file, 'rb') as f:
1140
- cert_data = f.read()
1141
-
1142
- try:
1143
- cert = x509.load_pem_x509_certificate(cert_data)
1144
- except Exception:
1145
- cert = x509.load_der_x509_certificate(cert_data)
1146
-
1147
- # Load CA certificate
1148
- with open(ca_cert_file, 'rb') as f:
1149
- ca_data = f.read()
1150
-
1151
- try:
1152
- ca_cert = x509.load_pem_x509_certificate(ca_data)
1153
- except Exception:
1154
- ca_cert = x509.load_der_x509_certificate(ca_data)
1155
-
1156
- # Verify certificate signature with CA
1157
- try:
1158
- ca_cert.public_key().verify(
1159
- cert.signature,
1160
- cert.tbs_certificate_bytes,
1161
- padding.PKCS1v15(),
1162
- cert.signature_algorithm_oid._name
1163
- )
1164
- except Exception as e:
1165
- self.validation_results.append(ValidationResult(
1166
- level="error",
1167
- message=f"Certificate '{cert_file}' is not signed by CA certificate '{ca_cert_file}': {e}",
1168
- section=section,
1169
- key="cert_file",
1170
- suggestion="Ensure the certificate is properly signed by the CA"
1171
- ))
1172
- return
1173
-
1174
- # Check if certificate issuer matches CA subject
1175
- if cert.issuer != ca_cert.subject:
1176
- self.validation_results.append(ValidationResult(
1177
- level="warning",
1178
- message=f"Certificate issuer '{cert.issuer}' does not match CA subject '{ca_cert.subject}'",
1179
- section=section,
1180
- key="cert_file",
1181
- suggestion="Verify that the certificate is issued by the correct CA"
1182
- ))
1183
-
1184
- except ImportError:
1185
- # cryptography library not available
1186
- self.validation_results.append(ValidationResult(
1187
- level="warning",
1188
- message=f"Cannot validate certificate chain - cryptography library not available",
1189
- section=section,
1190
- key="cert_file",
1191
- suggestion="Install cryptography library for detailed certificate chain validation"
1192
- ))
1193
- except Exception as e:
1194
- self.validation_results.append(ValidationResult(
1195
- level="error",
1196
- message=f"Error validating certificate chain: {e}",
1197
- section=section,
1198
- key="cert_file"
1199
- ))
1200
-
1201
- def _validate_uuid_format(self) -> None:
1202
- """Validate UUID4 format in configuration."""
1203
-
1204
- # Check root level UUID
1205
- if "uuid" in self.config_data:
1206
- uuid_value = self.config_data["uuid"]
1207
- if not self._is_valid_uuid4(uuid_value):
1208
- self.validation_results.append(ValidationResult(
1209
- level="error",
1210
- message=f"Invalid UUID4 format: '{uuid_value}'. Expected format: xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx",
1211
- section="uuid",
1212
- key="uuid"
1213
- ))
1214
-
1215
- # Check proxy_registration UUID if it exists
1216
- if self._has_nested_key("proxy_registration.uuid"):
1217
- uuid_value = self._get_nested_value("proxy_registration.uuid")
1218
- if not self._is_valid_uuid4(uuid_value):
1219
- self.validation_results.append(ValidationResult(
1220
- level="error",
1221
- message=f"Invalid UUID4 format in proxy_registration: '{uuid_value}'. Expected format: xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx",
1222
- section="proxy_registration",
1223
- key="uuid"
1224
- ))
1225
-
1226
- def _is_valid_uuid4(self, uuid_str: str) -> bool:
1227
- """Check if string is a valid UUID4."""
1228
- if not isinstance(uuid_str, str):
1229
- return False
1230
-
1231
- uuid4_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
1232
- return bool(re.match(uuid4_pattern, uuid_str, re.IGNORECASE))
1233
-
1234
- def _validate_unknown_fields(self) -> None:
1235
- """Validate that no unknown fields are present in configuration."""
1236
- # Define all known sections and their allowed fields
1237
- known_sections = {
1238
- "server": {"host", "port", "protocol", "debug", "log_level"},
1239
- "logging": {"level", "file", "log_dir", "log_file", "error_log_file", "access_log_file",
1240
- "max_file_size", "backup_count", "format", "date_format", "console_output", "file_output"},
1241
- "commands": {"auto_discovery", "commands_directory", "catalog_directory", "plugin_servers",
1242
- "auto_install_dependencies", "enabled_commands", "disabled_commands", "custom_commands_path"},
1243
- "transport": {"type", "port", "verify_client", "chk_hostname", "ssl"},
1244
- "proxy_registration": {"enabled", "proxy_url", "server_id", "server_name", "description", "version",
1245
- "registration_timeout", "retry_attempts", "retry_delay", "auto_register_on_startup",
1246
- "auto_unregister_on_shutdown", "uuid", "heartbeat"},
1247
- "debug": {"enabled", "level"},
1248
- "security": {"enabled", "tokens", "roles", "roles_file"},
1249
- "roles": {"enabled", "config_file", "default_policy", "auto_load", "validation_enabled"},
1250
- "ssl": {"enabled", "cert_file", "key_file", "ca_cert"},
1251
- "uuid": set() # Root level UUID
1252
- }
1253
-
1254
- # Check for unknown root level fields
1255
- for field in self.config_data:
1256
- if field not in known_sections:
1257
- self.validation_results.append(ValidationResult(
1258
- level="warning",
1259
- message=f"Unknown field '{field}' at root level",
1260
- section=field,
1261
- suggestion="Check if this field is needed or if it's a typo"
1262
- ))
1263
-
1264
- # Check for unknown fields in known sections
1265
- for section_name, allowed_fields in known_sections.items():
1266
- if section_name in self.config_data:
1267
- section_data = self.config_data[section_name]
1268
- if isinstance(section_data, dict):
1269
- for field in section_data:
1270
- if field not in allowed_fields:
1271
- self.validation_results.append(ValidationResult(
1272
- level="warning",
1273
- message=f"Unknown field '{field}' in section '{section_name}'",
1274
- section=section_name,
1275
- key=field,
1276
- suggestion="Check if this field is needed or if it's a typo"
1277
- ))
1278
-
1279
167
  def get_validation_summary(self) -> Dict[str, Any]:
1280
- """Get summary of validation results."""
168
+ """
169
+ Get a summary of validation results.
170
+
171
+ Returns:
172
+ Dictionary with validation summary
173
+ """
1281
174
  error_count = sum(1 for r in self.validation_results if r.level == "error")
1282
175
  warning_count = sum(1 for r in self.validation_results if r.level == "warning")
1283
176
  info_count = sum(1 for r in self.validation_results if r.level == "info")
@@ -1289,65 +182,30 @@ class ConfigValidator:
1289
182
  "info": info_count,
1290
183
  "is_valid": error_count == 0
1291
184
  }
1292
-
185
+
1293
186
  def print_validation_report(self) -> None:
1294
- """Print detailed validation report."""
187
+ """Print a formatted validation report."""
1295
188
  summary = self.get_validation_summary()
1296
189
 
1297
- print("=" * 60)
1298
- print("CONFIGURATION VALIDATION REPORT")
1299
- print("=" * 60)
190
+ print(f"\\nšŸ“‹ Configuration Validation Report")
191
+ print(f"{'=' * 40}")
1300
192
  print(f"Total issues: {summary['total_issues']}")
1301
193
  print(f"Errors: {summary['errors']}")
1302
194
  print(f"Warnings: {summary['warnings']}")
1303
195
  print(f"Info: {summary['info']}")
1304
- print(f"Configuration is valid: {summary['is_valid']}")
1305
- print("=" * 60)
196
+ print(f"Valid: {'āœ… Yes' if summary['is_valid'] else 'āŒ No'}")
1306
197
 
1307
198
  if self.validation_results:
1308
- for result in self.validation_results:
1309
- level_symbol = {
1310
- "error": "āŒ",
1311
- "warning": "āš ļø",
1312
- "info": "ā„¹ļø"
1313
- }[result.level]
1314
-
1315
- location = f"{result.section}.{result.key}" if result.key else result.section
1316
- print(f"{level_symbol} [{result.level.value.upper()}] {result.message}")
1317
- if location:
1318
- print(f" Location: {location}")
199
+ print(f"\\nšŸ“ Issues:")
200
+ for i, result in enumerate(self.validation_results, 1):
201
+ level_icon = {"error": "āŒ", "warning": "āš ļø", "info": "ā„¹ļø"}[result.level]
202
+ print(f"{i:2d}. {level_icon} {result.message}")
203
+ if result.section:
204
+ print(f" Section: {result.section}")
205
+ if result.key:
206
+ print(f" Key: {result.key}")
1319
207
  if result.suggestion:
1320
- print(f" Suggestion: {result.suggestion}")
208
+ print(f" Suggestion: {result.suggestion}")
1321
209
  print()
1322
- else:
1323
- print("āœ… No issues found in configuration!")
1324
210
 
1325
211
 
1326
- def validate_config_file(config_path: str) -> bool:
1327
- """
1328
- Validate a configuration file.
1329
-
1330
- Args:
1331
- config_path: Path to configuration file
1332
-
1333
- Returns:
1334
- True if configuration is valid, False otherwise
1335
- """
1336
- validator = ConfigValidator(config_path)
1337
- validator.load_config()
1338
- validator.validate_config()
1339
-
1340
- validator.print_validation_report()
1341
- return validator.get_validation_summary()["is_valid"]
1342
-
1343
-
1344
- if __name__ == "__main__":
1345
- import sys
1346
-
1347
- if len(sys.argv) != 2:
1348
- print("Usage: python config_validator.py <config_file>")
1349
- sys.exit(1)
1350
-
1351
- config_file = sys.argv[1]
1352
- is_valid = validate_config_file(config_file)
1353
- sys.exit(0 if is_valid else 1)