mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_proxy_adapter/__main__.py +12 -0
- mcp_proxy_adapter/api/app.py +138 -11
- mcp_proxy_adapter/api/handlers.py +16 -1
- mcp_proxy_adapter/api/middleware/__init__.py +30 -29
- mcp_proxy_adapter/api/middleware/auth_adapter.py +235 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +219 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/mtls_adapter.py +305 -0
- mcp_proxy_adapter/api/middleware/mtls_middleware.py +296 -0
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
- mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +241 -0
- mcp_proxy_adapter/api/middleware/roles_adapter.py +365 -0
- mcp_proxy_adapter/api/middleware/roles_middleware.py +381 -0
- mcp_proxy_adapter/api/middleware/security.py +376 -0
- mcp_proxy_adapter/api/middleware/token_auth_middleware.py +261 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/commands/__init__.py +13 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +61 -30
- mcp_proxy_adapter/commands/builtin_commands.py +89 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +703 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/health_command.py +7 -0
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +268 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +99 -2
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +827 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +11 -0
- mcp_proxy_adapter/core/protocol_manager.py +226 -0
- mcp_proxy_adapter/core/proxy_registration.py +270 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +373 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +233 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/basic_server/config.json +58 -23
- mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +54 -0
- mcp_proxy_adapter/examples/basic_server/config_http.json +70 -0
- mcp_proxy_adapter/examples/basic_server/config_http_only.json +52 -0
- mcp_proxy_adapter/examples/basic_server/config_https.json +58 -0
- mcp_proxy_adapter/examples/basic_server/config_mtls.json +58 -0
- mcp_proxy_adapter/examples/basic_server/config_ssl.json +46 -0
- mcp_proxy_adapter/examples/basic_server/server.py +12 -1
- mcp_proxy_adapter/examples/custom_commands/__init__.py +1 -1
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +339 -23
- mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +105 -0
- mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +129 -0
- mcp_proxy_adapter/examples/custom_commands/config.json +101 -18
- mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_https_only.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/full_help_response.json +1 -0
- mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +629 -0
- mcp_proxy_adapter/examples/custom_commands/get_openapi.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +129 -0
- mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +278 -0
- mcp_proxy_adapter/examples/custom_commands/server.py +92 -68
- mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +75 -0
- mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +299 -0
- mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +278 -0
- mcp_proxy_adapter/examples/custom_commands/test_openapi.py +27 -0
- mcp_proxy_adapter/examples/custom_commands/test_registry.py +23 -0
- mcp_proxy_adapter/examples/custom_commands/test_simple.py +19 -0
- mcp_proxy_adapter/examples/custom_project_example/README.md +103 -0
- mcp_proxy_adapter/examples/custom_project_example/README_EN.md +103 -0
- mcp_proxy_adapter/examples/simple_custom_commands/README.md +149 -0
- mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +149 -0
- mcp_proxy_adapter/main.py +175 -0
- mcp_proxy_adapter/schemas/roles_schema.json +162 -0
- mcp_proxy_adapter/tests/unit/test_config.py +53 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.0.0.dist-info/RECORD +179 -0
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
"""
|
2
|
+
Middleware Factory for creating and managing middleware components.
|
3
|
+
|
4
|
+
This module provides a factory for creating middleware components with proper
|
5
|
+
configuration and dependency management.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Dict, Any, List, Optional, Type
|
10
|
+
|
11
|
+
from fastapi import FastAPI
|
12
|
+
|
13
|
+
from mcp_proxy_adapter.core.logging import logger
|
14
|
+
from mcp_proxy_adapter.core.security_factory import SecurityFactory
|
15
|
+
from .base import BaseMiddleware
|
16
|
+
from .security import SecurityMiddleware
|
17
|
+
from .error_handling import ErrorHandlingMiddleware
|
18
|
+
from .logging import LoggingMiddleware
|
19
|
+
|
20
|
+
|
21
|
+
class MiddlewareFactory:
|
22
|
+
"""
|
23
|
+
Factory for creating and managing middleware components.
|
24
|
+
|
25
|
+
Provides methods to create middleware components with proper configuration
|
26
|
+
and dependency management.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self, app: FastAPI, config: Dict[str, Any]):
|
30
|
+
"""
|
31
|
+
Initialize middleware factory.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
app: FastAPI application
|
35
|
+
config: Application configuration
|
36
|
+
"""
|
37
|
+
self.app = app
|
38
|
+
self.config = config
|
39
|
+
self.middleware_stack: List[BaseMiddleware] = []
|
40
|
+
|
41
|
+
logger.info("Middleware factory initialized")
|
42
|
+
|
43
|
+
def create_security_middleware(self) -> Optional[SecurityMiddleware]:
|
44
|
+
"""
|
45
|
+
Create security middleware.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
SecurityMiddleware instance or None if creation failed
|
49
|
+
"""
|
50
|
+
try:
|
51
|
+
security_config = self.config.get("security", {})
|
52
|
+
|
53
|
+
if not security_config.get("enabled", True):
|
54
|
+
logger.info("Security middleware disabled by configuration")
|
55
|
+
return None
|
56
|
+
|
57
|
+
middleware = SecurityMiddleware(self.app, self.config)
|
58
|
+
self.middleware_stack.append(middleware)
|
59
|
+
|
60
|
+
logger.info("Security middleware created successfully")
|
61
|
+
return middleware
|
62
|
+
|
63
|
+
except Exception as e:
|
64
|
+
logger.error(f"Failed to create security middleware: {e}")
|
65
|
+
return None
|
66
|
+
|
67
|
+
def create_error_handling_middleware(self) -> Optional[ErrorHandlingMiddleware]:
|
68
|
+
"""
|
69
|
+
Create error handling middleware.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
ErrorHandlingMiddleware instance or None if creation failed
|
73
|
+
"""
|
74
|
+
try:
|
75
|
+
# Import here to avoid circular imports
|
76
|
+
from .error_handling import ErrorHandlingMiddleware
|
77
|
+
|
78
|
+
middleware = ErrorHandlingMiddleware(self.app)
|
79
|
+
self.middleware_stack.append(middleware)
|
80
|
+
|
81
|
+
logger.info("Error handling middleware created successfully")
|
82
|
+
return middleware
|
83
|
+
|
84
|
+
except Exception as e:
|
85
|
+
logger.error(f"Failed to create error handling middleware: {e}")
|
86
|
+
return None
|
87
|
+
|
88
|
+
def create_logging_middleware(self) -> Optional[LoggingMiddleware]:
|
89
|
+
"""
|
90
|
+
Create logging middleware.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
LoggingMiddleware instance or None if creation failed
|
94
|
+
"""
|
95
|
+
try:
|
96
|
+
# Import here to avoid circular imports
|
97
|
+
from .logging import LoggingMiddleware
|
98
|
+
|
99
|
+
middleware = LoggingMiddleware(self.app, self.config)
|
100
|
+
self.middleware_stack.append(middleware)
|
101
|
+
|
102
|
+
logger.info("Logging middleware created successfully")
|
103
|
+
return middleware
|
104
|
+
|
105
|
+
except Exception as e:
|
106
|
+
logger.error(f"Failed to create logging middleware: {e}")
|
107
|
+
return None
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
def create_all_middleware(self) -> List[BaseMiddleware]:
|
112
|
+
"""
|
113
|
+
Create all required middleware components.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
List of created middleware instances
|
117
|
+
"""
|
118
|
+
middleware_list = []
|
119
|
+
|
120
|
+
# Create security middleware (unified)
|
121
|
+
security_middleware = self.create_security_middleware()
|
122
|
+
if security_middleware:
|
123
|
+
middleware_list.append(security_middleware)
|
124
|
+
|
125
|
+
# Create error handling middleware
|
126
|
+
error_middleware = self.create_error_handling_middleware()
|
127
|
+
if error_middleware:
|
128
|
+
middleware_list.append(error_middleware)
|
129
|
+
|
130
|
+
# Create logging middleware
|
131
|
+
logging_middleware = self.create_logging_middleware()
|
132
|
+
if logging_middleware:
|
133
|
+
middleware_list.append(logging_middleware)
|
134
|
+
|
135
|
+
logger.info(f"Created {len(middleware_list)} middleware components")
|
136
|
+
return middleware_list
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
def get_middleware_by_type(self, middleware_type: Type[BaseMiddleware]) -> Optional[BaseMiddleware]:
|
141
|
+
"""
|
142
|
+
Get middleware instance by type.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
middleware_type: Type of middleware to find
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
Middleware instance or None if not found
|
149
|
+
"""
|
150
|
+
for middleware in self.middleware_stack:
|
151
|
+
if isinstance(middleware, middleware_type):
|
152
|
+
return middleware
|
153
|
+
return None
|
154
|
+
|
155
|
+
def get_security_middleware(self) -> Optional[SecurityMiddleware]:
|
156
|
+
"""
|
157
|
+
Get security middleware instance.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
SecurityMiddleware instance or None if not found
|
161
|
+
"""
|
162
|
+
return self.get_middleware_by_type(SecurityMiddleware)
|
163
|
+
|
164
|
+
def validate_middleware_config(self) -> bool:
|
165
|
+
"""
|
166
|
+
Validate middleware configuration.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
True if configuration is valid, False otherwise
|
170
|
+
"""
|
171
|
+
try:
|
172
|
+
security_config = self.config.get("security", {})
|
173
|
+
|
174
|
+
# Validate security configuration
|
175
|
+
if not SecurityFactory.validate_config(self.config):
|
176
|
+
logger.error("Security configuration validation failed")
|
177
|
+
return False
|
178
|
+
|
179
|
+
# Validate middleware-specific configurations
|
180
|
+
if security_config.get("enabled", True):
|
181
|
+
# Check required fields for security middleware
|
182
|
+
auth_config = security_config.get("auth", {})
|
183
|
+
if not isinstance(auth_config, dict):
|
184
|
+
logger.error("Auth configuration must be a dictionary")
|
185
|
+
return False
|
186
|
+
|
187
|
+
ssl_config = security_config.get("ssl", {})
|
188
|
+
if not isinstance(ssl_config, dict):
|
189
|
+
logger.error("SSL configuration must be a dictionary")
|
190
|
+
return False
|
191
|
+
|
192
|
+
logger.info("Middleware configuration validation passed")
|
193
|
+
return True
|
194
|
+
|
195
|
+
except Exception as e:
|
196
|
+
logger.error(f"Middleware configuration validation failed: {e}")
|
197
|
+
return False
|
198
|
+
|
199
|
+
def get_middleware_info(self) -> Dict[str, Any]:
|
200
|
+
"""
|
201
|
+
Get information about created middleware.
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
Dictionary with middleware information
|
205
|
+
"""
|
206
|
+
info = {
|
207
|
+
"total_middleware": len(self.middleware_stack),
|
208
|
+
"middleware_types": [],
|
209
|
+
"security_enabled": False
|
210
|
+
}
|
211
|
+
|
212
|
+
for middleware in self.middleware_stack:
|
213
|
+
middleware_type = type(middleware).__name__
|
214
|
+
info["middleware_types"].append(middleware_type)
|
215
|
+
|
216
|
+
if isinstance(middleware, SecurityMiddleware):
|
217
|
+
info["security_enabled"] = True
|
218
|
+
|
219
|
+
return info
|
@@ -5,7 +5,7 @@ Middleware for request logging.
|
|
5
5
|
import time
|
6
6
|
import json
|
7
7
|
import uuid
|
8
|
-
from typing import Callable, Awaitable
|
8
|
+
from typing import Callable, Awaitable, Dict, Any
|
9
9
|
|
10
10
|
from fastapi import Request, Response
|
11
11
|
|
@@ -17,6 +17,17 @@ class LoggingMiddleware(BaseMiddleware):
|
|
17
17
|
Middleware for logging requests and responses.
|
18
18
|
"""
|
19
19
|
|
20
|
+
def __init__(self, app, config: Dict[str, Any] = None):
|
21
|
+
"""
|
22
|
+
Initialize logging middleware.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
app: FastAPI application
|
26
|
+
config: Application configuration (optional)
|
27
|
+
"""
|
28
|
+
super().__init__(app)
|
29
|
+
self.config = config or {}
|
30
|
+
|
20
31
|
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
21
32
|
"""
|
22
33
|
Processes request and logs information about it.
|
@@ -43,7 +54,13 @@ class LoggingMiddleware(BaseMiddleware):
|
|
43
54
|
url = str(request.url)
|
44
55
|
client_host = request.client.host if request.client else "unknown"
|
45
56
|
|
46
|
-
|
57
|
+
# Check if this is an OpenAPI schema request (should be logged at DEBUG level)
|
58
|
+
is_openapi_request = "/openapi.json" in url
|
59
|
+
|
60
|
+
if is_openapi_request:
|
61
|
+
req_logger.debug(f"Request started: {method} {url} | Client: {client_host}")
|
62
|
+
else:
|
63
|
+
req_logger.info(f"Request started: {method} {url} | Client: {client_host}")
|
47
64
|
|
48
65
|
# Log request body if not GET or HEAD
|
49
66
|
if method not in ["GET", "HEAD"]:
|
@@ -79,8 +96,12 @@ class LoggingMiddleware(BaseMiddleware):
|
|
79
96
|
process_time = time.time() - start_time
|
80
97
|
status_code = response.status_code
|
81
98
|
|
82
|
-
|
83
|
-
|
99
|
+
if is_openapi_request:
|
100
|
+
req_logger.debug(f"Request completed: {method} {url} | Status: {status_code} | "
|
101
|
+
f"Time: {process_time:.3f}s")
|
102
|
+
else:
|
103
|
+
req_logger.info(f"Request completed: {method} {url} | Status: {status_code} | "
|
104
|
+
f"Time: {process_time:.3f}s")
|
84
105
|
|
85
106
|
# Add request ID to response headers
|
86
107
|
response.headers["X-Request-ID"] = request_id
|
@@ -90,7 +111,12 @@ class LoggingMiddleware(BaseMiddleware):
|
|
90
111
|
except Exception as e:
|
91
112
|
# Log error
|
92
113
|
process_time = time.time() - start_time
|
93
|
-
|
94
|
-
|
114
|
+
|
115
|
+
if is_openapi_request:
|
116
|
+
req_logger.debug(f"Request failed: {method} {url} | Error: {str(e)} | "
|
117
|
+
f"Time: {process_time:.3f}s")
|
118
|
+
else:
|
119
|
+
req_logger.error(f"Request failed: {method} {url} | Error: {str(e)} | "
|
120
|
+
f"Time: {process_time:.3f}s")
|
95
121
|
|
96
122
|
raise
|
@@ -0,0 +1,305 @@
|
|
1
|
+
"""
|
2
|
+
MTLS Middleware Adapter for backward compatibility.
|
3
|
+
|
4
|
+
This module provides an adapter that maintains the same interface as MTLSMiddleware
|
5
|
+
while using the new SecurityMiddleware internally.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Dict, List, Optional, Any, Callable, Awaitable
|
10
|
+
from cryptography import x509
|
11
|
+
from cryptography.hazmat.primitives import serialization
|
12
|
+
|
13
|
+
from fastapi import Request, Response
|
14
|
+
from starlette.responses import JSONResponse
|
15
|
+
|
16
|
+
from mcp_proxy_adapter.core.logging import logger
|
17
|
+
from mcp_proxy_adapter.core.auth_validator import AuthValidator
|
18
|
+
from mcp_proxy_adapter.core.role_utils import RoleUtils
|
19
|
+
from mcp_proxy_adapter.core.certificate_utils import CertificateUtils
|
20
|
+
from .base import BaseMiddleware
|
21
|
+
from .security import SecurityMiddleware
|
22
|
+
|
23
|
+
|
24
|
+
class MTLSMiddlewareAdapter(BaseMiddleware):
|
25
|
+
"""
|
26
|
+
Adapter for MTLSMiddleware that uses SecurityMiddleware internally.
|
27
|
+
|
28
|
+
Maintains the same interface as the original MTLSMiddleware for backward compatibility.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, app, mtls_config: Dict[str, Any]):
|
32
|
+
"""
|
33
|
+
Initialize mTLS middleware adapter.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
app: FastAPI application
|
37
|
+
mtls_config: mTLS configuration dictionary
|
38
|
+
"""
|
39
|
+
super().__init__(app)
|
40
|
+
|
41
|
+
# Store original configuration for backward compatibility
|
42
|
+
self.mtls_config = mtls_config
|
43
|
+
self.auth_validator = AuthValidator()
|
44
|
+
self.role_utils = RoleUtils()
|
45
|
+
self.certificate_utils = CertificateUtils()
|
46
|
+
|
47
|
+
# Extract configuration
|
48
|
+
self.enabled = mtls_config.get("enabled", False)
|
49
|
+
self.ca_cert_path = mtls_config.get("ca_cert")
|
50
|
+
self.verify_client = mtls_config.get("verify_client", True)
|
51
|
+
self.client_cert_required = mtls_config.get("client_cert_required", True)
|
52
|
+
self.allowed_roles = mtls_config.get("allowed_roles", [])
|
53
|
+
self.require_roles = mtls_config.get("require_roles", False)
|
54
|
+
|
55
|
+
# Create internal security middleware
|
56
|
+
self.security_middleware = self._create_security_middleware()
|
57
|
+
|
58
|
+
logger.info(f"MTLSMiddlewareAdapter initialized: enabled={self.enabled}, "
|
59
|
+
f"verify_client={self.verify_client}, "
|
60
|
+
f"client_cert_required={self.client_cert_required}")
|
61
|
+
|
62
|
+
def _create_security_middleware(self) -> SecurityMiddleware:
|
63
|
+
"""
|
64
|
+
Create internal SecurityMiddleware with MTLSMiddleware configuration.
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
SecurityMiddleware instance
|
68
|
+
"""
|
69
|
+
# Convert MTLSMiddleware config to SecurityMiddleware config
|
70
|
+
security_config = {
|
71
|
+
"security": {
|
72
|
+
"enabled": self.enabled,
|
73
|
+
"auth": {
|
74
|
+
"enabled": False
|
75
|
+
},
|
76
|
+
"ssl": {
|
77
|
+
"enabled": self.enabled,
|
78
|
+
"cert_file": None,
|
79
|
+
"key_file": None,
|
80
|
+
"ca_cert": self.ca_cert_path,
|
81
|
+
"min_tls_version": "TLSv1.2",
|
82
|
+
"verify_client": self.verify_client,
|
83
|
+
"client_cert_required": self.client_cert_required
|
84
|
+
},
|
85
|
+
"permissions": {
|
86
|
+
"enabled": self.require_roles,
|
87
|
+
"roles_file": None,
|
88
|
+
"default_role": "user",
|
89
|
+
"deny_by_default": True
|
90
|
+
},
|
91
|
+
"rate_limit": {
|
92
|
+
"enabled": False
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
return SecurityMiddleware(self.app, security_config)
|
98
|
+
|
99
|
+
async def before_request(self, request: Request) -> None:
|
100
|
+
"""
|
101
|
+
Process request before calling the main handler.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
request: FastAPI request object
|
105
|
+
"""
|
106
|
+
if not self.enabled:
|
107
|
+
return
|
108
|
+
|
109
|
+
try:
|
110
|
+
# Use SecurityMiddleware for validation
|
111
|
+
await self.security_middleware.before_request(request)
|
112
|
+
|
113
|
+
# Additional MTLS-specific processing
|
114
|
+
client_cert = self._extract_client_certificate(request)
|
115
|
+
if client_cert:
|
116
|
+
# Store certificate and roles in request state for backward compatibility
|
117
|
+
request.state.client_certificate = client_cert
|
118
|
+
request.state.client_roles = self._extract_roles_from_certificate(client_cert)
|
119
|
+
request.state.client_common_name = self._get_common_name(client_cert)
|
120
|
+
|
121
|
+
logger.debug(f"mTLS authentication successful for {request.state.client_common_name} "
|
122
|
+
f"with roles: {request.state.client_roles}")
|
123
|
+
|
124
|
+
except Exception as e:
|
125
|
+
logger.error(f"mTLS authentication failed: {e}")
|
126
|
+
raise
|
127
|
+
|
128
|
+
def _extract_client_certificate(self, request: Request) -> Optional[x509.Certificate]:
|
129
|
+
"""
|
130
|
+
Extract client certificate from request.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
request: FastAPI request object
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
Client certificate or None
|
137
|
+
"""
|
138
|
+
# Check for certificate in request headers
|
139
|
+
cert_header = request.headers.get("X-Client-Cert")
|
140
|
+
if cert_header:
|
141
|
+
try:
|
142
|
+
cert_data = cert_header.encode('utf-8')
|
143
|
+
return x509.load_pem_x509_certificate(cert_data)
|
144
|
+
except Exception as e:
|
145
|
+
logger.warning(f"Failed to parse certificate from header: {e}")
|
146
|
+
|
147
|
+
# Check for certificate in request state (from SSL context)
|
148
|
+
if hasattr(request, 'client') and hasattr(request.client, 'get_extra_info'):
|
149
|
+
cert = request.client.get_extra_info('ssl_object')
|
150
|
+
if cert:
|
151
|
+
return cert
|
152
|
+
|
153
|
+
return None
|
154
|
+
|
155
|
+
def _validate_client_certificate(self, cert: x509.Certificate) -> bool:
|
156
|
+
"""
|
157
|
+
Validate client certificate.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
cert: Client certificate
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
True if valid, False otherwise
|
164
|
+
"""
|
165
|
+
try:
|
166
|
+
# Basic validation
|
167
|
+
if not self.certificate_utils.is_certificate_valid(cert):
|
168
|
+
return False
|
169
|
+
|
170
|
+
# CA validation if CA cert is provided
|
171
|
+
if self.ca_cert_path:
|
172
|
+
return self.certificate_utils.validate_certificate_chain(cert, self.ca_cert_path)
|
173
|
+
|
174
|
+
return True
|
175
|
+
|
176
|
+
except Exception as e:
|
177
|
+
logger.error(f"Certificate validation failed: {e}")
|
178
|
+
return False
|
179
|
+
|
180
|
+
def _extract_roles_from_certificate(self, cert: x509.Certificate) -> List[str]:
|
181
|
+
"""
|
182
|
+
Extract roles from client certificate.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
cert: Client certificate
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
List of roles
|
189
|
+
"""
|
190
|
+
try:
|
191
|
+
# Extract from subject alternative names
|
192
|
+
roles = []
|
193
|
+
|
194
|
+
# Check for roles in SAN
|
195
|
+
san = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
196
|
+
if san:
|
197
|
+
for name in san.value:
|
198
|
+
if isinstance(name, x509.DNSName):
|
199
|
+
if name.value.startswith("role="):
|
200
|
+
role = name.value.split("=", 1)[1]
|
201
|
+
roles.append(role)
|
202
|
+
|
203
|
+
# Check for roles in subject
|
204
|
+
subject = cert.subject
|
205
|
+
for attr in subject:
|
206
|
+
if attr.oid.dotted_string == "2.5.4.3": # Common Name
|
207
|
+
if attr.value.startswith("role="):
|
208
|
+
role = attr.value.split("=", 1)[1]
|
209
|
+
roles.append(role)
|
210
|
+
|
211
|
+
# Check allowed roles if specified
|
212
|
+
if self.allowed_roles:
|
213
|
+
roles = [role for role in roles if role in self.allowed_roles]
|
214
|
+
|
215
|
+
return roles
|
216
|
+
|
217
|
+
except Exception as e:
|
218
|
+
logger.error(f"Failed to extract roles from certificate: {e}")
|
219
|
+
return []
|
220
|
+
|
221
|
+
def _get_common_name(self, cert: x509.Certificate) -> str:
|
222
|
+
"""
|
223
|
+
Get common name from certificate.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
cert: Client certificate
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
Common name
|
230
|
+
"""
|
231
|
+
try:
|
232
|
+
subject = cert.subject
|
233
|
+
for attr in subject:
|
234
|
+
if attr.oid.dotted_string == "2.5.4.3": # Common Name
|
235
|
+
return attr.value
|
236
|
+
return "unknown"
|
237
|
+
except Exception:
|
238
|
+
return "unknown"
|
239
|
+
|
240
|
+
def _validate_access(self, roles: List[str]) -> bool:
|
241
|
+
"""
|
242
|
+
Validate access based on roles.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
roles: List of client roles
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
True if access is allowed, False otherwise
|
249
|
+
"""
|
250
|
+
if not self.require_roles:
|
251
|
+
return True
|
252
|
+
|
253
|
+
if not roles:
|
254
|
+
return False
|
255
|
+
|
256
|
+
# Check if any role is allowed
|
257
|
+
return any(role in self.allowed_roles for role in roles)
|
258
|
+
|
259
|
+
def get_client_certificate(self, request: Request) -> Optional[x509.Certificate]:
|
260
|
+
"""
|
261
|
+
Get client certificate from request state (backward compatibility).
|
262
|
+
|
263
|
+
Args:
|
264
|
+
request: Request object
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
Client certificate or None
|
268
|
+
"""
|
269
|
+
return getattr(request.state, 'client_certificate', None)
|
270
|
+
|
271
|
+
def get_client_roles(self, request: Request) -> List[str]:
|
272
|
+
"""
|
273
|
+
Get client roles from request state (backward compatibility).
|
274
|
+
|
275
|
+
Args:
|
276
|
+
request: Request object
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
List of client roles
|
280
|
+
"""
|
281
|
+
return getattr(request.state, 'client_roles', [])
|
282
|
+
|
283
|
+
def get_client_common_name(self, request: Request) -> str:
|
284
|
+
"""
|
285
|
+
Get client common name from request state (backward compatibility).
|
286
|
+
|
287
|
+
Args:
|
288
|
+
request: Request object
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
Client common name
|
292
|
+
"""
|
293
|
+
return getattr(request.state, 'client_common_name', 'unknown')
|
294
|
+
|
295
|
+
def is_mtls_authenticated(self, request: Request) -> bool:
|
296
|
+
"""
|
297
|
+
Check if request is mTLS authenticated (backward compatibility).
|
298
|
+
|
299
|
+
Args:
|
300
|
+
request: Request object
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
True if mTLS authenticated, False otherwise
|
304
|
+
"""
|
305
|
+
return hasattr(request.state, 'client_certificate') and request.state.client_certificate is not None
|