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,365 @@
|
|
1
|
+
"""
|
2
|
+
Roles Middleware Adapter for backward compatibility.
|
3
|
+
|
4
|
+
This module provides an adapter that maintains the same interface as RolesMiddleware
|
5
|
+
while using the new SecurityMiddleware internally.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import logging
|
10
|
+
from typing import Dict, List, Optional, Any, Set, Callable, Awaitable
|
11
|
+
from pathlib import Path
|
12
|
+
from cryptography import x509
|
13
|
+
|
14
|
+
from fastapi import Request, Response
|
15
|
+
from starlette.responses import JSONResponse
|
16
|
+
|
17
|
+
from mcp_proxy_adapter.core.logging import logger
|
18
|
+
from mcp_proxy_adapter.core.auth_validator import AuthValidator
|
19
|
+
from mcp_proxy_adapter.core.role_utils import RoleUtils
|
20
|
+
from mcp_proxy_adapter.core.certificate_utils import CertificateUtils
|
21
|
+
from .base import BaseMiddleware
|
22
|
+
from .security import SecurityMiddleware
|
23
|
+
|
24
|
+
|
25
|
+
class RolesMiddlewareAdapter(BaseMiddleware):
|
26
|
+
"""
|
27
|
+
Adapter for RolesMiddleware that uses SecurityMiddleware internally.
|
28
|
+
|
29
|
+
Maintains the same interface as the original RolesMiddleware for backward compatibility.
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(self, app, roles_config_path: str):
|
33
|
+
"""
|
34
|
+
Initialize roles middleware adapter.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
app: FastAPI application
|
38
|
+
roles_config_path: Path to roles configuration file
|
39
|
+
"""
|
40
|
+
super().__init__(app)
|
41
|
+
|
42
|
+
# Store original parameters for backward compatibility
|
43
|
+
self.roles_config_path = roles_config_path
|
44
|
+
self.auth_validator = AuthValidator()
|
45
|
+
self.role_utils = RoleUtils()
|
46
|
+
self.certificate_utils = CertificateUtils()
|
47
|
+
|
48
|
+
# Load roles configuration
|
49
|
+
self.roles_config = self._load_roles_config()
|
50
|
+
|
51
|
+
# Check if roles are enabled and config file exists
|
52
|
+
if not self.roles_config.get("enabled", True):
|
53
|
+
logger.info("Roles middleware disabled by configuration")
|
54
|
+
self.enabled = False
|
55
|
+
return
|
56
|
+
|
57
|
+
# Extract configuration
|
58
|
+
self.enabled = self.roles_config.get("enabled", True)
|
59
|
+
self.default_policy = self.roles_config.get("default_policy", {})
|
60
|
+
self.roles = self.roles_config.get("roles", {})
|
61
|
+
self.server_roles = self.roles_config.get("server_roles", {})
|
62
|
+
self.role_hierarchy = self.roles_config.get("role_hierarchy", {})
|
63
|
+
|
64
|
+
# Create internal security middleware
|
65
|
+
self.security_middleware = self._create_security_middleware()
|
66
|
+
|
67
|
+
logger.info(f"RolesMiddlewareAdapter initialized: enabled={self.enabled}, "
|
68
|
+
f"roles_count={len(self.roles)}, "
|
69
|
+
f"server_roles_count={len(self.server_roles)}")
|
70
|
+
|
71
|
+
def _load_roles_config(self) -> Dict[str, Any]:
|
72
|
+
"""
|
73
|
+
Load roles configuration from file.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
Roles configuration dictionary
|
77
|
+
"""
|
78
|
+
try:
|
79
|
+
config_path = Path(self.roles_config_path)
|
80
|
+
if not config_path.exists():
|
81
|
+
logger.error(f"Roles config file not found: {self.roles_config_path}")
|
82
|
+
logger.error("Roles middleware will be disabled. Please create the roles schema file.")
|
83
|
+
return {"enabled": False}
|
84
|
+
|
85
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
86
|
+
config = json.load(f)
|
87
|
+
|
88
|
+
logger.info(f"Roles configuration loaded from {self.roles_config_path}")
|
89
|
+
return config
|
90
|
+
|
91
|
+
except Exception as e:
|
92
|
+
logger.error(f"Failed to load roles configuration: {e}")
|
93
|
+
logger.error("Roles middleware will be disabled due to configuration error.")
|
94
|
+
return {"enabled": False}
|
95
|
+
|
96
|
+
def _create_security_middleware(self) -> SecurityMiddleware:
|
97
|
+
"""
|
98
|
+
Create internal SecurityMiddleware with RolesMiddleware configuration.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
SecurityMiddleware instance
|
102
|
+
"""
|
103
|
+
# Convert RolesMiddleware config to SecurityMiddleware config
|
104
|
+
security_config = {
|
105
|
+
"security": {
|
106
|
+
"enabled": self.enabled,
|
107
|
+
"auth": {
|
108
|
+
"enabled": False
|
109
|
+
},
|
110
|
+
"ssl": {
|
111
|
+
"enabled": False
|
112
|
+
},
|
113
|
+
"permissions": {
|
114
|
+
"enabled": self.enabled,
|
115
|
+
"roles_file": self.roles_config_path,
|
116
|
+
"default_role": self.default_policy.get("default_role", "user"),
|
117
|
+
"deny_by_default": self.default_policy.get("deny_by_default", True)
|
118
|
+
},
|
119
|
+
"rate_limit": {
|
120
|
+
"enabled": False
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
return SecurityMiddleware(self.app, security_config)
|
126
|
+
|
127
|
+
async def before_request(self, request: Request) -> None:
|
128
|
+
"""
|
129
|
+
Process request before calling the main handler.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
request: FastAPI request object
|
133
|
+
"""
|
134
|
+
if not self.enabled:
|
135
|
+
return
|
136
|
+
|
137
|
+
try:
|
138
|
+
# Use SecurityMiddleware for validation
|
139
|
+
await self.security_middleware.before_request(request)
|
140
|
+
|
141
|
+
# Additional roles-specific processing
|
142
|
+
client_roles = self._get_client_roles(request)
|
143
|
+
if client_roles:
|
144
|
+
# Validate roles against server roles
|
145
|
+
if not self._validate_roles(client_roles):
|
146
|
+
raise ValueError("Access denied: insufficient roles")
|
147
|
+
|
148
|
+
# Store roles in request state for backward compatibility
|
149
|
+
request.state.client_roles = client_roles
|
150
|
+
request.state.role_validation_passed = True
|
151
|
+
|
152
|
+
logger.debug(f"Role validation successful for roles: {client_roles}")
|
153
|
+
|
154
|
+
except Exception as e:
|
155
|
+
logger.error(f"Role validation failed: {e}")
|
156
|
+
raise
|
157
|
+
|
158
|
+
def _get_client_roles(self, request: Request) -> List[str]:
|
159
|
+
"""
|
160
|
+
Get client roles from various sources.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
request: FastAPI request object
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
List of client roles
|
167
|
+
"""
|
168
|
+
roles = []
|
169
|
+
|
170
|
+
# Get roles from request state (from other middleware)
|
171
|
+
if hasattr(request.state, 'client_roles'):
|
172
|
+
roles.extend(request.state.client_roles)
|
173
|
+
|
174
|
+
# Get roles from certificate if available
|
175
|
+
if hasattr(request.state, 'client_certificate'):
|
176
|
+
cert_roles = self._extract_roles_from_certificate(request.state.client_certificate)
|
177
|
+
roles.extend(cert_roles)
|
178
|
+
|
179
|
+
# Get roles from headers
|
180
|
+
header_roles = request.headers.get("X-Client-Roles")
|
181
|
+
if header_roles:
|
182
|
+
try:
|
183
|
+
header_roles_list = json.loads(header_roles)
|
184
|
+
if isinstance(header_roles_list, list):
|
185
|
+
roles.extend(header_roles_list)
|
186
|
+
except json.JSONDecodeError:
|
187
|
+
# Treat as comma-separated string
|
188
|
+
roles.extend([role.strip() for role in header_roles.split(",")])
|
189
|
+
|
190
|
+
# Remove duplicates and return
|
191
|
+
return list(set(roles))
|
192
|
+
|
193
|
+
def _extract_roles_from_certificate(self, cert: x509.Certificate) -> List[str]:
|
194
|
+
"""
|
195
|
+
Extract roles from client certificate.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
cert: Client certificate
|
199
|
+
|
200
|
+
Returns:
|
201
|
+
List of roles
|
202
|
+
"""
|
203
|
+
try:
|
204
|
+
roles = []
|
205
|
+
|
206
|
+
# Extract from subject alternative names
|
207
|
+
san = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
208
|
+
if san:
|
209
|
+
for name in san.value:
|
210
|
+
if isinstance(name, x509.DNSName):
|
211
|
+
if name.value.startswith("role="):
|
212
|
+
role = name.value.split("=", 1)[1]
|
213
|
+
roles.append(role)
|
214
|
+
|
215
|
+
# Extract from subject
|
216
|
+
subject = cert.subject
|
217
|
+
for attr in subject:
|
218
|
+
if attr.oid.dotted_string == "2.5.4.3": # Common Name
|
219
|
+
if attr.value.startswith("role="):
|
220
|
+
role = attr.value.split("=", 1)[1]
|
221
|
+
roles.append(role)
|
222
|
+
|
223
|
+
return roles
|
224
|
+
|
225
|
+
except Exception as e:
|
226
|
+
logger.error(f"Failed to extract roles from certificate: {e}")
|
227
|
+
return []
|
228
|
+
|
229
|
+
def _validate_roles(self, client_roles: List[str]) -> bool:
|
230
|
+
"""
|
231
|
+
Validate client roles against server roles.
|
232
|
+
|
233
|
+
Args:
|
234
|
+
client_roles: List of client roles
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
True if roles are valid, False otherwise
|
238
|
+
"""
|
239
|
+
if not client_roles:
|
240
|
+
return self.default_policy.get("allow_empty_roles", False)
|
241
|
+
|
242
|
+
# Check if any client role matches server roles
|
243
|
+
for client_role in client_roles:
|
244
|
+
if client_role in self.server_roles:
|
245
|
+
return True
|
246
|
+
|
247
|
+
# Check role hierarchy
|
248
|
+
if client_role in self.role_hierarchy:
|
249
|
+
inherited_roles = self.role_hierarchy[client_role]
|
250
|
+
for inherited_role in inherited_roles:
|
251
|
+
if inherited_role in self.server_roles:
|
252
|
+
return True
|
253
|
+
|
254
|
+
return False
|
255
|
+
|
256
|
+
def _get_default_config(self) -> Dict[str, Any]:
|
257
|
+
"""
|
258
|
+
Get default roles configuration.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
Default roles configuration
|
262
|
+
"""
|
263
|
+
return {
|
264
|
+
"enabled": True,
|
265
|
+
"default_policy": {
|
266
|
+
"deny_by_default": True,
|
267
|
+
"require_role_match": True,
|
268
|
+
"case_sensitive": False,
|
269
|
+
"allow_wildcard": True,
|
270
|
+
"allow_empty_roles": False,
|
271
|
+
"default_role": "user"
|
272
|
+
},
|
273
|
+
"roles": {
|
274
|
+
"admin": {
|
275
|
+
"description": "Administrator",
|
276
|
+
"permissions": ["read", "write", "delete", "admin"],
|
277
|
+
"priority": 100
|
278
|
+
},
|
279
|
+
"user": {
|
280
|
+
"description": "Regular user",
|
281
|
+
"permissions": ["read", "write"],
|
282
|
+
"priority": 10
|
283
|
+
},
|
284
|
+
"guest": {
|
285
|
+
"description": "Guest user",
|
286
|
+
"permissions": ["read"],
|
287
|
+
"priority": 1
|
288
|
+
}
|
289
|
+
},
|
290
|
+
"server_roles": ["admin", "user"],
|
291
|
+
"role_hierarchy": {
|
292
|
+
"admin": ["user", "guest"],
|
293
|
+
"user": ["guest"]
|
294
|
+
}
|
295
|
+
}
|
296
|
+
|
297
|
+
def get_client_roles(self, request: Request) -> List[str]:
|
298
|
+
"""
|
299
|
+
Get client roles from request state (backward compatibility).
|
300
|
+
|
301
|
+
Args:
|
302
|
+
request: Request object
|
303
|
+
|
304
|
+
Returns:
|
305
|
+
List of client roles
|
306
|
+
"""
|
307
|
+
return getattr(request.state, 'client_roles', [])
|
308
|
+
|
309
|
+
def is_role_validation_passed(self, request: Request) -> bool:
|
310
|
+
"""
|
311
|
+
Check if role validation passed (backward compatibility).
|
312
|
+
|
313
|
+
Args:
|
314
|
+
request: Request object
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
True if role validation passed, False otherwise
|
318
|
+
"""
|
319
|
+
return getattr(request.state, 'role_validation_passed', False)
|
320
|
+
|
321
|
+
def has_role(self, request: Request, required_role: str) -> bool:
|
322
|
+
"""
|
323
|
+
Check if client has required role (backward compatibility).
|
324
|
+
|
325
|
+
Args:
|
326
|
+
request: Request object
|
327
|
+
required_role: Required role
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
True if client has required role, False otherwise
|
331
|
+
"""
|
332
|
+
client_roles = self.get_client_roles(request)
|
333
|
+
return required_role in client_roles
|
334
|
+
|
335
|
+
def has_any_role(self, request: Request, required_roles: List[str]) -> bool:
|
336
|
+
"""
|
337
|
+
Check if client has any of the required roles (backward compatibility).
|
338
|
+
|
339
|
+
Args:
|
340
|
+
request: Request object
|
341
|
+
required_roles: List of required roles
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
True if client has any of the required roles, False otherwise
|
345
|
+
"""
|
346
|
+
client_roles = self.get_client_roles(request)
|
347
|
+
return any(role in client_roles for role in required_roles)
|
348
|
+
|
349
|
+
def get_server_roles(self) -> List[str]:
|
350
|
+
"""
|
351
|
+
Get server roles (backward compatibility).
|
352
|
+
|
353
|
+
Returns:
|
354
|
+
List of server roles
|
355
|
+
"""
|
356
|
+
return self.server_roles
|
357
|
+
|
358
|
+
def get_role_hierarchy(self) -> Dict[str, List[str]]:
|
359
|
+
"""
|
360
|
+
Get role hierarchy (backward compatibility).
|
361
|
+
|
362
|
+
Returns:
|
363
|
+
Role hierarchy dictionary
|
364
|
+
"""
|
365
|
+
return self.role_hierarchy
|