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.
Files changed (101) hide show
  1. mcp_proxy_adapter/__main__.py +12 -0
  2. mcp_proxy_adapter/api/app.py +138 -11
  3. mcp_proxy_adapter/api/handlers.py +16 -1
  4. mcp_proxy_adapter/api/middleware/__init__.py +30 -29
  5. mcp_proxy_adapter/api/middleware/auth_adapter.py +235 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +219 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/mtls_adapter.py +305 -0
  10. mcp_proxy_adapter/api/middleware/mtls_middleware.py +296 -0
  11. mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
  12. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +241 -0
  13. mcp_proxy_adapter/api/middleware/roles_adapter.py +365 -0
  14. mcp_proxy_adapter/api/middleware/roles_middleware.py +381 -0
  15. mcp_proxy_adapter/api/middleware/security.py +376 -0
  16. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +261 -0
  17. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  18. mcp_proxy_adapter/commands/__init__.py +13 -4
  19. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  20. mcp_proxy_adapter/commands/base.py +61 -30
  21. mcp_proxy_adapter/commands/builtin_commands.py +89 -0
  22. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  23. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  24. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  25. mcp_proxy_adapter/commands/command_registry.py +703 -354
  26. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  27. mcp_proxy_adapter/commands/health_command.py +7 -0
  28. mcp_proxy_adapter/commands/hooks.py +200 -167
  29. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  30. mcp_proxy_adapter/commands/load_command.py +176 -0
  31. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  32. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  33. mcp_proxy_adapter/commands/proxy_registration_command.py +268 -0
  34. mcp_proxy_adapter/commands/reload_command.py +48 -50
  35. mcp_proxy_adapter/commands/result.py +1 -0
  36. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  37. mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
  38. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  39. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  40. mcp_proxy_adapter/commands/unload_command.py +158 -0
  41. mcp_proxy_adapter/config.py +99 -2
  42. mcp_proxy_adapter/core/auth_validator.py +606 -0
  43. mcp_proxy_adapter/core/certificate_utils.py +827 -0
  44. mcp_proxy_adapter/core/config_converter.py +405 -0
  45. mcp_proxy_adapter/core/config_validator.py +218 -0
  46. mcp_proxy_adapter/core/logging.py +11 -0
  47. mcp_proxy_adapter/core/protocol_manager.py +226 -0
  48. mcp_proxy_adapter/core/proxy_registration.py +270 -0
  49. mcp_proxy_adapter/core/role_utils.py +426 -0
  50. mcp_proxy_adapter/core/security_adapter.py +373 -0
  51. mcp_proxy_adapter/core/security_factory.py +239 -0
  52. mcp_proxy_adapter/core/settings.py +1 -0
  53. mcp_proxy_adapter/core/ssl_utils.py +233 -0
  54. mcp_proxy_adapter/core/transport_manager.py +292 -0
  55. mcp_proxy_adapter/custom_openapi.py +22 -11
  56. mcp_proxy_adapter/examples/basic_server/config.json +58 -23
  57. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +54 -0
  58. mcp_proxy_adapter/examples/basic_server/config_http.json +70 -0
  59. mcp_proxy_adapter/examples/basic_server/config_http_only.json +52 -0
  60. mcp_proxy_adapter/examples/basic_server/config_https.json +58 -0
  61. mcp_proxy_adapter/examples/basic_server/config_mtls.json +58 -0
  62. mcp_proxy_adapter/examples/basic_server/config_ssl.json +46 -0
  63. mcp_proxy_adapter/examples/basic_server/server.py +12 -1
  64. mcp_proxy_adapter/examples/custom_commands/__init__.py +1 -1
  65. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +339 -23
  66. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +105 -0
  67. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +129 -0
  68. mcp_proxy_adapter/examples/custom_commands/config.json +101 -18
  69. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +46 -0
  70. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +46 -0
  71. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +33 -0
  72. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +46 -0
  73. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +33 -0
  74. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +33 -0
  75. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +1 -0
  76. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +629 -0
  77. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +103 -0
  78. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +129 -0
  79. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +278 -0
  80. mcp_proxy_adapter/examples/custom_commands/server.py +92 -68
  81. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +75 -0
  82. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +299 -0
  83. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +278 -0
  84. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +27 -0
  85. mcp_proxy_adapter/examples/custom_commands/test_registry.py +23 -0
  86. mcp_proxy_adapter/examples/custom_commands/test_simple.py +19 -0
  87. mcp_proxy_adapter/examples/custom_project_example/README.md +103 -0
  88. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +103 -0
  89. mcp_proxy_adapter/examples/simple_custom_commands/README.md +149 -0
  90. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +149 -0
  91. mcp_proxy_adapter/main.py +175 -0
  92. mcp_proxy_adapter/schemas/roles_schema.json +162 -0
  93. mcp_proxy_adapter/tests/unit/test_config.py +53 -0
  94. mcp_proxy_adapter/version.py +1 -1
  95. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/METADATA +2 -1
  96. mcp_proxy_adapter-6.0.0.dist-info/RECORD +179 -0
  97. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  98. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  99. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/WHEEL +0 -0
  100. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/licenses/LICENSE +0 -0
  101. {mcp_proxy_adapter-4.1.1.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()
@@ -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.commands.help_command import HelpCommand
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
- # Register built-in commands if they don't exist (user can override them)
35
- if not registry.command_exists("help"):
36
- registry.register(HelpCommand)
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
- if not registry.command_exists("health"):
39
- registry.register(HealthCommand)
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
- # Discover and register additional commands automatically
42
- registry.discover_commands()
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
- logger.info(f"Application started with {len(registry.get_all_commands())} commands registered")
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.info(f"Executing command via /cmd: {command_name}, params: {params}")
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.get_command_with_priority(command_name)
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 .logging import LoggingMiddleware
11
- from .error_handling import ErrorHandlingMiddleware
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
- # Add error handling middleware first (last to execute)
24
- app.add_middleware(ErrorHandlingMiddleware)
21
+ # Create middleware factory
22
+ factory = MiddlewareFactory(app, config.get_all())
25
23
 
26
- # Add logging middleware
27
- app.add_middleware(LoggingMiddleware)
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
- # Add rate limiting middleware if configured
30
- from mcp_proxy_adapter.config import config
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
- # Добавляем authentication middleware с явным указанием auth_enabled
39
- auth_enabled = config.get("auth_enabled", False)
40
- app.add_middleware(
41
- AuthMiddleware,
42
- api_keys=config.get("api_keys", {}),
43
- auth_enabled=auth_enabled
44
- )
45
-
46
- # Add performance middleware
47
- app.add_middleware(PerformanceMiddleware)
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
- logger.info(f"Middleware setup completed. Auth enabled: {auth_enabled}")
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.