mcp-proxy-adapter 6.9.43__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/__init__.py +47 -0
- mcp_proxy_adapter/__main__.py +13 -0
- mcp_proxy_adapter/api/__init__.py +0 -0
- mcp_proxy_adapter/api/app.py +66 -0
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +355 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_context.py +356 -0
- mcp_proxy_adapter/api/core/registration_manager.py +266 -0
- mcp_proxy_adapter/api/core/registration_tasks.py +84 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +181 -0
- mcp_proxy_adapter/api/middleware/__init__.py +21 -0
- mcp_proxy_adapter/api/middleware/base.py +54 -0
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +73 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +76 -0
- mcp_proxy_adapter/api/middleware/factory.py +147 -0
- mcp_proxy_adapter/api/middleware/logging.py +31 -0
- mcp_proxy_adapter/api/middleware/performance.py +51 -0
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +140 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +87 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +223 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +132 -0
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +270 -0
- mcp_proxy_adapter/api/tool_integration.py +131 -0
- mcp_proxy_adapter/api/tools.py +163 -0
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +35 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +74 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +338 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client/__init__.py +9 -0
- mcp_proxy_adapter/client/jsonrpc_client/client.py +42 -0
- mcp_proxy_adapter/client/jsonrpc_client/command_api.py +45 -0
- mcp_proxy_adapter/client/jsonrpc_client/proxy_api.py +224 -0
- mcp_proxy_adapter/client/jsonrpc_client/queue_api.py +60 -0
- mcp_proxy_adapter/client/jsonrpc_client/transport.py +108 -0
- mcp_proxy_adapter/client/proxy.py +123 -0
- mcp_proxy_adapter/commands/__init__.py +66 -0
- mcp_proxy_adapter/commands/auth_validation_command.py +69 -0
- mcp_proxy_adapter/commands/base.py +389 -0
- mcp_proxy_adapter/commands/builtin_commands.py +30 -0
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +97 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +552 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +562 -0
- mcp_proxy_adapter/commands/command_registry.py +298 -0
- mcp_proxy_adapter/commands/config_command.py +102 -0
- mcp_proxy_adapter/commands/dependency_container.py +40 -0
- mcp_proxy_adapter/commands/dependency_manager.py +143 -0
- mcp_proxy_adapter/commands/echo_command.py +48 -0
- mcp_proxy_adapter/commands/health_command.py +142 -0
- mcp_proxy_adapter/commands/help_command.py +175 -0
- mcp_proxy_adapter/commands/hooks.py +172 -0
- mcp_proxy_adapter/commands/key_management_command.py +484 -0
- mcp_proxy_adapter/commands/load_command.py +123 -0
- mcp_proxy_adapter/commands/plugins_command.py +246 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +216 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +319 -0
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +76 -0
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +136 -0
- mcp_proxy_adapter/commands/result.py +157 -0
- mcp_proxy_adapter/commands/role_test_command.py +99 -0
- mcp_proxy_adapter/commands/roles_management_command.py +502 -0
- mcp_proxy_adapter/commands/security_command.py +472 -0
- mcp_proxy_adapter/commands/settings_command.py +113 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +306 -0
- mcp_proxy_adapter/commands/token_management_command.py +500 -0
- mcp_proxy_adapter/commands/transport_management_command.py +129 -0
- mcp_proxy_adapter/commands/unload_command.py +92 -0
- mcp_proxy_adapter/config.py +32 -0
- mcp_proxy_adapter/core/__init__.py +8 -0
- mcp_proxy_adapter/core/app_factory.py +560 -0
- mcp_proxy_adapter/core/app_runner.py +318 -0
- mcp_proxy_adapter/core/auth_validator.py +508 -0
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +372 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +185 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +388 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +65 -0
- mcp_proxy_adapter/core/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/client.py +608 -0
- mcp_proxy_adapter/core/client_manager.py +271 -0
- mcp_proxy_adapter/core/client_security.py +411 -0
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +237 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +116 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +100 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +380 -0
- mcp_proxy_adapter/core/config_converter.py +252 -0
- mcp_proxy_adapter/core/config_validator.py +211 -0
- mcp_proxy_adapter/core/crl_utils.py +362 -0
- mcp_proxy_adapter/core/errors.py +276 -0
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +250 -0
- mcp_proxy_adapter/core/mtls_asgi.py +140 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/mtls_proxy.py +229 -0
- mcp_proxy_adapter/core/mtls_server.py +154 -0
- mcp_proxy_adapter/core/protocol_manager.py +232 -0
- mcp_proxy_adapter/core/proxy/__init__.py +19 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +26 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +160 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +186 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +184 -0
- mcp_proxy_adapter/core/proxy_registration.py +80 -0
- mcp_proxy_adapter/core/role_utils.py +103 -0
- mcp_proxy_adapter/core/security_adapter.py +343 -0
- mcp_proxy_adapter/core/security_factory.py +96 -0
- mcp_proxy_adapter/core/security_integration.py +342 -0
- mcp_proxy_adapter/core/server_adapter.py +251 -0
- mcp_proxy_adapter/core/server_engine.py +217 -0
- mcp_proxy_adapter/core/settings.py +260 -0
- mcp_proxy_adapter/core/signal_handler.py +107 -0
- mcp_proxy_adapter/core/ssl_utils.py +161 -0
- mcp_proxy_adapter/core/transport_manager.py +153 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +471 -0
- mcp_proxy_adapter/core/utils.py +101 -0
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +219 -0
- mcp_proxy_adapter/core/validation/file_validator.py +131 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +190 -0
- mcp_proxy_adapter/core/validation/security_validator.py +140 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +58 -0
- mcp_proxy_adapter/examples/__init__.py +16 -0
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +52 -0
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +261 -0
- mcp_proxy_adapter/examples/cert_manager_bugfix.py +203 -0
- mcp_proxy_adapter/examples/check_config.py +413 -0
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/config_builder.py +234 -0
- mcp_proxy_adapter/examples/config_cli.py +282 -0
- mcp_proxy_adapter/examples/create_test_configs.py +174 -0
- mcp_proxy_adapter/examples/debug_request_state.py +130 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +191 -0
- mcp_proxy_adapter/examples/demo_client.py +287 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +13 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +8 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +45 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +52 -0
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +32 -0
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +54 -0
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +57 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +5 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +29 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +27 -0
- mcp_proxy_adapter/examples/full_application/main.py +264 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +81 -0
- 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_minimal_server.py +45 -0
- mcp_proxy_adapter/examples/full_application/test_server.py +163 -0
- mcp_proxy_adapter/examples/full_application/test_simple_server.py +62 -0
- mcp_proxy_adapter/examples/generate_config.py +502 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +335 -0
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +208 -0
- mcp_proxy_adapter/examples/run_example.py +77 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +619 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +153 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +435 -0
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +72 -0
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +235 -0
- mcp_proxy_adapter/examples/simple_protocol_test.py +125 -0
- mcp_proxy_adapter/examples/test_chk_hostname_automated.py +211 -0
- mcp_proxy_adapter/examples/test_config.py +205 -0
- mcp_proxy_adapter/examples/test_config_builder.py +110 -0
- mcp_proxy_adapter/examples/test_examples.py +308 -0
- mcp_proxy_adapter/examples/test_framework_complete.py +267 -0
- mcp_proxy_adapter/examples/test_mcp_server.py +187 -0
- mcp_proxy_adapter/examples/test_protocol_examples.py +337 -0
- mcp_proxy_adapter/examples/universal_client.py +674 -0
- mcp_proxy_adapter/examples/update_config_certificates.py +135 -0
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +385 -0
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +61 -0
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +313 -0
- mcp_proxy_adapter/openapi.py +375 -0
- mcp_proxy_adapter/schemas/base_schema.json +114 -0
- mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
- mcp_proxy_adapter/schemas/roles.json +37 -0
- mcp_proxy_adapter/schemas/roles_schema.json +162 -0
- mcp_proxy_adapter/version.py +5 -0
- mcp_proxy_adapter-6.9.43.dist-info/METADATA +739 -0
- mcp_proxy_adapter-6.9.43.dist-info/RECORD +242 -0
- mcp_proxy_adapter-6.9.43.dist-info/WHEEL +5 -0
- mcp_proxy_adapter-6.9.43.dist-info/entry_points.txt +12 -0
- mcp_proxy_adapter-6.9.43.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: Vasiliy Zdanovskiy
|
|
3
|
+
email: vasilyvz@gmail.com
|
|
4
|
+
|
|
5
|
+
Main configuration validator for MCP Proxy Adapter.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Any, Optional
|
|
12
|
+
|
|
13
|
+
from .file_validator import FileValidator
|
|
14
|
+
from .security_validator import SecurityValidator
|
|
15
|
+
from .protocol_validator import ProtocolValidator
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigValidator:
|
|
21
|
+
"""
|
|
22
|
+
Comprehensive configuration validator for MCP Proxy Adapter.
|
|
23
|
+
|
|
24
|
+
Validates:
|
|
25
|
+
- Required sections and keys
|
|
26
|
+
- File existence for referenced files
|
|
27
|
+
- Feature flag dependencies
|
|
28
|
+
- Protocol-specific requirements
|
|
29
|
+
- Security configuration consistency
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
33
|
+
"""
|
|
34
|
+
Initialize configuration validator.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config_path: Path to configuration file (optional)
|
|
38
|
+
"""
|
|
39
|
+
self.config_path = config_path
|
|
40
|
+
self.config_data: Dict[str, Any] = {}
|
|
41
|
+
self.validation_results: List[ValidationResult] = []
|
|
42
|
+
|
|
43
|
+
def load_config(self, config_path: Optional[str] = None) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Load configuration from file.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config_path: Path to configuration file
|
|
49
|
+
"""
|
|
50
|
+
if config_path:
|
|
51
|
+
self.config_path = config_path
|
|
52
|
+
|
|
53
|
+
if not self.config_path:
|
|
54
|
+
raise ValueError("No configuration path provided")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
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}")
|
|
62
|
+
except json.JSONDecodeError as e:
|
|
63
|
+
raise ValueError(f"Invalid JSON in configuration file: {e}")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
raise RuntimeError(f"Error loading configuration: {e}")
|
|
66
|
+
|
|
67
|
+
def validate_config(self, config_data: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
|
|
68
|
+
"""
|
|
69
|
+
Validate configuration data.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
config_data: Configuration data to validate (optional)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of validation results
|
|
76
|
+
"""
|
|
77
|
+
if config_data is not None:
|
|
78
|
+
self.config_data = config_data
|
|
79
|
+
|
|
80
|
+
if not self.config_data:
|
|
81
|
+
raise ValueError("No configuration data to validate")
|
|
82
|
+
|
|
83
|
+
self.validation_results = []
|
|
84
|
+
|
|
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
|
|
100
|
+
self._validate_unknown_fields()
|
|
101
|
+
self._validate_uuid_format()
|
|
102
|
+
|
|
103
|
+
return self.validation_results
|
|
104
|
+
|
|
105
|
+
def validate_all(self, config_data: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
|
|
106
|
+
"""
|
|
107
|
+
Validate all aspects of the configuration.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
config_data: Configuration data to validate (optional)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of validation results
|
|
114
|
+
"""
|
|
115
|
+
return self.validate_config(config_data)
|
|
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"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for section in self.config_data.keys():
|
|
125
|
+
if section not in known_sections:
|
|
126
|
+
self.validation_results.append(ValidationResult(
|
|
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"
|
|
131
|
+
))
|
|
132
|
+
|
|
133
|
+
def _validate_uuid_format(self) -> None:
|
|
134
|
+
"""Validate UUID format in configuration."""
|
|
135
|
+
uuid_fields = ["server.server_id", "proxy_registration.server_id"]
|
|
136
|
+
|
|
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)):
|
|
140
|
+
self.validation_results.append(ValidationResult(
|
|
141
|
+
level="warning",
|
|
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"
|
|
146
|
+
))
|
|
147
|
+
|
|
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))
|
|
153
|
+
|
|
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('.')
|
|
157
|
+
value = self.config_data
|
|
158
|
+
|
|
159
|
+
for k in keys:
|
|
160
|
+
if isinstance(value, dict) and k in value:
|
|
161
|
+
value = value[k]
|
|
162
|
+
else:
|
|
163
|
+
return default
|
|
164
|
+
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
def get_validation_summary(self) -> Dict[str, Any]:
|
|
168
|
+
"""
|
|
169
|
+
Get a summary of validation results.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dictionary with validation summary
|
|
173
|
+
"""
|
|
174
|
+
error_count = sum(1 for r in self.validation_results if r.level == "error")
|
|
175
|
+
warning_count = sum(1 for r in self.validation_results if r.level == "warning")
|
|
176
|
+
info_count = sum(1 for r in self.validation_results if r.level == "info")
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"total_issues": len(self.validation_results),
|
|
180
|
+
"errors": error_count,
|
|
181
|
+
"warnings": warning_count,
|
|
182
|
+
"info": info_count,
|
|
183
|
+
"is_valid": error_count == 0
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def print_validation_report(self) -> None:
|
|
187
|
+
"""Print a formatted validation report."""
|
|
188
|
+
summary = self.get_validation_summary()
|
|
189
|
+
|
|
190
|
+
print(f"\\n📋 Configuration Validation Report")
|
|
191
|
+
print(f"{'=' * 40}")
|
|
192
|
+
print(f"Total issues: {summary['total_issues']}")
|
|
193
|
+
print(f"Errors: {summary['errors']}")
|
|
194
|
+
print(f"Warnings: {summary['warnings']}")
|
|
195
|
+
print(f"Info: {summary['info']}")
|
|
196
|
+
print(f"Valid: {'✅ Yes' if summary['is_valid'] else '❌ No'}")
|
|
197
|
+
|
|
198
|
+
if self.validation_results:
|
|
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}")
|
|
207
|
+
if result.suggestion:
|
|
208
|
+
print(f" Suggestion: {result.suggestion}")
|
|
209
|
+
print()
|
|
210
|
+
|
|
211
|
+
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CRL Utilities Module
|
|
3
|
+
|
|
4
|
+
This module provides utilities for working with Certificate Revocation Lists (CRL).
|
|
5
|
+
Supports both file-based and URL-based CRL sources.
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
Version: 1.0.0
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, Union, Dict, Any
|
|
17
|
+
import requests
|
|
18
|
+
from requests.adapters import HTTPAdapter
|
|
19
|
+
from urllib3.util.retry import Retry
|
|
20
|
+
|
|
21
|
+
# Import mcp_security_framework CRL utilities
|
|
22
|
+
try:
|
|
23
|
+
from mcp_security_framework.utils.cert_utils import (
|
|
24
|
+
is_certificate_revoked,
|
|
25
|
+
validate_certificate_against_crl,
|
|
26
|
+
is_crl_valid,
|
|
27
|
+
get_crl_info,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CRLManager:
|
|
38
|
+
"""
|
|
39
|
+
Manager for Certificate Revocation Lists (CRL).
|
|
40
|
+
|
|
41
|
+
Supports both file-based and URL-based CRL sources.
|
|
42
|
+
Automatically downloads CRL from URLs and caches them locally.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, config: Dict[str, Any]):
|
|
46
|
+
"""
|
|
47
|
+
Initialize CRL manager.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config: Configuration dictionary containing CRL settings
|
|
51
|
+
"""
|
|
52
|
+
self.config = config
|
|
53
|
+
self.crl_enabled = config.get("crl_enabled", False)
|
|
54
|
+
|
|
55
|
+
# Only analyze CRL paths if certificates are enabled
|
|
56
|
+
certificates_enabled = config.get("certificates_enabled", True)
|
|
57
|
+
if certificates_enabled and self.crl_enabled:
|
|
58
|
+
self.crl_path = config.get("crl_path")
|
|
59
|
+
self.crl_url = config.get("crl_url")
|
|
60
|
+
self.crl_validity_days = config.get("crl_validity_days", 30)
|
|
61
|
+
else:
|
|
62
|
+
# Don't analyze CRL paths if certificates are disabled
|
|
63
|
+
self.crl_path = None
|
|
64
|
+
self.crl_url = None
|
|
65
|
+
self.crl_validity_days = 30
|
|
66
|
+
|
|
67
|
+
# Cache for downloaded CRL files
|
|
68
|
+
self._crl_cache: Dict[str, str] = {}
|
|
69
|
+
|
|
70
|
+
# Setup HTTP session with retry strategy
|
|
71
|
+
self._setup_http_session()
|
|
72
|
+
|
|
73
|
+
get_global_logger().info(
|
|
74
|
+
f"CRL Manager initialized - enabled: {self.crl_enabled}, certificates_enabled: {certificates_enabled}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _setup_http_session(self):
|
|
78
|
+
"""Setup HTTP session with retry strategy for CRL downloads."""
|
|
79
|
+
self.session = requests.Session()
|
|
80
|
+
|
|
81
|
+
# Configure retry strategy
|
|
82
|
+
retry_strategy = Retry(
|
|
83
|
+
total=3,
|
|
84
|
+
backoff_factor=1,
|
|
85
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
89
|
+
self.session.mount("http://", adapter)
|
|
90
|
+
self.session.mount("https://", adapter)
|
|
91
|
+
|
|
92
|
+
# Set timeout
|
|
93
|
+
self.session.timeout = 30
|
|
94
|
+
|
|
95
|
+
def get_crl_data(self) -> Optional[Union[str, bytes, Path]]:
|
|
96
|
+
"""
|
|
97
|
+
Get CRL data from configured source.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
CRL data as string, bytes, or Path, or None if not available
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If CRL is enabled but no source is configured
|
|
104
|
+
FileNotFoundError: If CRL file is not found
|
|
105
|
+
requests.RequestException: If CRL download fails
|
|
106
|
+
"""
|
|
107
|
+
if not self.crl_enabled:
|
|
108
|
+
get_global_logger().debug("CRL is disabled, skipping CRL check")
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Check if CRL URL is configured
|
|
112
|
+
if self.crl_url:
|
|
113
|
+
return self._get_crl_from_url()
|
|
114
|
+
|
|
115
|
+
# Check if CRL file path is configured
|
|
116
|
+
if self.crl_path:
|
|
117
|
+
return self._get_crl_from_file()
|
|
118
|
+
|
|
119
|
+
# If CRL is enabled but no source is configured, this is an error
|
|
120
|
+
if self.crl_enabled:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"CRL is enabled but neither crl_path nor crl_url is configured"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def _get_crl_from_url(self) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Download CRL from URL.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Path to downloaded CRL file
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
requests.RequestException: If download fails
|
|
136
|
+
ValueError: If downloaded data is not valid CRL
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
get_global_logger().info(f"Downloading CRL from URL: {self.crl_url}")
|
|
140
|
+
|
|
141
|
+
# Download CRL
|
|
142
|
+
response = self.session.get(self.crl_url)
|
|
143
|
+
response.raise_for_status()
|
|
144
|
+
|
|
145
|
+
# Validate content type
|
|
146
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
147
|
+
if (
|
|
148
|
+
"application/pkix-crl" not in content_type
|
|
149
|
+
and "application/x-pkcs7-crl" not in content_type
|
|
150
|
+
):
|
|
151
|
+
get_global_logger().warning(f"Unexpected content type for CRL: {content_type}")
|
|
152
|
+
|
|
153
|
+
# Save to temporary file
|
|
154
|
+
with tempfile.NamedTemporaryFile(
|
|
155
|
+
mode="wb", suffix=".crl", delete=False
|
|
156
|
+
) as temp_file:
|
|
157
|
+
temp_file.write(response.content)
|
|
158
|
+
temp_file_path = temp_file.name
|
|
159
|
+
|
|
160
|
+
# Validate CRL format
|
|
161
|
+
if SECURITY_FRAMEWORK_AVAILABLE:
|
|
162
|
+
try:
|
|
163
|
+
is_crl_valid(temp_file_path)
|
|
164
|
+
get_global_logger().info(
|
|
165
|
+
f"CRL downloaded and validated successfully from {self.crl_url}"
|
|
166
|
+
)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
os.unlink(temp_file_path)
|
|
169
|
+
raise ValueError(f"Downloaded CRL is not valid: {e}")
|
|
170
|
+
else:
|
|
171
|
+
get_global_logger().warning(
|
|
172
|
+
"mcp_security_framework not available, skipping CRL validation"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Cache the file path
|
|
176
|
+
self._crl_cache[self.crl_url] = temp_file_path
|
|
177
|
+
|
|
178
|
+
return temp_file_path
|
|
179
|
+
|
|
180
|
+
except requests.RequestException as e:
|
|
181
|
+
get_global_logger().error(f"Failed to download CRL from {self.crl_url}: {e}")
|
|
182
|
+
raise
|
|
183
|
+
except Exception as e:
|
|
184
|
+
get_global_logger().error(f"CRL download failed: {e}")
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
def _get_crl_from_file(self) -> str:
|
|
188
|
+
"""
|
|
189
|
+
Get CRL from file path.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Path to CRL file
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
FileNotFoundError: If CRL file is not found
|
|
196
|
+
ValueError: If CRL file is not valid
|
|
197
|
+
"""
|
|
198
|
+
if not os.path.exists(self.crl_path):
|
|
199
|
+
raise FileNotFoundError(f"CRL file not found: {self.crl_path}")
|
|
200
|
+
|
|
201
|
+
# Validate CRL format
|
|
202
|
+
if SECURITY_FRAMEWORK_AVAILABLE:
|
|
203
|
+
try:
|
|
204
|
+
is_crl_valid(self.crl_path)
|
|
205
|
+
get_global_logger().info(f"CRL file validated successfully: {self.crl_path}")
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise ValueError(f"CRL file is not valid: {e}")
|
|
208
|
+
else:
|
|
209
|
+
get_global_logger().warning(
|
|
210
|
+
"mcp_security_framework not available, skipping CRL validation"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return self.crl_path
|
|
214
|
+
|
|
215
|
+
def is_certificate_revoked(self, cert_path: str) -> bool:
|
|
216
|
+
"""
|
|
217
|
+
Check if certificate is revoked according to CRL.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
cert_path: Path to certificate file
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if certificate is revoked, False otherwise
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: If CRL is enabled but not available
|
|
227
|
+
FileNotFoundError: If certificate file is not found
|
|
228
|
+
"""
|
|
229
|
+
if not self.crl_enabled:
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
|
233
|
+
get_global_logger().warning("mcp_security_framework not available, skipping CRL check")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
crl_data = self.get_crl_data()
|
|
238
|
+
if not crl_data:
|
|
239
|
+
get_global_logger().warning("CRL is enabled but no CRL data is available")
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
is_revoked = is_certificate_revoked(cert_path, crl_data)
|
|
243
|
+
|
|
244
|
+
if is_revoked:
|
|
245
|
+
get_global_logger().warning(f"Certificate is revoked according to CRL: {cert_path}")
|
|
246
|
+
else:
|
|
247
|
+
get_global_logger().debug(
|
|
248
|
+
f"Certificate is not revoked according to CRL: {cert_path}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return is_revoked
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
get_global_logger().error(f"CRL check failed for certificate {cert_path}: {e}")
|
|
255
|
+
# For security, consider certificate invalid if CRL check fails
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
def validate_certificate_against_crl(self, cert_path: str) -> Dict[str, Any]:
|
|
259
|
+
"""
|
|
260
|
+
Validate certificate against CRL and return detailed status.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
cert_path: Path to certificate file
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary containing validation results
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
ValueError: If CRL is enabled but not available
|
|
270
|
+
FileNotFoundError: If certificate file is not found
|
|
271
|
+
"""
|
|
272
|
+
if not self.crl_enabled:
|
|
273
|
+
return {
|
|
274
|
+
"is_revoked": False,
|
|
275
|
+
"crl_checked": False,
|
|
276
|
+
"crl_source": None,
|
|
277
|
+
"message": "CRL check is disabled",
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
|
281
|
+
get_global_logger().warning(
|
|
282
|
+
"mcp_security_framework not available, skipping CRL validation"
|
|
283
|
+
)
|
|
284
|
+
return {
|
|
285
|
+
"is_revoked": False,
|
|
286
|
+
"crl_checked": False,
|
|
287
|
+
"crl_source": None,
|
|
288
|
+
"message": "mcp_security_framework not available",
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
crl_data = self.get_crl_data()
|
|
293
|
+
if not crl_data:
|
|
294
|
+
get_global_logger().warning("CRL is enabled but no CRL data is available")
|
|
295
|
+
return {
|
|
296
|
+
"is_revoked": True, # For security, consider invalid if CRL unavailable
|
|
297
|
+
"crl_checked": False,
|
|
298
|
+
"crl_source": None,
|
|
299
|
+
"message": "CRL is enabled but not available",
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# Get CRL source info
|
|
303
|
+
crl_source = self.crl_url if self.crl_url else self.crl_path
|
|
304
|
+
|
|
305
|
+
# Validate certificate against CRL
|
|
306
|
+
result = validate_certificate_against_crl(cert_path, crl_data)
|
|
307
|
+
|
|
308
|
+
result["crl_checked"] = True
|
|
309
|
+
result["crl_source"] = crl_source
|
|
310
|
+
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
get_global_logger().error(f"CRL validation failed for certificate {cert_path}: {e}")
|
|
315
|
+
# For security, consider certificate invalid if CRL validation fails
|
|
316
|
+
return {
|
|
317
|
+
"is_revoked": True,
|
|
318
|
+
"crl_checked": False,
|
|
319
|
+
"crl_source": self.crl_url if self.crl_url else self.crl_path,
|
|
320
|
+
"message": f"CRL validation failed: {e}",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
def get_crl_info(self) -> Optional[Dict[str, Any]]:
|
|
324
|
+
"""
|
|
325
|
+
Get information about the configured CRL.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Dictionary containing CRL information, or None if CRL is not available
|
|
329
|
+
"""
|
|
330
|
+
if not self.crl_enabled:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
|
334
|
+
get_global_logger().warning("mcp_security_framework not available, cannot get CRL info")
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
crl_data = self.get_crl_data()
|
|
339
|
+
if not crl_data:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
return get_crl_info(crl_data)
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
get_global_logger().error(f"Failed to get CRL info: {e}")
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
def cleanup_cache(self):
|
|
349
|
+
"""Clean up temporary CRL files."""
|
|
350
|
+
for url, temp_path in self._crl_cache.items():
|
|
351
|
+
try:
|
|
352
|
+
if os.path.exists(temp_path):
|
|
353
|
+
os.unlink(temp_path)
|
|
354
|
+
get_global_logger().debug(f"Cleaned up temporary CRL file: {temp_path}")
|
|
355
|
+
except Exception as e:
|
|
356
|
+
get_global_logger().warning(f"Failed to cleanup temporary CRL file {temp_path}: {e}")
|
|
357
|
+
|
|
358
|
+
self._crl_cache.clear()
|
|
359
|
+
|
|
360
|
+
def __del__(self):
|
|
361
|
+
"""Cleanup when object is destroyed."""
|
|
362
|
+
self.cleanup_cache()
|