mcp-proxy-adapter 4.1.0__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 +705 -345
- 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 +17 -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 +97 -41
- 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 -63
- 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.0.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.0.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Main entry point for MCP Proxy Adapter.
|
4
|
+
|
5
|
+
This module allows running the MCP Proxy Adapter as a module:
|
6
|
+
python -m mcp_proxy_adapter
|
7
|
+
"""
|
8
|
+
|
9
|
+
from mcp_proxy_adapter.main import main
|
10
|
+
|
11
|
+
if __name__ == "__main__":
|
12
|
+
main()
|
mcp_proxy_adapter/api/app.py
CHANGED
@@ -3,6 +3,7 @@ Module for FastAPI application setup.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import json
|
6
|
+
import ssl
|
6
7
|
from typing import Any, Dict, List, Optional, Union
|
7
8
|
from contextlib import asynccontextmanager
|
8
9
|
|
@@ -17,6 +18,7 @@ from mcp_proxy_adapter.api.tools import get_tool_description, execute_tool
|
|
17
18
|
from mcp_proxy_adapter.config import config
|
18
19
|
from mcp_proxy_adapter.core.errors import MicroserviceError, NotFoundError
|
19
20
|
from mcp_proxy_adapter.core.logging import logger, RequestLogger
|
21
|
+
from mcp_proxy_adapter.core.ssl_utils import SSLUtils
|
20
22
|
from mcp_proxy_adapter.commands.command_registry import registry
|
21
23
|
from mcp_proxy_adapter.custom_openapi import custom_openapi_with_fallback
|
22
24
|
|
@@ -28,25 +30,91 @@ async def lifespan(app: FastAPI):
|
|
28
30
|
"""
|
29
31
|
# Startup events
|
30
32
|
from mcp_proxy_adapter.commands.command_registry import registry
|
31
|
-
from mcp_proxy_adapter.
|
32
|
-
from mcp_proxy_adapter.commands.health_command import HealthCommand
|
33
|
+
from mcp_proxy_adapter.core.proxy_registration import register_with_proxy, unregister_from_proxy
|
33
34
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
35
|
+
# Initialize system using unified logic
|
36
|
+
# This will load config, register custom commands, and discover auto-commands
|
37
|
+
init_result = await registry.reload_system()
|
37
38
|
|
38
|
-
|
39
|
-
|
39
|
+
logger.info(f"Application started with {init_result['total_commands']} commands registered")
|
40
|
+
logger.info(f"System initialization result: {init_result}")
|
40
41
|
|
41
|
-
#
|
42
|
-
|
42
|
+
# Register with proxy if enabled
|
43
|
+
server_config = config.get("server", {})
|
44
|
+
server_host = server_config.get("host", "0.0.0.0")
|
45
|
+
server_port = server_config.get("port", 8000)
|
43
46
|
|
44
|
-
|
47
|
+
# Determine server URL based on SSL configuration
|
48
|
+
ssl_config = config.get("ssl", {})
|
49
|
+
if ssl_config.get("enabled", False):
|
50
|
+
protocol = "https"
|
51
|
+
else:
|
52
|
+
protocol = "http"
|
53
|
+
|
54
|
+
# Use localhost for external access if host is 0.0.0.0
|
55
|
+
if server_host == "0.0.0.0":
|
56
|
+
server_host = "localhost"
|
57
|
+
|
58
|
+
server_url = f"{protocol}://{server_host}:{server_port}"
|
59
|
+
|
60
|
+
# Attempt proxy registration
|
61
|
+
registration_success = await register_with_proxy(server_url)
|
62
|
+
if registration_success:
|
63
|
+
logger.info("✅ Proxy registration completed successfully")
|
64
|
+
else:
|
65
|
+
logger.info("ℹ️ Proxy registration is disabled or failed")
|
45
66
|
|
46
67
|
yield # Application is running
|
47
68
|
|
48
69
|
# Shutdown events
|
49
70
|
logger.info("Application shutting down")
|
71
|
+
|
72
|
+
# Unregister from proxy if enabled
|
73
|
+
unregistration_success = await unregister_from_proxy()
|
74
|
+
if unregistration_success:
|
75
|
+
logger.info("✅ Proxy unregistration completed successfully")
|
76
|
+
else:
|
77
|
+
logger.warning("⚠️ Proxy unregistration failed or was disabled")
|
78
|
+
|
79
|
+
|
80
|
+
def create_ssl_context() -> Optional[ssl.SSLContext]:
|
81
|
+
"""
|
82
|
+
Create SSL context based on configuration.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
SSL context if SSL is enabled and properly configured, None otherwise
|
86
|
+
"""
|
87
|
+
ssl_config = config.get("ssl", {})
|
88
|
+
|
89
|
+
if not ssl_config.get("enabled", False):
|
90
|
+
logger.info("SSL is disabled in configuration")
|
91
|
+
return None
|
92
|
+
|
93
|
+
cert_file = ssl_config.get("cert_file")
|
94
|
+
key_file = ssl_config.get("key_file")
|
95
|
+
|
96
|
+
if not cert_file or not key_file:
|
97
|
+
logger.warning("SSL enabled but certificate or key file not specified")
|
98
|
+
return None
|
99
|
+
|
100
|
+
try:
|
101
|
+
# Create SSL context using SSLUtils
|
102
|
+
ssl_context = SSLUtils.create_ssl_context(
|
103
|
+
cert_file=cert_file,
|
104
|
+
key_file=key_file,
|
105
|
+
ca_cert=ssl_config.get("ca_cert"),
|
106
|
+
verify_client=ssl_config.get("verify_client", False),
|
107
|
+
cipher_suites=ssl_config.get("cipher_suites", []),
|
108
|
+
min_tls_version=ssl_config.get("min_tls_version", "1.2"),
|
109
|
+
max_tls_version=ssl_config.get("max_tls_version", "1.3")
|
110
|
+
)
|
111
|
+
|
112
|
+
logger.info(f"SSL context created successfully for mode: {ssl_config.get('mode', 'https_only')}")
|
113
|
+
return ssl_context
|
114
|
+
|
115
|
+
except Exception as e:
|
116
|
+
logger.error(f"Failed to create SSL context: {e}")
|
117
|
+
return None
|
50
118
|
|
51
119
|
|
52
120
|
def create_app(title: Optional[str] = None, description: Optional[str] = None, version: Optional[str] = None) -> FastAPI:
|
@@ -60,7 +128,66 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
60
128
|
|
61
129
|
Returns:
|
62
130
|
Configured FastAPI application.
|
131
|
+
|
132
|
+
Raises:
|
133
|
+
SystemExit: If authentication is enabled but required files are missing (security issue)
|
63
134
|
"""
|
135
|
+
# Security check: Validate all authentication configurations before startup
|
136
|
+
security_errors = []
|
137
|
+
|
138
|
+
# Check roles configuration
|
139
|
+
roles_config = config.get("roles", {})
|
140
|
+
if roles_config.get("enabled", False):
|
141
|
+
roles_config_path = roles_config.get("config_file", "schemas/roles_schema.json")
|
142
|
+
from pathlib import Path
|
143
|
+
if not Path(roles_config_path).exists():
|
144
|
+
security_errors.append(f"Roles are enabled but schema file not found: {roles_config_path}")
|
145
|
+
|
146
|
+
# Check SSL configuration
|
147
|
+
ssl_config = config.get("ssl", {})
|
148
|
+
if ssl_config.get("enabled", False):
|
149
|
+
# Check SSL certificate files
|
150
|
+
cert_file = ssl_config.get("cert_file")
|
151
|
+
key_file = ssl_config.get("key_file")
|
152
|
+
|
153
|
+
if cert_file and not Path(cert_file).exists():
|
154
|
+
security_errors.append(f"SSL is enabled but certificate file not found: {cert_file}")
|
155
|
+
|
156
|
+
if key_file and not Path(key_file).exists():
|
157
|
+
security_errors.append(f"SSL is enabled but private key file not found: {key_file}")
|
158
|
+
|
159
|
+
# Check mTLS configuration
|
160
|
+
if ssl_config.get("mode") == "mtls":
|
161
|
+
ca_cert = ssl_config.get("ca_cert")
|
162
|
+
if ca_cert and not Path(ca_cert).exists():
|
163
|
+
security_errors.append(f"mTLS is enabled but CA certificate file not found: {ca_cert}")
|
164
|
+
|
165
|
+
# Check token authentication configuration
|
166
|
+
token_auth_config = ssl_config.get("token_auth", {})
|
167
|
+
if token_auth_config.get("enabled", False):
|
168
|
+
tokens_file = token_auth_config.get("tokens_file", "tokens.json")
|
169
|
+
if not Path(tokens_file).exists():
|
170
|
+
security_errors.append(f"Token authentication is enabled but tokens file not found: {tokens_file}")
|
171
|
+
|
172
|
+
# Check general authentication
|
173
|
+
if config.get("auth_enabled", False):
|
174
|
+
# If auth is enabled, check if any authentication method is properly configured
|
175
|
+
ssl_enabled = ssl_config.get("enabled", False)
|
176
|
+
roles_enabled = roles_config.get("enabled", False)
|
177
|
+
token_auth_enabled = token_auth_config.get("enabled", False)
|
178
|
+
|
179
|
+
if not (ssl_enabled or roles_enabled or token_auth_enabled):
|
180
|
+
security_errors.append("Authentication is enabled but no authentication method is properly configured")
|
181
|
+
|
182
|
+
# If there are security errors, block startup
|
183
|
+
if security_errors:
|
184
|
+
logger.critical("CRITICAL SECURITY ERROR: Authentication configuration issues detected:")
|
185
|
+
for error in security_errors:
|
186
|
+
logger.critical(f" - {error}")
|
187
|
+
logger.critical("Server startup blocked for security reasons.")
|
188
|
+
logger.critical("Please fix authentication configuration or disable authentication features.")
|
189
|
+
raise SystemExit(1)
|
190
|
+
|
64
191
|
# Use provided parameters or defaults
|
65
192
|
app_title = title or "MCP Proxy Adapter"
|
66
193
|
app_description = description or "JSON-RPC API for interacting with MCP Proxy"
|
@@ -186,7 +313,7 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
186
313
|
command_name = command_data["command"]
|
187
314
|
params = command_data.get("params", {})
|
188
315
|
|
189
|
-
req_logger.
|
316
|
+
req_logger.debug(f"Executing command via /cmd: {command_name}, params: {params}")
|
190
317
|
|
191
318
|
# Check if command exists
|
192
319
|
if not registry.command_exists(command_name):
|
@@ -39,17 +39,32 @@ async def execute_command(command_name: str, params: Dict[str, Any], request_id:
|
|
39
39
|
try:
|
40
40
|
log.info(f"Executing command: {command_name}")
|
41
41
|
|
42
|
+
# Execute before command hooks
|
43
|
+
try:
|
44
|
+
from mcp_proxy_adapter.commands.hooks import hooks
|
45
|
+
hooks.execute_before_command_hooks(command_name, params)
|
46
|
+
log.debug(f"Executed before command hooks for: {command_name}")
|
47
|
+
except Exception as e:
|
48
|
+
log.warning(f"Failed to execute before command hooks: {e}")
|
49
|
+
|
42
50
|
# Get command class from registry and execute with parameters
|
43
51
|
start_time = time.time()
|
44
52
|
|
45
53
|
# Use Command.run that handles instances with dependencies properly
|
46
|
-
command_class = registry.
|
54
|
+
command_class = registry.get_command(command_name)
|
47
55
|
result = await command_class.run(**params)
|
48
56
|
|
49
57
|
execution_time = time.time() - start_time
|
50
58
|
|
51
59
|
log.info(f"Command '{command_name}' executed in {execution_time:.3f} sec")
|
52
60
|
|
61
|
+
# Execute after command hooks
|
62
|
+
try:
|
63
|
+
hooks.execute_after_command_hooks(command_name, params, result)
|
64
|
+
log.debug(f"Executed after command hooks for: {command_name}")
|
65
|
+
except Exception as e:
|
66
|
+
log.warning(f"Failed to execute after command hooks: {e}")
|
67
|
+
|
53
68
|
# Return result
|
54
69
|
return result.to_dict()
|
55
70
|
except NotFoundError as e:
|
@@ -6,44 +6,45 @@ This package contains middleware components for request processing.
|
|
6
6
|
from fastapi import FastAPI
|
7
7
|
|
8
8
|
from mcp_proxy_adapter.core.logging import logger
|
9
|
+
from mcp_proxy_adapter.config import config
|
9
10
|
from .base import BaseMiddleware
|
10
|
-
from .
|
11
|
-
from .
|
12
|
-
from .auth import AuthMiddleware
|
13
|
-
from .rate_limit import RateLimitMiddleware
|
14
|
-
from .performance import PerformanceMiddleware
|
11
|
+
from .factory import MiddlewareFactory
|
12
|
+
from .protocol_middleware import setup_protocol_middleware
|
15
13
|
|
16
14
|
def setup_middleware(app: FastAPI) -> None:
|
17
15
|
"""
|
18
|
-
Sets up middleware for application.
|
16
|
+
Sets up middleware for application using the new middleware factory.
|
19
17
|
|
20
18
|
Args:
|
21
19
|
app: FastAPI application instance.
|
22
20
|
"""
|
23
|
-
#
|
24
|
-
app.
|
21
|
+
# Create middleware factory
|
22
|
+
factory = MiddlewareFactory(app, config.get_all())
|
25
23
|
|
26
|
-
#
|
27
|
-
|
24
|
+
# Validate middleware configuration
|
25
|
+
if not factory.validate_middleware_config():
|
26
|
+
logger.error("Middleware configuration validation failed")
|
27
|
+
raise SystemExit(1)
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
if config.get("rate_limit_enabled", False):
|
32
|
-
app.add_middleware(
|
33
|
-
RateLimitMiddleware,
|
34
|
-
rate_limit=config.get("rate_limit", 100),
|
35
|
-
time_window=config.get("rate_limit_window", 60)
|
36
|
-
)
|
29
|
+
logger.info("Using unified security middleware")
|
30
|
+
middleware_list = factory.create_all_middleware()
|
37
31
|
|
38
|
-
#
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
32
|
+
# Add middleware to application
|
33
|
+
for middleware in middleware_list:
|
34
|
+
# For ASGI middleware, we need to wrap the application
|
35
|
+
if hasattr(middleware, 'dispatch'):
|
36
|
+
# This is a proper ASGI middleware
|
37
|
+
app.middleware("http")(middleware.dispatch)
|
38
|
+
else:
|
39
|
+
logger.warning(f"Middleware {middleware.__class__.__name__} doesn't have dispatch method")
|
40
|
+
|
41
|
+
# Add protocol middleware (always needed)
|
42
|
+
setup_protocol_middleware(app)
|
48
43
|
|
49
|
-
|
44
|
+
# Log middleware information
|
45
|
+
middleware_info = factory.get_middleware_info()
|
46
|
+
logger.info(f"Middleware setup completed:")
|
47
|
+
logger.info(f" - Total middleware: {middleware_info['total_middleware']}")
|
48
|
+
logger.info(f" - Types: {', '.join(middleware_info['middleware_types'])}")
|
49
|
+
logger.info(f" - Security enabled: {middleware_info['security_enabled']}")
|
50
|
+
|
@@ -0,0 +1,235 @@
|
|
1
|
+
"""
|
2
|
+
Auth Middleware Adapter for backward compatibility.
|
3
|
+
|
4
|
+
This module provides an adapter that maintains the same interface as AuthMiddleware
|
5
|
+
while using the new SecurityMiddleware internally.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
from typing import Dict, List, Optional, Callable, Awaitable
|
10
|
+
|
11
|
+
from fastapi import Request, Response
|
12
|
+
from starlette.responses import JSONResponse
|
13
|
+
|
14
|
+
from mcp_proxy_adapter.core.logging import logger
|
15
|
+
from .base import BaseMiddleware
|
16
|
+
from .security import SecurityMiddleware
|
17
|
+
|
18
|
+
|
19
|
+
class AuthMiddlewareAdapter(BaseMiddleware):
|
20
|
+
"""
|
21
|
+
Adapter for AuthMiddleware that uses SecurityMiddleware internally.
|
22
|
+
|
23
|
+
Maintains the same interface as the original AuthMiddleware for backward compatibility.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, app, api_keys: Dict[str, str] = None,
|
27
|
+
public_paths: List[str] = None, auth_enabled: bool = True):
|
28
|
+
"""
|
29
|
+
Initialize auth middleware adapter.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
app: FastAPI application
|
33
|
+
api_keys: Dictionary with API keys (key: username)
|
34
|
+
public_paths: List of paths accessible without authentication
|
35
|
+
auth_enabled: Flag to enable/disable authentication
|
36
|
+
"""
|
37
|
+
super().__init__(app)
|
38
|
+
|
39
|
+
# Store original parameters for backward compatibility
|
40
|
+
self.api_keys = api_keys or {}
|
41
|
+
self.public_paths = public_paths or [
|
42
|
+
"/docs",
|
43
|
+
"/redoc",
|
44
|
+
"/openapi.json",
|
45
|
+
"/health"
|
46
|
+
]
|
47
|
+
self.auth_enabled = auth_enabled
|
48
|
+
|
49
|
+
# Create internal security middleware
|
50
|
+
self.security_middleware = self._create_security_middleware()
|
51
|
+
|
52
|
+
logger.info(f"AuthMiddlewareAdapter initialized: auth_enabled={auth_enabled}, "
|
53
|
+
f"api_keys_count={len(self.api_keys)}, public_paths_count={len(self.public_paths)}")
|
54
|
+
|
55
|
+
def _create_security_middleware(self) -> SecurityMiddleware:
|
56
|
+
"""
|
57
|
+
Create internal SecurityMiddleware with AuthMiddleware configuration.
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
SecurityMiddleware instance
|
61
|
+
"""
|
62
|
+
# Convert AuthMiddleware config to SecurityMiddleware config
|
63
|
+
security_config = {
|
64
|
+
"security": {
|
65
|
+
"enabled": self.auth_enabled,
|
66
|
+
"auth": {
|
67
|
+
"enabled": self.auth_enabled,
|
68
|
+
"methods": ["api_key"],
|
69
|
+
"api_keys": self.api_keys
|
70
|
+
},
|
71
|
+
"ssl": {
|
72
|
+
"enabled": False
|
73
|
+
},
|
74
|
+
"permissions": {
|
75
|
+
"enabled": False
|
76
|
+
},
|
77
|
+
"rate_limit": {
|
78
|
+
"enabled": False
|
79
|
+
},
|
80
|
+
"public_paths": self.public_paths
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
return SecurityMiddleware(self.app, security_config)
|
85
|
+
|
86
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
87
|
+
"""
|
88
|
+
Process request using internal SecurityMiddleware with legacy API key handling.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
request: Request object
|
92
|
+
call_next: Next handler
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
Response object
|
96
|
+
"""
|
97
|
+
# Check if authentication is disabled
|
98
|
+
if not self.auth_enabled:
|
99
|
+
logger.debug("Authentication is disabled, skipping authentication check")
|
100
|
+
return await call_next(request)
|
101
|
+
|
102
|
+
# Check if path is public
|
103
|
+
path = request.url.path
|
104
|
+
if self._is_public_path(path):
|
105
|
+
return await call_next(request)
|
106
|
+
|
107
|
+
# Extract API key from various sources (legacy compatibility)
|
108
|
+
api_key = self._extract_api_key(request)
|
109
|
+
|
110
|
+
# Check for API key in JSON-RPC request body if not found in headers/query
|
111
|
+
if not api_key and request.method in ["POST", "PUT", "PATCH"]:
|
112
|
+
try:
|
113
|
+
body = await request.body()
|
114
|
+
if body:
|
115
|
+
try:
|
116
|
+
body_json = json.loads(body.decode("utf-8"))
|
117
|
+
# Look for API key in params of JSON-RPC object
|
118
|
+
if isinstance(body_json, dict) and "params" in body_json:
|
119
|
+
api_key = body_json.get("params", {}).get("api_key")
|
120
|
+
except json.JSONDecodeError:
|
121
|
+
pass
|
122
|
+
except Exception:
|
123
|
+
pass
|
124
|
+
|
125
|
+
if api_key:
|
126
|
+
# Validate API key
|
127
|
+
username = self._validate_api_key(api_key)
|
128
|
+
if username:
|
129
|
+
# Store username in request state for backward compatibility
|
130
|
+
request.state.username = username
|
131
|
+
logger.debug(f"API key authentication successful for {username}")
|
132
|
+
return await call_next(request)
|
133
|
+
else:
|
134
|
+
logger.warning(f"Invalid API key provided | Path: {path}")
|
135
|
+
return self._create_error_response("Invalid API key", 401)
|
136
|
+
else:
|
137
|
+
logger.warning(f"API key not provided | Path: {path}")
|
138
|
+
return self._create_error_response("API key not provided", 401)
|
139
|
+
|
140
|
+
def _extract_api_key(self, request: Request) -> Optional[str]:
|
141
|
+
"""
|
142
|
+
Extract API key from request (legacy compatibility).
|
143
|
+
|
144
|
+
Args:
|
145
|
+
request: Request object
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
API key or None
|
149
|
+
"""
|
150
|
+
# Check for API key in header
|
151
|
+
api_key = request.headers.get("X-API-Key")
|
152
|
+
|
153
|
+
if not api_key:
|
154
|
+
# Check for API key in query parameters
|
155
|
+
api_key = request.query_params.get("api_key")
|
156
|
+
|
157
|
+
# Note: Body extraction is handled in dispatch method
|
158
|
+
# This method only handles headers and query parameters
|
159
|
+
|
160
|
+
return api_key
|
161
|
+
|
162
|
+
def _is_public_path(self, path: str) -> bool:
|
163
|
+
"""
|
164
|
+
Check if the path is public (doesn't require authentication).
|
165
|
+
|
166
|
+
Args:
|
167
|
+
path: Request path
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
True if path is public, False otherwise
|
171
|
+
"""
|
172
|
+
return any(path.startswith(public_path) for public_path in self.public_paths)
|
173
|
+
|
174
|
+
def _validate_api_key(self, api_key: str) -> Optional[str]:
|
175
|
+
"""
|
176
|
+
Validate API key and return username.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
api_key: API key to validate
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
Username if valid, None otherwise
|
183
|
+
"""
|
184
|
+
return self.api_keys.get(api_key)
|
185
|
+
|
186
|
+
def _create_error_response(self, message: str, status_code: int) -> JSONResponse:
|
187
|
+
"""
|
188
|
+
Create error response in AuthMiddleware format.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
message: Error message
|
192
|
+
status_code: HTTP status code
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
JSONResponse with error
|
196
|
+
"""
|
197
|
+
return JSONResponse(
|
198
|
+
status_code=status_code,
|
199
|
+
content={
|
200
|
+
"jsonrpc": "2.0",
|
201
|
+
"error": {
|
202
|
+
"code": -32000 if status_code == 401 else -32603,
|
203
|
+
"message": message,
|
204
|
+
"data": {
|
205
|
+
"auth_type": "api_key",
|
206
|
+
"status_code": status_code
|
207
|
+
}
|
208
|
+
},
|
209
|
+
"id": None
|
210
|
+
}
|
211
|
+
)
|
212
|
+
|
213
|
+
def get_username(self, request: Request) -> Optional[str]:
|
214
|
+
"""
|
215
|
+
Get username from request state (backward compatibility).
|
216
|
+
|
217
|
+
Args:
|
218
|
+
request: Request object
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
Username or None
|
222
|
+
"""
|
223
|
+
return getattr(request.state, 'username', None)
|
224
|
+
|
225
|
+
def is_authenticated(self, request: Request) -> bool:
|
226
|
+
"""
|
227
|
+
Check if request is authenticated (backward compatibility).
|
228
|
+
|
229
|
+
Args:
|
230
|
+
request: Request object
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
True if authenticated, False otherwise
|
234
|
+
"""
|
235
|
+
return hasattr(request.state, 'username') and request.state.username is not None
|
@@ -17,6 +17,15 @@ class ErrorHandlingMiddleware(BaseMiddleware):
|
|
17
17
|
Middleware for handling and formatting errors.
|
18
18
|
"""
|
19
19
|
|
20
|
+
def __init__(self, app):
|
21
|
+
"""
|
22
|
+
Initialize error handling middleware.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
app: FastAPI application
|
26
|
+
"""
|
27
|
+
super().__init__(app)
|
28
|
+
|
20
29
|
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
21
30
|
"""
|
22
31
|
Processes request and catches errors.
|