mcp-proxy-adapter 6.8.2__py3-none-any.whl → 6.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_proxy_adapter/config.py +208 -110
- mcp_proxy_adapter/core/config_validator.py +1077 -189
- mcp_proxy_adapter/core/errors.py +42 -0
- mcp_proxy_adapter/core/logging.py +16 -16
- mcp_proxy_adapter/examples/config_builder.py +192 -741
- mcp_proxy_adapter/examples/full_application/main.py +10 -2
- mcp_proxy_adapter/examples/full_application/run_mtls.py +252 -0
- mcp_proxy_adapter/examples/full_application/run_simple.py +152 -0
- mcp_proxy_adapter/examples/full_application/test_server.py +221 -0
- mcp_proxy_adapter/examples/generate_config.py +61 -8
- mcp_proxy_adapter/examples/test_config.py +47 -2
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.8.2.dist-info → mcp_proxy_adapter-6.9.1.dist-info}/METADATA +1 -1
- {mcp_proxy_adapter-6.8.2.dist-info → mcp_proxy_adapter-6.9.1.dist-info}/RECORD +17 -14
- {mcp_proxy_adapter-6.8.2.dist-info → mcp_proxy_adapter-6.9.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.8.2.dist-info → mcp_proxy_adapter-6.9.1.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.8.2.dist-info → mcp_proxy_adapter-6.9.1.dist-info}/top_level.txt +0 -0
@@ -1,218 +1,1106 @@
|
|
1
1
|
"""
|
2
|
-
Configuration
|
2
|
+
Configuration Validator for MCP Proxy Adapter
|
3
|
+
Validates configuration files and ensures all required settings are present and correct.
|
3
4
|
|
4
5
|
Author: Vasiliy Zdanovskiy
|
5
6
|
email: vasilyvz@gmail.com
|
6
|
-
|
7
|
-
This module provides strict configuration validation with support for two modes:
|
8
|
-
- startup (strict): fail-fast with SystemExit on invalid config
|
9
|
-
- reload (soft): log errors and keep previous config
|
10
7
|
"""
|
11
8
|
|
12
|
-
|
13
|
-
|
9
|
+
import json
|
14
10
|
import os
|
15
|
-
|
11
|
+
import logging
|
16
12
|
from pathlib import Path
|
17
|
-
from typing import
|
13
|
+
from typing import Dict, List, Any, Optional, Tuple, Set
|
14
|
+
from enum import Enum
|
15
|
+
from dataclasses import dataclass
|
16
|
+
from datetime import datetime, timezone
|
17
|
+
import ssl
|
18
|
+
import socket
|
18
19
|
|
19
|
-
|
20
|
+
logger = logging.getLogger(__name__)
|
20
21
|
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
class ValidationLevel(Enum):
|
24
|
+
"""Validation severity levels."""
|
25
|
+
ERROR = "error"
|
26
|
+
WARNING = "warning"
|
27
|
+
INFO = "info"
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
|
30
|
+
# Import ValidationResult from errors to avoid circular imports
|
31
|
+
from .errors import ValidationResult
|
31
32
|
|
32
33
|
|
33
34
|
class ConfigValidator:
|
34
35
|
"""
|
35
|
-
|
36
|
+
Comprehensive configuration validator for MCP Proxy Adapter.
|
37
|
+
|
38
|
+
Validates:
|
39
|
+
- Required sections and keys
|
40
|
+
- File existence for referenced files
|
41
|
+
- Feature flag dependencies
|
42
|
+
- Protocol-specific requirements
|
43
|
+
- Security configuration consistency
|
36
44
|
"""
|
37
|
-
|
38
|
-
def __init__(self,
|
39
|
-
# Optional stored config for validate_all() compatibility
|
40
|
-
self._config: Optional[Dict[str, Any]] = _config
|
41
|
-
self.errors: List[str] = []
|
42
|
-
self.warnings: List[str] = []
|
43
|
-
|
44
|
-
def validate(self, config: Optional[Dict[str, Any]] = None) -> ConfigValidationResult:
|
45
|
+
|
46
|
+
def __init__(self, config_path: Optional[str] = None):
|
45
47
|
"""
|
46
|
-
|
47
|
-
|
48
|
+
Initialize configuration validator.
|
49
|
+
|
48
50
|
Args:
|
49
|
-
|
51
|
+
config_path: Path to configuration file for validation
|
50
52
|
"""
|
51
|
-
|
52
|
-
self.
|
53
|
-
self.
|
54
|
-
|
55
|
-
#
|
56
|
-
self.
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
"
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
53
|
+
self.config_path = config_path
|
54
|
+
self.config_data: Dict[str, Any] = {}
|
55
|
+
self.validation_results: List[ValidationResult] = []
|
56
|
+
|
57
|
+
# Define required sections and their keys
|
58
|
+
self.required_sections = {
|
59
|
+
"server": {
|
60
|
+
"host": str,
|
61
|
+
"port": int,
|
62
|
+
"protocol": str,
|
63
|
+
"debug": bool,
|
64
|
+
"log_level": str
|
65
|
+
},
|
66
|
+
"logging": {
|
67
|
+
"level": str,
|
68
|
+
"log_dir": str,
|
69
|
+
"log_file": str,
|
70
|
+
"error_log_file": str,
|
71
|
+
"access_log_file": str,
|
72
|
+
"max_file_size": (str, int),
|
73
|
+
"backup_count": int,
|
74
|
+
"format": str,
|
75
|
+
"date_format": str,
|
76
|
+
"console_output": bool,
|
77
|
+
"file_output": bool
|
78
|
+
},
|
79
|
+
"commands": {
|
80
|
+
"auto_discovery": bool,
|
81
|
+
"commands_directory": str,
|
82
|
+
"catalog_directory": str,
|
83
|
+
"plugin_servers": list,
|
84
|
+
"auto_install_dependencies": bool,
|
85
|
+
"enabled_commands": list,
|
86
|
+
"disabled_commands": list,
|
87
|
+
"custom_commands_path": str
|
88
|
+
},
|
89
|
+
"transport": {
|
90
|
+
"type": str,
|
91
|
+
"port": (int, type(None)),
|
92
|
+
"verify_client": bool,
|
93
|
+
"chk_hostname": bool
|
94
|
+
},
|
95
|
+
"proxy_registration": {
|
96
|
+
"enabled": bool,
|
97
|
+
"proxy_url": str,
|
98
|
+
"server_id": str,
|
99
|
+
"server_name": str,
|
100
|
+
"description": str,
|
101
|
+
"version": str,
|
102
|
+
"registration_timeout": int,
|
103
|
+
"retry_attempts": int,
|
104
|
+
"retry_delay": int,
|
105
|
+
"auto_register_on_startup": bool,
|
106
|
+
"auto_unregister_on_shutdown": bool
|
107
|
+
},
|
108
|
+
"debug": {
|
109
|
+
"enabled": bool,
|
110
|
+
"level": str
|
111
|
+
},
|
112
|
+
"security": {
|
113
|
+
"enabled": bool,
|
114
|
+
"tokens": dict,
|
115
|
+
"roles": dict,
|
116
|
+
"roles_file": (str, type(None))
|
117
|
+
},
|
118
|
+
"roles": {
|
119
|
+
"enabled": bool,
|
120
|
+
"config_file": (str, type(None)),
|
121
|
+
"default_policy": dict,
|
122
|
+
"auto_load": bool,
|
123
|
+
"validation_enabled": bool
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
# Define feature flags and their dependencies
|
128
|
+
self.feature_flags = {
|
129
|
+
"security": {
|
130
|
+
"enabled_key": "security.enabled",
|
131
|
+
"dependencies": ["security.tokens", "security.roles"],
|
132
|
+
"required_files": ["security.roles_file"],
|
133
|
+
"optional_files": []
|
134
|
+
},
|
135
|
+
"roles": {
|
136
|
+
"enabled_key": "roles.enabled",
|
137
|
+
"dependencies": ["roles.config_file"],
|
138
|
+
"required_files": ["roles.config_file"],
|
139
|
+
"optional_files": []
|
140
|
+
},
|
141
|
+
"proxy_registration": {
|
142
|
+
"enabled_key": "proxy_registration.enabled",
|
143
|
+
"dependencies": ["proxy_registration.proxy_url"],
|
144
|
+
"required_files": [],
|
145
|
+
"optional_files": [
|
146
|
+
"proxy_registration.certificate.cert_file",
|
147
|
+
"proxy_registration.certificate.key_file"
|
148
|
+
]
|
149
|
+
},
|
150
|
+
"ssl": {
|
151
|
+
"enabled_key": "ssl.enabled",
|
152
|
+
"dependencies": ["ssl.cert_file", "ssl.key_file"],
|
153
|
+
"required_files": ["ssl.cert_file", "ssl.key_file"],
|
154
|
+
"optional_files": ["ssl.ca_cert"]
|
155
|
+
},
|
156
|
+
"transport_ssl": {
|
157
|
+
"enabled_key": "transport.ssl.enabled",
|
158
|
+
"dependencies": ["transport.ssl.cert_file", "transport.ssl.key_file"],
|
159
|
+
"required_files": ["transport.ssl.cert_file", "transport.ssl.key_file"],
|
160
|
+
"optional_files": ["transport.ssl.ca_cert"]
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
# Protocol-specific requirements
|
165
|
+
self.protocol_requirements = {
|
166
|
+
"http": {
|
167
|
+
"ssl_enabled": False,
|
168
|
+
"client_verification": False,
|
169
|
+
"required_files": []
|
170
|
+
},
|
171
|
+
"https": {
|
172
|
+
"ssl_enabled": True,
|
173
|
+
"client_verification": False,
|
174
|
+
"required_files": ["ssl.cert_file", "ssl.key_file"]
|
175
|
+
},
|
176
|
+
"mtls": {
|
177
|
+
"ssl_enabled": True,
|
178
|
+
"client_verification": True,
|
179
|
+
"required_files": ["ssl.cert_file", "ssl.key_file", "ssl.ca_cert"]
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
def load_config(self, config_path: Optional[str] = None) -> None:
|
184
|
+
"""
|
185
|
+
Load configuration from file.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
config_path: Path to configuration file
|
189
|
+
"""
|
190
|
+
if config_path:
|
191
|
+
self.config_path = config_path
|
192
|
+
|
193
|
+
if not self.config_path or not os.path.exists(self.config_path):
|
194
|
+
self.validation_results.append(ValidationResult(
|
195
|
+
level="error",
|
196
|
+
message=f"Configuration file not found: {self.config_path}",
|
197
|
+
section="config_file"
|
198
|
+
))
|
108
199
|
return
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
200
|
+
|
201
|
+
try:
|
202
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
203
|
+
self.config_data = json.load(f)
|
204
|
+
except json.JSONDecodeError as e:
|
205
|
+
self.validation_results.append(ValidationResult(
|
206
|
+
level="error",
|
207
|
+
message=f"Invalid JSON in configuration file: {e}",
|
208
|
+
section="config_file"
|
209
|
+
))
|
210
|
+
except Exception as e:
|
211
|
+
self.validation_results.append(ValidationResult(
|
212
|
+
level="error",
|
213
|
+
message=f"Error loading configuration file: {e}",
|
214
|
+
section="config_file"
|
215
|
+
))
|
216
|
+
|
217
|
+
def validate_config(self, config_data: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
|
218
|
+
"""
|
219
|
+
Validate configuration data.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
config_data: Configuration data to validate. If None, uses loaded config.
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
List of validation results
|
226
|
+
"""
|
227
|
+
if config_data is not None:
|
228
|
+
self.config_data = config_data
|
229
|
+
|
230
|
+
if not self.config_data:
|
231
|
+
self.validation_results.append(ValidationResult(
|
232
|
+
level="error",
|
233
|
+
message="No configuration data to validate",
|
234
|
+
section="config_data"
|
235
|
+
))
|
236
|
+
return self.validation_results
|
237
|
+
|
238
|
+
# Clear previous results
|
239
|
+
self.validation_results = []
|
240
|
+
|
241
|
+
# Perform all validation checks
|
242
|
+
self._validate_required_sections()
|
243
|
+
self._validate_feature_flags()
|
244
|
+
self._validate_protocol_requirements()
|
245
|
+
self._validate_file_existence()
|
246
|
+
self._validate_security_consistency()
|
247
|
+
self._validate_ssl_configuration()
|
248
|
+
self._validate_proxy_registration()
|
249
|
+
self._validate_roles_configuration()
|
250
|
+
|
251
|
+
return self.validation_results
|
252
|
+
|
253
|
+
def _validate_required_sections(self) -> None:
|
254
|
+
"""Validate that all required sections and keys are present for enabled features only."""
|
255
|
+
# Always required sections (core functionality)
|
256
|
+
always_required = {
|
257
|
+
"server": self.required_sections["server"],
|
258
|
+
"logging": self.required_sections["logging"],
|
259
|
+
"commands": self.required_sections["commands"]
|
260
|
+
}
|
261
|
+
|
262
|
+
# Check always required sections
|
263
|
+
for section_name, required_keys in always_required.items():
|
264
|
+
if section_name not in self.config_data:
|
265
|
+
self.validation_results.append(ValidationResult(
|
266
|
+
level="error",
|
267
|
+
message=f"Required section '{section_name}' is missing",
|
268
|
+
section=section_name
|
269
|
+
))
|
270
|
+
continue
|
271
|
+
|
272
|
+
section_data = self.config_data[section_name]
|
273
|
+
for key, expected_type in required_keys.items():
|
274
|
+
if key not in section_data:
|
275
|
+
self.validation_results.append(ValidationResult(
|
276
|
+
level="error",
|
277
|
+
message=f"Required key '{key}' is missing in section '{section_name}'",
|
278
|
+
section=section_name,
|
279
|
+
key=key
|
280
|
+
))
|
281
|
+
else:
|
282
|
+
# Validate type
|
283
|
+
value = section_data[key]
|
284
|
+
if isinstance(expected_type, tuple):
|
285
|
+
if not isinstance(value, expected_type):
|
286
|
+
expected_names = [t.__name__ for t in expected_type]
|
287
|
+
self.validation_results.append(ValidationResult(
|
288
|
+
level="error",
|
289
|
+
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {' or '.join(expected_names)}, got {type(value).__name__}",
|
290
|
+
section=section_name,
|
291
|
+
key=key
|
292
|
+
))
|
293
|
+
else:
|
294
|
+
if not isinstance(value, expected_type):
|
295
|
+
self.validation_results.append(ValidationResult(
|
296
|
+
level="error",
|
297
|
+
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {expected_type.__name__}, got {type(value).__name__}",
|
298
|
+
section=section_name,
|
299
|
+
key=key
|
300
|
+
))
|
301
|
+
|
302
|
+
# Check conditional sections based on feature flags
|
303
|
+
for feature_name, feature_config in self.feature_flags.items():
|
304
|
+
enabled_key = feature_config["enabled_key"]
|
305
|
+
is_enabled = self._get_nested_value(enabled_key, False)
|
306
|
+
|
307
|
+
if is_enabled and feature_name in self.required_sections:
|
308
|
+
section_name = feature_name
|
309
|
+
required_keys = self.required_sections[section_name]
|
310
|
+
|
311
|
+
if section_name not in self.config_data:
|
312
|
+
self.validation_results.append(ValidationResult(
|
313
|
+
level="error",
|
314
|
+
message=f"Required section '{section_name}' is missing for enabled feature",
|
315
|
+
section=section_name
|
316
|
+
))
|
317
|
+
continue
|
318
|
+
|
319
|
+
section_data = self.config_data[section_name]
|
320
|
+
for key, expected_type in required_keys.items():
|
321
|
+
if key not in section_data:
|
322
|
+
self.validation_results.append(ValidationResult(
|
323
|
+
level="error",
|
324
|
+
message=f"Required key '{key}' is missing in section '{section_name}' for enabled feature",
|
325
|
+
section=section_name,
|
326
|
+
key=key
|
327
|
+
))
|
328
|
+
else:
|
329
|
+
# Validate type
|
330
|
+
value = section_data[key]
|
331
|
+
if isinstance(expected_type, tuple):
|
332
|
+
if not isinstance(value, expected_type):
|
333
|
+
expected_names = [t.__name__ for t in expected_type]
|
334
|
+
self.validation_results.append(ValidationResult(
|
335
|
+
level="error",
|
336
|
+
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {' or '.join(expected_names)}, got {type(value).__name__}",
|
337
|
+
section=section_name,
|
338
|
+
key=key
|
339
|
+
))
|
340
|
+
else:
|
341
|
+
if not isinstance(value, expected_type):
|
342
|
+
self.validation_results.append(ValidationResult(
|
343
|
+
level="error",
|
344
|
+
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {expected_type.__name__}, got {type(value).__name__}",
|
345
|
+
section=section_name,
|
346
|
+
key=key
|
347
|
+
))
|
348
|
+
|
349
|
+
def _validate_feature_flags(self) -> None:
|
350
|
+
"""Validate feature flags and their dependencies."""
|
351
|
+
for feature_name, feature_config in self.feature_flags.items():
|
352
|
+
enabled_key = feature_config["enabled_key"]
|
353
|
+
is_enabled = self._get_nested_value(enabled_key, False)
|
354
|
+
|
355
|
+
if is_enabled:
|
356
|
+
# Check dependencies
|
357
|
+
for dependency in feature_config["dependencies"]:
|
358
|
+
if not self._has_nested_key(dependency):
|
359
|
+
self.validation_results.append(ValidationResult(
|
360
|
+
level="error",
|
361
|
+
message=f"Feature '{feature_name}' is enabled but required dependency '{dependency}' is missing",
|
362
|
+
section=feature_name,
|
363
|
+
key=dependency
|
364
|
+
))
|
365
|
+
|
366
|
+
# Check required files
|
367
|
+
for file_key in feature_config["required_files"]:
|
368
|
+
file_path = self._get_nested_value(file_key)
|
369
|
+
if file_path and not os.path.exists(file_path):
|
370
|
+
self.validation_results.append(ValidationResult(
|
371
|
+
level="error",
|
372
|
+
message=f"Feature '{feature_name}' is enabled but required file '{file_path}' does not exist",
|
373
|
+
section=feature_name,
|
374
|
+
key=file_key
|
375
|
+
))
|
376
|
+
else:
|
377
|
+
# Feature is disabled - check that optional files are not required
|
378
|
+
for file_key in feature_config["optional_files"]:
|
379
|
+
file_path = self._get_nested_value(file_key)
|
380
|
+
if file_path and not os.path.exists(file_path):
|
381
|
+
self.validation_results.append(ValidationResult(
|
382
|
+
level="warning",
|
383
|
+
message=f"Optional file '{file_path}' for disabled feature '{feature_name}' does not exist",
|
384
|
+
section=feature_name,
|
385
|
+
key=file_key,
|
386
|
+
suggestion="This is not an error since the feature is disabled"
|
387
|
+
))
|
388
|
+
|
389
|
+
def _validate_protocol_requirements(self) -> None:
|
390
|
+
"""Validate protocol-specific requirements."""
|
391
|
+
protocol = self._get_nested_value("server.protocol", "http")
|
392
|
+
|
393
|
+
if protocol not in self.protocol_requirements:
|
394
|
+
self.validation_results.append(ValidationResult(
|
395
|
+
level="error",
|
396
|
+
message=f"Unsupported protocol: {protocol}",
|
397
|
+
section="server",
|
398
|
+
key="protocol"
|
399
|
+
))
|
131
400
|
return
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
401
|
+
|
402
|
+
requirements = self.protocol_requirements[protocol]
|
403
|
+
|
404
|
+
# Check SSL requirements
|
405
|
+
if requirements["ssl_enabled"]:
|
406
|
+
ssl_enabled = self._get_nested_value("ssl.enabled", False)
|
407
|
+
if not ssl_enabled:
|
408
|
+
self.validation_results.append(ValidationResult(
|
409
|
+
level="error",
|
410
|
+
message=f"Protocol '{protocol}' requires SSL to be enabled",
|
411
|
+
section="ssl",
|
412
|
+
key="enabled"
|
413
|
+
))
|
414
|
+
|
415
|
+
# Check required SSL files
|
416
|
+
for file_key in requirements["required_files"]:
|
417
|
+
file_path = self._get_nested_value(file_key)
|
418
|
+
if not file_path:
|
419
|
+
self.validation_results.append(ValidationResult(
|
420
|
+
level="error",
|
421
|
+
message=f"Protocol '{protocol}' requires {file_key} to be specified",
|
422
|
+
section="ssl",
|
423
|
+
key=file_key.split(".")[-1]
|
424
|
+
))
|
425
|
+
elif not os.path.exists(file_path):
|
426
|
+
self.validation_results.append(ValidationResult(
|
427
|
+
level="error",
|
428
|
+
message=f"Protocol '{protocol}' requires file '{file_path}' to exist",
|
429
|
+
section="ssl",
|
430
|
+
key=file_key.split(".")[-1]
|
431
|
+
))
|
432
|
+
|
433
|
+
# Check client verification requirements
|
434
|
+
if requirements["client_verification"]:
|
435
|
+
verify_client = self._get_nested_value("transport.ssl.verify_client", False)
|
436
|
+
if not verify_client:
|
437
|
+
self.validation_results.append(ValidationResult(
|
438
|
+
level="error",
|
439
|
+
message=f"Protocol '{protocol}' requires client certificate verification",
|
440
|
+
section="transport.ssl",
|
441
|
+
key="verify_client"
|
442
|
+
))
|
443
|
+
|
444
|
+
def _validate_file_existence(self) -> None:
|
445
|
+
"""Validate that all referenced files exist."""
|
446
|
+
file_keys = [
|
447
|
+
"logging.log_dir",
|
448
|
+
"commands.commands_directory",
|
449
|
+
"commands.catalog_directory",
|
450
|
+
"commands.custom_commands_path",
|
451
|
+
"security.roles_file",
|
452
|
+
"roles.config_file",
|
453
|
+
"ssl.cert_file",
|
454
|
+
"ssl.key_file",
|
455
|
+
"ssl.ca_cert",
|
456
|
+
"transport.ssl.cert_file",
|
457
|
+
"transport.ssl.key_file",
|
458
|
+
"transport.ssl.ca_cert",
|
459
|
+
"proxy_registration.certificate.cert_file",
|
460
|
+
"proxy_registration.certificate.key_file"
|
461
|
+
]
|
462
|
+
|
463
|
+
for file_key in file_keys:
|
464
|
+
file_path = self._get_nested_value(file_key)
|
465
|
+
if file_path and not os.path.exists(file_path):
|
466
|
+
# Check if this is a required file based on enabled features
|
467
|
+
is_required = self._is_file_required_for_enabled_features(file_key)
|
468
|
+
level = "error" if is_required else "warning"
|
469
|
+
|
470
|
+
self.validation_results.append(ValidationResult(
|
471
|
+
level=level,
|
472
|
+
message=f"Referenced file '{file_path}' does not exist",
|
473
|
+
section=file_key.split(".")[0],
|
474
|
+
key=file_key.split(".")[-1],
|
475
|
+
suggestion="Create the file or update the configuration" if is_required else "This file is optional"
|
476
|
+
))
|
477
|
+
|
478
|
+
def _validate_security_consistency(self) -> None:
|
479
|
+
"""Validate security configuration consistency."""
|
480
|
+
security_enabled = self._get_nested_value("security.enabled", False)
|
481
|
+
|
482
|
+
if security_enabled:
|
483
|
+
# Check if authentication is properly configured
|
484
|
+
tokens = self._get_nested_value("security.tokens", {})
|
485
|
+
roles = self._get_nested_value("security.roles", {})
|
486
|
+
roles_file = self._get_nested_value("security.roles_file")
|
487
|
+
|
488
|
+
has_tokens = bool(tokens and any(tokens.values()))
|
489
|
+
has_roles = bool(roles and any(roles.values()))
|
490
|
+
has_roles_file = bool(roles_file and os.path.exists(roles_file))
|
491
|
+
|
492
|
+
if not (has_tokens or has_roles or has_roles_file):
|
493
|
+
self.validation_results.append(ValidationResult(
|
494
|
+
level="warning",
|
495
|
+
message="Security is enabled but no authentication methods are configured",
|
496
|
+
section="security",
|
497
|
+
suggestion="Configure tokens, roles, or roles_file in the security section"
|
498
|
+
))
|
499
|
+
|
500
|
+
# Check roles consistency
|
501
|
+
if has_roles and has_roles_file:
|
502
|
+
self.validation_results.append(ValidationResult(
|
503
|
+
level="warning",
|
504
|
+
message="Both inline roles and roles_file are configured. roles_file will take precedence",
|
505
|
+
section="security",
|
506
|
+
suggestion="Remove either inline roles or roles_file configuration"
|
507
|
+
))
|
508
|
+
|
509
|
+
def _validate_proxy_registration(self) -> None:
|
510
|
+
"""Validate proxy registration configuration."""
|
511
|
+
registration_enabled = self._get_nested_value("proxy_registration.enabled", False)
|
512
|
+
|
513
|
+
if registration_enabled:
|
514
|
+
proxy_url = self._get_nested_value("proxy_registration.proxy_url")
|
515
|
+
if not proxy_url:
|
516
|
+
self.validation_results.append(ValidationResult(
|
517
|
+
level="error",
|
518
|
+
message="Proxy registration is enabled but proxy_url is not specified",
|
519
|
+
section="proxy_registration",
|
520
|
+
key="proxy_url"
|
521
|
+
))
|
522
|
+
|
523
|
+
# Check authentication method consistency
|
524
|
+
auth_method = self._get_nested_value("proxy_registration.auth_method", "none")
|
525
|
+
if auth_method != "none":
|
526
|
+
if auth_method == "certificate":
|
527
|
+
cert_file = self._get_nested_value("proxy_registration.certificate.cert_file")
|
528
|
+
key_file = self._get_nested_value("proxy_registration.certificate.key_file")
|
529
|
+
if not cert_file or not key_file:
|
530
|
+
self.validation_results.append(ValidationResult(
|
531
|
+
level="error",
|
532
|
+
message="Certificate authentication is enabled but certificate files are not specified",
|
533
|
+
section="proxy_registration",
|
534
|
+
key="certificate"
|
535
|
+
))
|
536
|
+
elif auth_method == "token":
|
537
|
+
token = self._get_nested_value("proxy_registration.token.token")
|
538
|
+
if not token:
|
539
|
+
self.validation_results.append(ValidationResult(
|
540
|
+
level="error",
|
541
|
+
message="Token authentication is enabled but token is not specified",
|
542
|
+
section="proxy_registration",
|
543
|
+
key="token"
|
544
|
+
))
|
545
|
+
|
546
|
+
def _validate_ssl_configuration(self) -> None:
|
547
|
+
"""Validate SSL configuration with detailed certificate validation."""
|
548
|
+
ssl_enabled = self._get_nested_value("ssl.enabled", False)
|
549
|
+
|
550
|
+
if ssl_enabled:
|
551
|
+
cert_file = self._get_nested_value("ssl.cert_file")
|
552
|
+
key_file = self._get_nested_value("ssl.key_file")
|
553
|
+
ca_cert = self._get_nested_value("ssl.ca_cert")
|
554
|
+
|
555
|
+
# Check certificate file
|
556
|
+
if not cert_file:
|
557
|
+
self.validation_results.append(ValidationResult(
|
558
|
+
level="error",
|
559
|
+
message="SSL is enabled but cert_file is not specified",
|
560
|
+
section="ssl",
|
561
|
+
key="cert_file"
|
562
|
+
))
|
563
|
+
elif not os.path.exists(cert_file):
|
564
|
+
self.validation_results.append(ValidationResult(
|
565
|
+
level="error",
|
566
|
+
message=f"SSL certificate file '{cert_file}' does not exist",
|
567
|
+
section="ssl",
|
568
|
+
key="cert_file"
|
569
|
+
))
|
140
570
|
else:
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
571
|
+
# Validate certificate file
|
572
|
+
self._validate_certificate_file(cert_file, "ssl", "cert_file")
|
573
|
+
|
574
|
+
# Check key file
|
575
|
+
if not key_file:
|
576
|
+
self.validation_results.append(ValidationResult(
|
577
|
+
level="error",
|
578
|
+
message="SSL is enabled but key_file is not specified",
|
579
|
+
section="ssl",
|
580
|
+
key="key_file"
|
581
|
+
))
|
582
|
+
elif not os.path.exists(key_file):
|
583
|
+
self.validation_results.append(ValidationResult(
|
584
|
+
level="error",
|
585
|
+
message=f"SSL key file '{key_file}' does not exist",
|
586
|
+
section="ssl",
|
587
|
+
key="key_file"
|
588
|
+
))
|
589
|
+
else:
|
590
|
+
# Validate key file
|
591
|
+
self._validate_key_file(key_file, "ssl", "key_file")
|
592
|
+
|
593
|
+
# Check CA certificate if specified
|
594
|
+
if ca_cert:
|
595
|
+
if not os.path.exists(ca_cert):
|
596
|
+
self.validation_results.append(ValidationResult(
|
597
|
+
level="error",
|
598
|
+
message=f"SSL CA certificate file '{ca_cert}' does not exist",
|
599
|
+
section="ssl",
|
600
|
+
key="ca_cert"
|
601
|
+
))
|
602
|
+
else:
|
603
|
+
# Validate CA certificate
|
604
|
+
self._validate_ca_certificate_file(ca_cert, "ssl", "ca_cert")
|
605
|
+
|
606
|
+
# Validate certificate-key pair if both exist
|
607
|
+
if cert_file and key_file and os.path.exists(cert_file) and os.path.exists(key_file):
|
608
|
+
self._validate_certificate_key_pair(cert_file, key_file, ca_cert, "ssl")
|
609
|
+
|
610
|
+
def _validate_roles_configuration(self) -> None:
|
611
|
+
"""Validate roles configuration."""
|
612
|
+
roles_enabled = self._get_nested_value("roles.enabled", False)
|
613
|
+
|
614
|
+
if roles_enabled:
|
615
|
+
config_file = self._get_nested_value("roles.config_file")
|
616
|
+
if not config_file:
|
617
|
+
self.validation_results.append(ValidationResult(
|
618
|
+
level="error",
|
619
|
+
message="Roles are enabled but config_file is not specified",
|
620
|
+
section="roles",
|
621
|
+
key="config_file"
|
622
|
+
))
|
623
|
+
elif not os.path.exists(config_file):
|
624
|
+
self.validation_results.append(ValidationResult(
|
625
|
+
level="error",
|
626
|
+
message=f"Roles config file '{config_file}' does not exist",
|
627
|
+
section="roles",
|
628
|
+
key="config_file"
|
629
|
+
))
|
630
|
+
|
631
|
+
def _get_nested_value(self, key: str, default: Any = None) -> Any:
|
632
|
+
"""Get value from nested dictionary using dot notation."""
|
633
|
+
keys = key.split(".")
|
634
|
+
value = self.config_data
|
635
|
+
|
636
|
+
for k in keys:
|
637
|
+
if isinstance(value, dict) and k in value:
|
638
|
+
value = value[k]
|
639
|
+
else:
|
640
|
+
return default
|
641
|
+
|
642
|
+
return value
|
643
|
+
|
644
|
+
def _has_nested_key(self, key: str) -> bool:
|
645
|
+
"""Check if nested key exists in configuration."""
|
646
|
+
keys = key.split(".")
|
647
|
+
value = self.config_data
|
648
|
+
|
649
|
+
for k in keys:
|
650
|
+
if isinstance(value, dict) and k in value:
|
651
|
+
value = value[k]
|
652
|
+
else:
|
653
|
+
return False
|
654
|
+
|
655
|
+
return True
|
656
|
+
|
657
|
+
def _is_file_required_for_enabled_features(self, file_key: str) -> bool:
|
658
|
+
"""Check if file is required based on enabled features."""
|
659
|
+
for feature_name, feature_config in self.feature_flags.items():
|
660
|
+
enabled_key = feature_config["enabled_key"]
|
661
|
+
is_enabled = self._get_nested_value(enabled_key, False)
|
662
|
+
|
663
|
+
if is_enabled and file_key in feature_config["required_files"]:
|
664
|
+
return True
|
665
|
+
|
666
|
+
return False
|
667
|
+
|
668
|
+
def _validate_certificate_file(self, cert_file: str, section: str, key: str) -> None:
|
669
|
+
"""Validate certificate file format and content."""
|
670
|
+
try:
|
671
|
+
import cryptography
|
672
|
+
from cryptography import x509
|
673
|
+
from cryptography.hazmat.primitives import serialization
|
674
|
+
|
675
|
+
with open(cert_file, 'rb') as f:
|
676
|
+
cert_data = f.read()
|
677
|
+
|
678
|
+
# Try to parse as PEM
|
679
|
+
try:
|
680
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
681
|
+
except Exception:
|
682
|
+
# Try to parse as DER
|
683
|
+
try:
|
684
|
+
cert = x509.load_der_x509_certificate(cert_data)
|
685
|
+
except Exception as e:
|
686
|
+
self.validation_results.append(ValidationResult(
|
687
|
+
level="error",
|
688
|
+
message=f"Certificate file '{cert_file}' is not a valid PEM or DER certificate: {e}",
|
689
|
+
section=section,
|
690
|
+
key=key
|
691
|
+
))
|
692
|
+
return
|
693
|
+
|
694
|
+
# Check certificate expiration
|
695
|
+
now = datetime.now(timezone.utc)
|
696
|
+
not_after = cert.not_valid_after.replace(tzinfo=timezone.utc)
|
697
|
+
|
698
|
+
if now > not_after:
|
699
|
+
self.validation_results.append(ValidationResult(
|
700
|
+
level="error",
|
701
|
+
message=f"Certificate '{cert_file}' has expired on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
702
|
+
section=section,
|
703
|
+
key=key
|
704
|
+
))
|
705
|
+
elif (not_after - now).days < 30:
|
706
|
+
self.validation_results.append(ValidationResult(
|
707
|
+
level="warning",
|
708
|
+
message=f"Certificate '{cert_file}' expires in {(not_after - now).days} days on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
709
|
+
section=section,
|
710
|
+
key=key,
|
711
|
+
suggestion="Consider renewing the certificate"
|
712
|
+
))
|
713
|
+
|
714
|
+
# Check if certificate is self-signed
|
715
|
+
issuer = cert.issuer
|
716
|
+
subject = cert.subject
|
717
|
+
|
718
|
+
if issuer == subject:
|
719
|
+
self.validation_results.append(ValidationResult(
|
720
|
+
level="warning",
|
721
|
+
message=f"Certificate '{cert_file}' is self-signed",
|
722
|
+
section=section,
|
723
|
+
key=key,
|
724
|
+
suggestion="Consider using a certificate from a trusted CA for production"
|
725
|
+
))
|
726
|
+
|
727
|
+
# Check certificate key usage
|
728
|
+
try:
|
729
|
+
key_usage = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.KEY_USAGE)
|
730
|
+
if not key_usage.value.digital_signature:
|
731
|
+
self.validation_results.append(ValidationResult(
|
732
|
+
level="warning",
|
733
|
+
message=f"Certificate '{cert_file}' does not have digital signature key usage",
|
734
|
+
section=section,
|
735
|
+
key=key,
|
736
|
+
suggestion="Ensure the certificate supports digital signature for SSL/TLS"
|
737
|
+
))
|
738
|
+
except x509.ExtensionNotFound:
|
739
|
+
pass # Key usage extension not present, which is sometimes OK
|
740
|
+
|
741
|
+
except ImportError:
|
742
|
+
# cryptography library not available, do basic validation
|
743
|
+
self.validation_results.append(ValidationResult(
|
744
|
+
level="warning",
|
745
|
+
message=f"Cannot validate certificate '{cert_file}' - cryptography library not available",
|
746
|
+
section=section,
|
747
|
+
key=key,
|
748
|
+
suggestion="Install cryptography library for detailed certificate validation"
|
749
|
+
))
|
750
|
+
except Exception as e:
|
751
|
+
self.validation_results.append(ValidationResult(
|
752
|
+
level="error",
|
753
|
+
message=f"Error validating certificate '{cert_file}': {e}",
|
754
|
+
section=section,
|
755
|
+
key=key
|
756
|
+
))
|
757
|
+
|
758
|
+
def _validate_key_file(self, key_file: str, section: str, key: str) -> None:
|
759
|
+
"""Validate private key file format and content."""
|
760
|
+
try:
|
761
|
+
import cryptography
|
762
|
+
from cryptography.hazmat.primitives import serialization
|
763
|
+
|
764
|
+
with open(key_file, 'rb') as f:
|
765
|
+
key_data = f.read()
|
766
|
+
|
767
|
+
# Try to parse as PEM
|
768
|
+
try:
|
769
|
+
private_key = serialization.load_pem_private_key(
|
770
|
+
key_data,
|
771
|
+
password=None
|
772
|
+
)
|
773
|
+
except Exception:
|
774
|
+
# Try to parse as DER
|
775
|
+
try:
|
776
|
+
private_key = serialization.load_der_private_key(
|
777
|
+
key_data,
|
778
|
+
password=None
|
158
779
|
)
|
159
|
-
|
160
|
-
self.
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
780
|
+
except Exception as e:
|
781
|
+
self.validation_results.append(ValidationResult(
|
782
|
+
level="error",
|
783
|
+
message=f"Key file '{key_file}' is not a valid PEM or DER private key: {e}",
|
784
|
+
section=section,
|
785
|
+
key=key
|
786
|
+
))
|
787
|
+
return
|
788
|
+
|
789
|
+
# Check key size
|
790
|
+
if hasattr(private_key, 'key_size'):
|
791
|
+
if private_key.key_size < 2048:
|
792
|
+
self.validation_results.append(ValidationResult(
|
793
|
+
level="warning",
|
794
|
+
message=f"Private key '{key_file}' has key size {private_key.key_size} bits, which is below recommended 2048 bits",
|
795
|
+
section=section,
|
796
|
+
key=key,
|
797
|
+
suggestion="Consider using a key with at least 2048 bits for better security"
|
798
|
+
))
|
799
|
+
|
800
|
+
except ImportError:
|
801
|
+
# cryptography library not available, do basic validation
|
802
|
+
self.validation_results.append(ValidationResult(
|
803
|
+
level="warning",
|
804
|
+
message=f"Cannot validate private key '{key_file}' - cryptography library not available",
|
805
|
+
section=section,
|
806
|
+
key=key,
|
807
|
+
suggestion="Install cryptography library for detailed key validation"
|
808
|
+
))
|
809
|
+
except Exception as e:
|
810
|
+
self.validation_results.append(ValidationResult(
|
811
|
+
level="error",
|
812
|
+
message=f"Error validating private key '{key_file}': {e}",
|
813
|
+
section=section,
|
814
|
+
key=key
|
815
|
+
))
|
816
|
+
|
817
|
+
def _validate_ca_certificate_file(self, ca_cert_file: str, section: str, key: str) -> None:
|
818
|
+
"""Validate CA certificate file."""
|
819
|
+
try:
|
820
|
+
import cryptography
|
821
|
+
from cryptography import x509
|
822
|
+
|
823
|
+
with open(ca_cert_file, 'rb') as f:
|
824
|
+
ca_data = f.read()
|
825
|
+
|
826
|
+
# Try to parse as PEM
|
827
|
+
try:
|
828
|
+
ca_cert = x509.load_pem_x509_certificate(ca_data)
|
829
|
+
except Exception:
|
830
|
+
# Try to parse as DER
|
831
|
+
try:
|
832
|
+
ca_cert = x509.load_der_x509_certificate(ca_data)
|
833
|
+
except Exception as e:
|
834
|
+
self.validation_results.append(ValidationResult(
|
835
|
+
level="error",
|
836
|
+
message=f"CA certificate file '{ca_cert_file}' is not a valid PEM or DER certificate: {e}",
|
837
|
+
section=section,
|
838
|
+
key=key
|
839
|
+
))
|
840
|
+
return
|
841
|
+
|
842
|
+
# Check CA certificate expiration
|
843
|
+
now = datetime.now(timezone.utc)
|
844
|
+
not_after = ca_cert.not_valid_after.replace(tzinfo=timezone.utc)
|
845
|
+
|
846
|
+
if now > not_after:
|
847
|
+
self.validation_results.append(ValidationResult(
|
848
|
+
level="error",
|
849
|
+
message=f"CA certificate '{ca_cert_file}' has expired on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
850
|
+
section=section,
|
851
|
+
key=key
|
852
|
+
))
|
853
|
+
elif (not_after - now).days < 30:
|
854
|
+
self.validation_results.append(ValidationResult(
|
855
|
+
level="warning",
|
856
|
+
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')}",
|
857
|
+
section=section,
|
858
|
+
key=key,
|
859
|
+
suggestion="Consider renewing the CA certificate"
|
860
|
+
))
|
861
|
+
|
862
|
+
# Check if CA certificate has CA basic constraint
|
863
|
+
try:
|
864
|
+
basic_constraints = ca_cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.BASIC_CONSTRAINTS)
|
865
|
+
if not basic_constraints.value.ca:
|
866
|
+
self.validation_results.append(ValidationResult(
|
867
|
+
level="warning",
|
868
|
+
message=f"CA certificate '{ca_cert_file}' does not have CA basic constraint set",
|
869
|
+
section=section,
|
870
|
+
key=key,
|
871
|
+
suggestion="Ensure the certificate is marked as a CA certificate"
|
872
|
+
))
|
873
|
+
except x509.ExtensionNotFound:
|
874
|
+
self.validation_results.append(ValidationResult(
|
875
|
+
level="warning",
|
876
|
+
message=f"CA certificate '{ca_cert_file}' does not have basic constraints extension",
|
877
|
+
section=section,
|
878
|
+
key=key,
|
879
|
+
suggestion="Consider using a proper CA certificate with basic constraints"
|
880
|
+
))
|
881
|
+
|
882
|
+
except ImportError:
|
883
|
+
# cryptography library not available
|
884
|
+
self.validation_results.append(ValidationResult(
|
885
|
+
level="warning",
|
886
|
+
message=f"Cannot validate CA certificate '{ca_cert_file}' - cryptography library not available",
|
887
|
+
section=section,
|
888
|
+
key=key,
|
889
|
+
suggestion="Install cryptography library for detailed CA certificate validation"
|
890
|
+
))
|
891
|
+
except Exception as e:
|
892
|
+
self.validation_results.append(ValidationResult(
|
893
|
+
level="error",
|
894
|
+
message=f"Error validating CA certificate '{ca_cert_file}': {e}",
|
895
|
+
section=section,
|
896
|
+
key=key
|
897
|
+
))
|
898
|
+
|
899
|
+
def _validate_certificate_key_pair(self, cert_file: str, key_file: str, ca_cert_file: Optional[str], section: str) -> None:
|
900
|
+
"""Validate that certificate and key are a matching pair."""
|
901
|
+
try:
|
902
|
+
import cryptography
|
903
|
+
from cryptography import x509
|
904
|
+
from cryptography.hazmat.primitives import serialization, hashes
|
905
|
+
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
906
|
+
|
907
|
+
# Load certificate
|
908
|
+
with open(cert_file, 'rb') as f:
|
909
|
+
cert_data = f.read()
|
910
|
+
|
911
|
+
try:
|
912
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
913
|
+
except Exception:
|
914
|
+
cert = x509.load_der_x509_certificate(cert_data)
|
915
|
+
|
916
|
+
# Load private key
|
917
|
+
with open(key_file, 'rb') as f:
|
918
|
+
key_data = f.read()
|
919
|
+
|
920
|
+
try:
|
921
|
+
private_key = serialization.load_pem_private_key(key_data, password=None)
|
922
|
+
except Exception:
|
923
|
+
private_key = serialization.load_der_private_key(key_data, password=None)
|
924
|
+
|
925
|
+
# Check if certificate public key matches private key
|
926
|
+
cert_public_key = cert.public_key()
|
927
|
+
|
928
|
+
# For RSA keys, compare modulus
|
929
|
+
if isinstance(cert_public_key, rsa.RSAPublicKey) and isinstance(private_key, rsa.RSAPrivateKey):
|
930
|
+
if cert_public_key.public_numbers().n != private_key.public_key().public_numbers().n:
|
931
|
+
self.validation_results.append(ValidationResult(
|
932
|
+
level="error",
|
933
|
+
message=f"Certificate '{cert_file}' and private key '{key_file}' do not match",
|
934
|
+
section=section,
|
935
|
+
key="cert_file",
|
936
|
+
suggestion="Ensure the certificate and private key are from the same key pair"
|
937
|
+
))
|
938
|
+
return
|
939
|
+
|
940
|
+
# If CA certificate is provided, validate certificate chain
|
941
|
+
if ca_cert_file and os.path.exists(ca_cert_file):
|
942
|
+
self._validate_certificate_chain(cert_file, ca_cert_file, section)
|
943
|
+
|
944
|
+
except ImportError:
|
945
|
+
# cryptography library not available
|
946
|
+
self.validation_results.append(ValidationResult(
|
947
|
+
level="warning",
|
948
|
+
message=f"Cannot validate certificate-key pair - cryptography library not available",
|
949
|
+
section=section,
|
950
|
+
key="cert_file",
|
951
|
+
suggestion="Install cryptography library for detailed certificate validation"
|
952
|
+
))
|
953
|
+
except Exception as e:
|
954
|
+
self.validation_results.append(ValidationResult(
|
955
|
+
level="error",
|
956
|
+
message=f"Error validating certificate-key pair: {e}",
|
957
|
+
section=section,
|
958
|
+
key=key
|
959
|
+
))
|
960
|
+
|
961
|
+
def _validate_certificate_chain(self, cert_file: str, ca_cert_file: str, section: str) -> None:
|
962
|
+
"""Validate certificate chain against CA certificate."""
|
963
|
+
try:
|
964
|
+
import cryptography
|
965
|
+
from cryptography import x509
|
966
|
+
from cryptography.hazmat.primitives import hashes
|
967
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
968
|
+
|
969
|
+
# Load certificate
|
970
|
+
with open(cert_file, 'rb') as f:
|
971
|
+
cert_data = f.read()
|
972
|
+
|
973
|
+
try:
|
974
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
975
|
+
except Exception:
|
976
|
+
cert = x509.load_der_x509_certificate(cert_data)
|
977
|
+
|
978
|
+
# Load CA certificate
|
979
|
+
with open(ca_cert_file, 'rb') as f:
|
980
|
+
ca_data = f.read()
|
981
|
+
|
982
|
+
try:
|
983
|
+
ca_cert = x509.load_pem_x509_certificate(ca_data)
|
984
|
+
except Exception:
|
985
|
+
ca_cert = x509.load_der_x509_certificate(ca_data)
|
986
|
+
|
987
|
+
# Verify certificate signature with CA
|
988
|
+
try:
|
989
|
+
ca_cert.public_key().verify(
|
990
|
+
cert.signature,
|
991
|
+
cert.tbs_certificate_bytes,
|
992
|
+
padding.PKCS1v15(),
|
993
|
+
cert.signature_algorithm_oid._name
|
168
994
|
)
|
995
|
+
except Exception as e:
|
996
|
+
self.validation_results.append(ValidationResult(
|
997
|
+
level="error",
|
998
|
+
message=f"Certificate '{cert_file}' is not signed by CA certificate '{ca_cert_file}': {e}",
|
999
|
+
section=section,
|
1000
|
+
key="cert_file",
|
1001
|
+
suggestion="Ensure the certificate is properly signed by the CA"
|
1002
|
+
))
|
1003
|
+
return
|
1004
|
+
|
1005
|
+
# Check if certificate issuer matches CA subject
|
1006
|
+
if cert.issuer != ca_cert.subject:
|
1007
|
+
self.validation_results.append(ValidationResult(
|
1008
|
+
level="warning",
|
1009
|
+
message=f"Certificate issuer '{cert.issuer}' does not match CA subject '{ca_cert.subject}'",
|
1010
|
+
section=section,
|
1011
|
+
key="cert_file",
|
1012
|
+
suggestion="Verify that the certificate is issued by the correct CA"
|
1013
|
+
))
|
1014
|
+
|
1015
|
+
except ImportError:
|
1016
|
+
# cryptography library not available
|
1017
|
+
self.validation_results.append(ValidationResult(
|
1018
|
+
level="warning",
|
1019
|
+
message=f"Cannot validate certificate chain - cryptography library not available",
|
1020
|
+
section=section,
|
1021
|
+
key="cert_file",
|
1022
|
+
suggestion="Install cryptography library for detailed certificate chain validation"
|
1023
|
+
))
|
1024
|
+
except Exception as e:
|
1025
|
+
self.validation_results.append(ValidationResult(
|
1026
|
+
level="error",
|
1027
|
+
message=f"Error validating certificate chain: {e}",
|
1028
|
+
section=section,
|
1029
|
+
key="cert_file"
|
1030
|
+
))
|
1031
|
+
|
1032
|
+
def get_validation_summary(self) -> Dict[str, Any]:
|
1033
|
+
"""Get summary of validation results."""
|
1034
|
+
error_count = sum(1 for r in self.validation_results if r.level == "error")
|
1035
|
+
warning_count = sum(1 for r in self.validation_results if r.level == "warning")
|
1036
|
+
info_count = sum(1 for r in self.validation_results if r.level == "info")
|
1037
|
+
|
1038
|
+
return {
|
1039
|
+
"total_issues": len(self.validation_results),
|
1040
|
+
"errors": error_count,
|
1041
|
+
"warnings": warning_count,
|
1042
|
+
"info": info_count,
|
1043
|
+
"is_valid": error_count == 0
|
1044
|
+
}
|
1045
|
+
|
1046
|
+
def print_validation_report(self) -> None:
|
1047
|
+
"""Print detailed validation report."""
|
1048
|
+
summary = self.get_validation_summary()
|
1049
|
+
|
1050
|
+
print("=" * 60)
|
1051
|
+
print("CONFIGURATION VALIDATION REPORT")
|
1052
|
+
print("=" * 60)
|
1053
|
+
print(f"Total issues: {summary['total_issues']}")
|
1054
|
+
print(f"Errors: {summary['errors']}")
|
1055
|
+
print(f"Warnings: {summary['warnings']}")
|
1056
|
+
print(f"Info: {summary['info']}")
|
1057
|
+
print(f"Configuration is valid: {summary['is_valid']}")
|
1058
|
+
print("=" * 60)
|
1059
|
+
|
1060
|
+
if self.validation_results:
|
1061
|
+
for result in self.validation_results:
|
1062
|
+
level_symbol = {
|
1063
|
+
"error": "❌",
|
1064
|
+
"warning": "⚠️",
|
1065
|
+
"info": "ℹ️"
|
1066
|
+
}[result.level]
|
1067
|
+
|
1068
|
+
location = f"{result.section}.{result.key}" if result.key else result.section
|
1069
|
+
print(f"{level_symbol} [{result.level.value.upper()}] {result.message}")
|
1070
|
+
if location:
|
1071
|
+
print(f" Location: {location}")
|
1072
|
+
if result.suggestion:
|
1073
|
+
print(f" Suggestion: {result.suggestion}")
|
1074
|
+
print()
|
1075
|
+
else:
|
1076
|
+
print("✅ No issues found in configuration!")
|
169
1077
|
|
170
|
-
# Conflict check
|
171
|
-
if security_ssl.get("enabled") and root_ssl.get("enabled"):
|
172
|
-
self.warnings.append(
|
173
|
-
"SSL configured in both security.ssl and root ssl; security.ssl is preferred"
|
174
|
-
)
|
175
|
-
|
176
|
-
def _validate_security_auth(self, config: Dict[str, Any]) -> None:
|
177
|
-
sec = config.get("security", {}) if isinstance(config.get("security"), dict) else {}
|
178
|
-
auth = sec.get("auth", {}) if isinstance(sec.get("auth"), dict) else {}
|
179
|
-
|
180
|
-
if not sec.get("enabled", False) or not auth.get("enabled", False):
|
181
|
-
return
|
182
|
-
|
183
|
-
methods = auth.get("methods", [])
|
184
|
-
if not isinstance(methods, list):
|
185
|
-
self.errors.append("security.auth.methods must be a list")
|
186
|
-
return
|
187
1078
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
1079
|
+
def validate_config_file(config_path: str) -> bool:
|
1080
|
+
"""
|
1081
|
+
Validate a configuration file.
|
1082
|
+
|
1083
|
+
Args:
|
1084
|
+
config_path: Path to configuration file
|
1085
|
+
|
1086
|
+
Returns:
|
1087
|
+
True if configuration is valid, False otherwise
|
1088
|
+
"""
|
1089
|
+
validator = ConfigValidator(config_path)
|
1090
|
+
validator.load_config()
|
1091
|
+
validator.validate_config()
|
1092
|
+
|
1093
|
+
validator.print_validation_report()
|
1094
|
+
return validator.get_validation_summary()["is_valid"]
|
201
1095
|
|
202
|
-
def _validate_permissions(self, config: Dict[str, Any]) -> None:
|
203
|
-
sec = config.get("security", {}) if isinstance(config.get("security"), dict) else {}
|
204
|
-
perm = sec.get("permissions", {}) if isinstance(sec.get("permissions"), dict) else {}
|
205
|
-
roles = perm.get("roles_file")
|
206
|
-
if perm.get("enabled", False) and roles:
|
207
|
-
if not Path(roles).exists():
|
208
|
-
self.errors.append(f"Permissions enabled but roles file not found: {roles}")
|
209
1096
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
1097
|
+
if __name__ == "__main__":
|
1098
|
+
import sys
|
1099
|
+
|
1100
|
+
if len(sys.argv) != 2:
|
1101
|
+
print("Usage: python config_validator.py <config_file>")
|
1102
|
+
sys.exit(1)
|
1103
|
+
|
1104
|
+
config_file = sys.argv[1]
|
1105
|
+
is_valid = validate_config_file(config_file)
|
1106
|
+
sys.exit(0 if is_valid else 1)
|