mcp-proxy-adapter 6.4.48__py3-none-any.whl → 6.6.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/api/middleware/unified_security.py +8 -12
- mcp_proxy_adapter/config.py +76 -117
- mcp_proxy_adapter/core/protocol_manager.py +25 -42
- mcp_proxy_adapter/core/security_integration.py +60 -97
- mcp_proxy_adapter/core/server_adapter.py +4 -0
- mcp_proxy_adapter/examples/config_builder.py +142 -428
- mcp_proxy_adapter/examples/config_builder_simple.py +271 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +186 -23
- mcp_proxy_adapter/examples/security_test_client.py +21 -7
- mcp_proxy_adapter/examples/test_config_builder.py +40 -0
- mcp_proxy_adapter/main.py +54 -27
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.4.48.dist-info → mcp_proxy_adapter-6.6.0.dist-info}/METADATA +1 -1
- {mcp_proxy_adapter-6.4.48.dist-info → mcp_proxy_adapter-6.6.0.dist-info}/RECORD +17 -16
- {mcp_proxy_adapter-6.4.48.dist-info → mcp_proxy_adapter-6.6.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.4.48.dist-info → mcp_proxy_adapter-6.6.0.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.4.48.dist-info → mcp_proxy_adapter-6.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,271 @@
|
|
1
|
+
"""
|
2
|
+
Simplified configuration builder for MCP Proxy Adapter.
|
3
|
+
|
4
|
+
Author: Vasiliy Zdanovskiy
|
5
|
+
email: vasilyvz@gmail.com
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import uuid
|
10
|
+
from enum import Enum
|
11
|
+
from typing import Dict, List, Optional, Any
|
12
|
+
|
13
|
+
|
14
|
+
class Protocol(Enum):
|
15
|
+
"""Supported protocols."""
|
16
|
+
HTTP = "http"
|
17
|
+
HTTPS = "https"
|
18
|
+
MTLS = "mtls"
|
19
|
+
|
20
|
+
|
21
|
+
class AuthMethod(Enum):
|
22
|
+
"""Authentication methods."""
|
23
|
+
NONE = "none"
|
24
|
+
TOKEN = "token"
|
25
|
+
TOKEN_ROLES = "token_roles"
|
26
|
+
|
27
|
+
|
28
|
+
class ConfigBuilder:
|
29
|
+
"""Simplified configuration builder."""
|
30
|
+
|
31
|
+
def __init__(self):
|
32
|
+
"""Initialize the configuration builder."""
|
33
|
+
self._reset_to_defaults()
|
34
|
+
|
35
|
+
def _reset_to_defaults(self):
|
36
|
+
"""Reset configuration to default values."""
|
37
|
+
self.config = {
|
38
|
+
"uuid": str(uuid.uuid4()),
|
39
|
+
"server": {
|
40
|
+
"host": "0.0.0.0",
|
41
|
+
"port": 8000,
|
42
|
+
"protocol": "http",
|
43
|
+
"debug": False,
|
44
|
+
"log_level": "INFO"
|
45
|
+
},
|
46
|
+
"logging": {
|
47
|
+
"level": "INFO",
|
48
|
+
"file": None,
|
49
|
+
"log_dir": "./logs",
|
50
|
+
"log_file": "mcp_proxy_adapter.log",
|
51
|
+
"max_size": 10,
|
52
|
+
"backup_count": 5,
|
53
|
+
"console_output": True,
|
54
|
+
"json_format": False
|
55
|
+
},
|
56
|
+
"security": {
|
57
|
+
"enabled": False,
|
58
|
+
"tokens": {
|
59
|
+
"admin": "admin-secret-key",
|
60
|
+
"user": "user-secret-key",
|
61
|
+
"readonly": "readonly-secret-key"
|
62
|
+
},
|
63
|
+
"roles": {
|
64
|
+
"admin": ["read", "write", "delete", "admin"],
|
65
|
+
"user": ["read", "write"],
|
66
|
+
"readonly": ["read"]
|
67
|
+
},
|
68
|
+
"roles_file": None
|
69
|
+
},
|
70
|
+
"debug": {
|
71
|
+
"enabled": False,
|
72
|
+
"log_level": "DEBUG",
|
73
|
+
"trace_requests": False,
|
74
|
+
"trace_responses": False
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
def set_protocol(self, protocol: Protocol, cert_dir: str = "./certs", key_dir: str = "./keys"):
|
79
|
+
"""Set protocol configuration (HTTP, HTTPS, or mTLS)."""
|
80
|
+
self.config["server"]["protocol"] = protocol.value
|
81
|
+
|
82
|
+
if protocol == Protocol.HTTP:
|
83
|
+
# HTTP - no SSL
|
84
|
+
pass
|
85
|
+
|
86
|
+
elif protocol == Protocol.HTTPS:
|
87
|
+
# HTTPS - server SSL only
|
88
|
+
# SSL configuration will be handled by the server based on protocol
|
89
|
+
pass
|
90
|
+
|
91
|
+
elif protocol == Protocol.MTLS:
|
92
|
+
# mTLS - server SSL + client certificates
|
93
|
+
# SSL configuration will be handled by the server based on protocol
|
94
|
+
pass
|
95
|
+
|
96
|
+
return self
|
97
|
+
|
98
|
+
def set_auth(self, auth_method: AuthMethod, api_keys: Optional[Dict[str, str]] = None, roles: Optional[Dict[str, List[str]]] = None):
|
99
|
+
"""Set authentication configuration."""
|
100
|
+
if auth_method == AuthMethod.NONE:
|
101
|
+
self.config["security"]["enabled"] = False
|
102
|
+
self.config["security"]["tokens"] = {}
|
103
|
+
self.config["security"]["roles"] = {}
|
104
|
+
self.config["security"]["roles_file"] = None
|
105
|
+
|
106
|
+
elif auth_method == AuthMethod.TOKEN:
|
107
|
+
self.config["security"]["enabled"] = True
|
108
|
+
self.config["security"]["tokens"] = api_keys or {
|
109
|
+
"admin": "admin-secret-key",
|
110
|
+
"user": "user-secret-key"
|
111
|
+
}
|
112
|
+
self.config["security"]["roles"] = {}
|
113
|
+
self.config["security"]["roles_file"] = None
|
114
|
+
|
115
|
+
elif auth_method == AuthMethod.TOKEN_ROLES:
|
116
|
+
self.config["security"]["enabled"] = True
|
117
|
+
self.config["security"]["tokens"] = api_keys or {
|
118
|
+
"admin": "admin-secret-key",
|
119
|
+
"user": "user-secret-key",
|
120
|
+
"readonly": "readonly-secret-key"
|
121
|
+
}
|
122
|
+
self.config["security"]["roles"] = roles or {
|
123
|
+
"admin": ["read", "write", "delete", "admin"],
|
124
|
+
"user": ["read", "write"],
|
125
|
+
"readonly": ["read"]
|
126
|
+
}
|
127
|
+
self.config["security"]["roles_file"] = "configs/roles.json"
|
128
|
+
|
129
|
+
return self
|
130
|
+
|
131
|
+
def set_server(self, host: str = "0.0.0.0", port: int = 8000):
|
132
|
+
"""Set server configuration."""
|
133
|
+
self.config["server"]["host"] = host
|
134
|
+
self.config["server"]["port"] = port
|
135
|
+
return self
|
136
|
+
|
137
|
+
def set_roles_file(self, roles_file: str):
|
138
|
+
"""Set roles file path."""
|
139
|
+
self.config["security"]["roles_file"] = roles_file
|
140
|
+
return self
|
141
|
+
|
142
|
+
def build(self) -> Dict[str, Any]:
|
143
|
+
"""Build and return the configuration."""
|
144
|
+
return self.config.copy()
|
145
|
+
|
146
|
+
def save(self, file_path: str) -> None:
|
147
|
+
"""Save configuration to file."""
|
148
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
149
|
+
json.dump(self.config, f, indent=2, ensure_ascii=False)
|
150
|
+
|
151
|
+
|
152
|
+
class ConfigFactory:
|
153
|
+
"""Factory for creating common configurations."""
|
154
|
+
|
155
|
+
@staticmethod
|
156
|
+
def create_http_config(port: int = 8000) -> Dict[str, Any]:
|
157
|
+
"""Create HTTP configuration."""
|
158
|
+
return (ConfigBuilder()
|
159
|
+
.set_protocol(Protocol.HTTP)
|
160
|
+
.set_server(port=port)
|
161
|
+
.build())
|
162
|
+
|
163
|
+
@staticmethod
|
164
|
+
def create_http_token_config(port: int = 8001) -> Dict[str, Any]:
|
165
|
+
"""Create HTTP with token authentication configuration."""
|
166
|
+
return (ConfigBuilder()
|
167
|
+
.set_protocol(Protocol.HTTP)
|
168
|
+
.set_auth(AuthMethod.TOKEN)
|
169
|
+
.set_server(port=port)
|
170
|
+
.build())
|
171
|
+
|
172
|
+
@staticmethod
|
173
|
+
def create_http_token_roles_config(port: int = 8002) -> Dict[str, Any]:
|
174
|
+
"""Create HTTP with token and roles configuration."""
|
175
|
+
return (ConfigBuilder()
|
176
|
+
.set_protocol(Protocol.HTTP)
|
177
|
+
.set_auth(AuthMethod.TOKEN_ROLES)
|
178
|
+
.set_server(port=port)
|
179
|
+
.build())
|
180
|
+
|
181
|
+
@staticmethod
|
182
|
+
def create_https_config(port: int = 8003) -> Dict[str, Any]:
|
183
|
+
"""Create HTTPS configuration."""
|
184
|
+
return (ConfigBuilder()
|
185
|
+
.set_protocol(Protocol.HTTPS)
|
186
|
+
.set_server(port=port)
|
187
|
+
.build())
|
188
|
+
|
189
|
+
@staticmethod
|
190
|
+
def create_https_token_config(port: int = 8004) -> Dict[str, Any]:
|
191
|
+
"""Create HTTPS with token authentication configuration."""
|
192
|
+
return (ConfigBuilder()
|
193
|
+
.set_protocol(Protocol.HTTPS)
|
194
|
+
.set_auth(AuthMethod.TOKEN)
|
195
|
+
.set_server(port=port)
|
196
|
+
.build())
|
197
|
+
|
198
|
+
@staticmethod
|
199
|
+
def create_https_token_roles_config(port: int = 8005) -> Dict[str, Any]:
|
200
|
+
"""Create HTTPS with token and roles configuration."""
|
201
|
+
return (ConfigBuilder()
|
202
|
+
.set_protocol(Protocol.HTTPS)
|
203
|
+
.set_auth(AuthMethod.TOKEN_ROLES)
|
204
|
+
.set_server(port=port)
|
205
|
+
.build())
|
206
|
+
|
207
|
+
@staticmethod
|
208
|
+
def create_mtls_config(port: int = 8006) -> Dict[str, Any]:
|
209
|
+
"""Create mTLS configuration."""
|
210
|
+
return (ConfigBuilder()
|
211
|
+
.set_protocol(Protocol.MTLS)
|
212
|
+
.set_server(port=port)
|
213
|
+
.build())
|
214
|
+
|
215
|
+
@staticmethod
|
216
|
+
def create_mtls_token_config(port: int = 8007) -> Dict[str, Any]:
|
217
|
+
"""Create mTLS with token authentication configuration."""
|
218
|
+
return (ConfigBuilder()
|
219
|
+
.set_protocol(Protocol.MTLS)
|
220
|
+
.set_auth(AuthMethod.TOKEN)
|
221
|
+
.set_server(port=port)
|
222
|
+
.build())
|
223
|
+
|
224
|
+
@staticmethod
|
225
|
+
def create_mtls_token_roles_config(port: int = 8008) -> Dict[str, Any]:
|
226
|
+
"""Create mTLS with token and roles configuration."""
|
227
|
+
return (ConfigBuilder()
|
228
|
+
.set_protocol(Protocol.MTLS)
|
229
|
+
.set_auth(AuthMethod.TOKEN_ROLES)
|
230
|
+
.set_server(port=port)
|
231
|
+
.build())
|
232
|
+
|
233
|
+
|
234
|
+
def create_config_from_flags(protocol: str, token: bool = False, roles: bool = False, port: int = 8000) -> Dict[str, Any]:
|
235
|
+
"""
|
236
|
+
Create configuration from command line flags.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
protocol: Protocol type (http, https, mtls)
|
240
|
+
token: Enable token authentication
|
241
|
+
roles: Enable role-based access control
|
242
|
+
port: Server port
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
Configuration dictionary
|
246
|
+
"""
|
247
|
+
protocol_map = {
|
248
|
+
"http": Protocol.HTTP,
|
249
|
+
"https": Protocol.HTTPS,
|
250
|
+
"mtls": Protocol.MTLS
|
251
|
+
}
|
252
|
+
|
253
|
+
if protocol not in protocol_map:
|
254
|
+
raise ValueError(f"Unsupported protocol: {protocol}")
|
255
|
+
|
256
|
+
builder = ConfigBuilder().set_protocol(protocol_map[protocol]).set_server(port=port)
|
257
|
+
|
258
|
+
if roles:
|
259
|
+
builder.set_auth(AuthMethod.TOKEN_ROLES)
|
260
|
+
elif token:
|
261
|
+
builder.set_auth(AuthMethod.TOKEN)
|
262
|
+
else:
|
263
|
+
builder.set_auth(AuthMethod.NONE)
|
264
|
+
|
265
|
+
return builder.build()
|
266
|
+
|
267
|
+
|
268
|
+
if __name__ == "__main__":
|
269
|
+
# Example usage
|
270
|
+
config = create_config_from_flags("http", token=True, port=8001)
|
271
|
+
print(json.dumps(config, indent=2))
|
@@ -27,9 +27,7 @@ class SecurityTestRunner:
|
|
27
27
|
|
28
28
|
def __init__(self):
|
29
29
|
self.project_root = Path(__file__).parent.parent.parent
|
30
|
-
self.configs_dir =
|
31
|
-
self.project_root / "mcp_proxy_adapter" / "examples" / "configs"
|
32
|
-
)
|
30
|
+
self.configs_dir = self.project_root / "configs"
|
33
31
|
self.server_processes = {}
|
34
32
|
self.test_results = []
|
35
33
|
|
@@ -137,23 +135,21 @@ class SecurityTestRunner:
|
|
137
135
|
]
|
138
136
|
try:
|
139
137
|
# Get remaining config for client setup
|
140
|
-
auth_enabled = (
|
141
|
-
|
142
|
-
|
143
|
-
auth_methods = config.get("security", {}).get("auth", {}).get("methods", [])
|
138
|
+
auth_enabled = config.get("security", {}).get("enabled", False)
|
139
|
+
# For new simplified structure, if security is enabled, we use token auth
|
140
|
+
auth_methods = ["api_key"] if auth_enabled else []
|
144
141
|
# Create test client with correct protocol
|
145
|
-
|
146
|
-
|
147
|
-
)
|
142
|
+
server_protocol = config.get("server", {}).get("protocol", "http")
|
143
|
+
protocol = "https" if server_protocol in ["https", "mtls"] else "http"
|
148
144
|
client = SecurityTestClient(base_url=f"{protocol}://localhost:{port}")
|
145
|
+
print(f"🔍 DEBUG: Created client with URL: {client.base_url}")
|
149
146
|
client.auth_enabled = auth_enabled
|
150
147
|
client.auth_methods = auth_methods
|
151
|
-
client.api_keys = (
|
152
|
-
|
153
|
-
)
|
154
|
-
client.roles_file = config.get("security", {}).get("permissions", {}).get("roles_file")
|
148
|
+
client.api_keys = config.get("security", {}).get("tokens", {})
|
149
|
+
client.roles_file = config.get("security", {}).get("roles_file")
|
150
|
+
client.roles = config.get("security", {}).get("roles", {})
|
155
151
|
# For mTLS, override SSL context creation and change working directory
|
156
|
-
if
|
152
|
+
if server_protocol == "mtls":
|
157
153
|
client.create_ssl_context = client.create_ssl_context_for_mtls
|
158
154
|
# Ensure mTLS uses certificate auth
|
159
155
|
client.auth_methods = ["certificate"]
|
@@ -214,10 +210,10 @@ class SecurityTestRunner:
|
|
214
210
|
result = await client.test_negative_authentication()
|
215
211
|
results.append(result)
|
216
212
|
except Exception as e:
|
217
|
-
|
213
|
+
results.append(
|
218
214
|
TestResult(
|
219
215
|
test_name=f"{config_name}_client_error",
|
220
|
-
server_url=f"
|
216
|
+
server_url=f"{protocol}://localhost:{port}",
|
221
217
|
auth_type="none",
|
222
218
|
success=False,
|
223
219
|
error_message=str(e),
|
@@ -228,17 +224,68 @@ class SecurityTestRunner:
|
|
228
224
|
self.stop_server(config_name, process)
|
229
225
|
return results
|
230
226
|
|
227
|
+
def create_variant_from_full_config(self, full_config_path: Path, protocol: str, auth: str, port: int) -> Path:
|
228
|
+
"""
|
229
|
+
Create a variant configuration from full config.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
full_config_path: Path to the full configuration file
|
233
|
+
protocol: Protocol type (http, https, mtls)
|
234
|
+
auth: Authentication type (none, token, token_roles)
|
235
|
+
port: Server port
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
Path to the temporary configuration file
|
239
|
+
"""
|
240
|
+
import tempfile
|
241
|
+
import json
|
242
|
+
|
243
|
+
# Load the full configuration
|
244
|
+
with open(full_config_path, 'r') as f:
|
245
|
+
full_config = json.load(f)
|
246
|
+
|
247
|
+
# Create a copy of the full config
|
248
|
+
variant_config = full_config.copy()
|
249
|
+
|
250
|
+
# Set server port and protocol
|
251
|
+
variant_config["server"]["port"] = port
|
252
|
+
variant_config["server"]["protocol"] = protocol
|
253
|
+
|
254
|
+
# Apply protocol configuration
|
255
|
+
if protocol in variant_config.get("protocol_variants", {}):
|
256
|
+
protocol_config = variant_config["protocol_variants"][protocol]
|
257
|
+
variant_config["server"].update(protocol_config["server"])
|
258
|
+
|
259
|
+
# Apply authentication configuration
|
260
|
+
if auth in variant_config.get("auth_variants", {}):
|
261
|
+
auth_config = variant_config["auth_variants"][auth]
|
262
|
+
variant_config["security"].update(auth_config["security"])
|
263
|
+
|
264
|
+
# Remove the helper sections
|
265
|
+
variant_config.pop("protocol_variants", None)
|
266
|
+
variant_config.pop("auth_variants", None)
|
267
|
+
|
268
|
+
# Create temporary config file
|
269
|
+
temp_dir = tempfile.mkdtemp(prefix="full_config_test_")
|
270
|
+
config_name = f"{protocol}_{auth}.json"
|
271
|
+
config_path = Path(temp_dir) / config_name
|
272
|
+
|
273
|
+
with open(config_path, 'w') as f:
|
274
|
+
json.dump(variant_config, f, indent=2, ensure_ascii=False)
|
275
|
+
|
276
|
+
return config_path
|
277
|
+
|
231
278
|
async def run_all_tests(self):
|
232
279
|
"""Run all security tests."""
|
233
280
|
print("🔒 Starting Security Testing Suite")
|
234
281
|
print("=" * 50)
|
235
282
|
# Test configurations
|
236
283
|
configs = [
|
237
|
-
("basic_http", "
|
238
|
-
("http_token", "
|
239
|
-
("https", "
|
240
|
-
("https_token", "
|
241
|
-
("mtls", "
|
284
|
+
("basic_http", "http.json"),
|
285
|
+
("http_token", "http_token_roles.json"),
|
286
|
+
("https", "https.json"),
|
287
|
+
("https_token", "https_token_roles.json"),
|
288
|
+
("mtls", "mtls.json"),
|
242
289
|
]
|
243
290
|
total_tests = 0
|
244
291
|
passed_tests = 0
|
@@ -280,12 +327,128 @@ class SecurityTestRunner:
|
|
280
327
|
print(f" Error: {result.error_message}")
|
281
328
|
return passed_tests == total_tests
|
282
329
|
|
330
|
+
async def run_full_config_tests(self, full_config_path: str):
|
331
|
+
"""Run tests using full configuration with all variants."""
|
332
|
+
print("🚀 Full Configuration Variants Testing")
|
333
|
+
print("=" * 60)
|
334
|
+
print(f"📁 Using full config: {full_config_path}")
|
335
|
+
|
336
|
+
full_config_file = Path(full_config_path)
|
337
|
+
if not full_config_file.exists():
|
338
|
+
print(f"❌ Full configuration file not found: {full_config_path}")
|
339
|
+
return False
|
340
|
+
|
341
|
+
# Define all combinations to test
|
342
|
+
variants = [
|
343
|
+
# HTTP variants
|
344
|
+
("http", "none", 20000),
|
345
|
+
("http", "token", 20001),
|
346
|
+
("http", "token_roles", 20002),
|
347
|
+
|
348
|
+
# HTTPS variants
|
349
|
+
("https", "none", 20003),
|
350
|
+
("https", "token", 20004),
|
351
|
+
("https", "token_roles", 20005),
|
352
|
+
|
353
|
+
# mTLS variants
|
354
|
+
("mtls", "none", 20006),
|
355
|
+
("mtls", "token", 20007),
|
356
|
+
("mtls", "token_roles", 20008),
|
357
|
+
]
|
358
|
+
|
359
|
+
total_tests = 0
|
360
|
+
passed_tests = 0
|
361
|
+
all_results = []
|
362
|
+
|
363
|
+
for protocol, auth, port in variants:
|
364
|
+
print(f"\n{'='*60}")
|
365
|
+
print(f"🧪 Testing {protocol.upper()} with {auth.upper()} authentication")
|
366
|
+
print(f"{'='*60}")
|
367
|
+
|
368
|
+
# Create variant configuration
|
369
|
+
config_path = self.create_variant_from_full_config(full_config_file, protocol, auth, port)
|
370
|
+
|
371
|
+
# Test the variant
|
372
|
+
config_name = f"{protocol}_{auth}"
|
373
|
+
results = await self.test_server(config_name, config_path)
|
374
|
+
|
375
|
+
# Count results
|
376
|
+
for result in results:
|
377
|
+
total_tests += 1
|
378
|
+
if result.success:
|
379
|
+
passed_tests += 1
|
380
|
+
print(f"✅ {result.test_name}: PASS")
|
381
|
+
else:
|
382
|
+
print(f"❌ {result.test_name}: FAIL - {result.error_message}")
|
383
|
+
|
384
|
+
all_results.extend(results)
|
385
|
+
|
386
|
+
# Clean up temporary config
|
387
|
+
import shutil
|
388
|
+
shutil.rmtree(config_path.parent)
|
389
|
+
|
390
|
+
# Print final summary
|
391
|
+
print(f"\n{'='*60}")
|
392
|
+
print("📊 FULL CONFIG TEST SUMMARY")
|
393
|
+
print(f"{'='*60}")
|
394
|
+
print(f"Total tests: {total_tests}")
|
395
|
+
print(f"Passed: {passed_tests}")
|
396
|
+
print(f"Failed: {total_tests - passed_tests}")
|
397
|
+
print(f"Success rate: {(passed_tests/total_tests)*100:.1f}%")
|
398
|
+
|
399
|
+
if total_tests - passed_tests > 0:
|
400
|
+
print(f"\n❌ Failed tests:")
|
401
|
+
for result in all_results:
|
402
|
+
if not result.success:
|
403
|
+
print(f" • {result.test_name}: {result.error_message}")
|
404
|
+
|
405
|
+
return passed_tests == total_tests
|
406
|
+
|
407
|
+
def print_summary(self):
|
408
|
+
"""Print test summary."""
|
409
|
+
if not self.test_results:
|
410
|
+
print("📊 No test results to display")
|
411
|
+
return
|
412
|
+
|
413
|
+
total_tests = len(self.test_results)
|
414
|
+
passed_tests = sum(1 for result in self.test_results if result.success)
|
415
|
+
|
416
|
+
print("\n" + "=" * 50)
|
417
|
+
print("📊 TEST SUMMARY")
|
418
|
+
print("=" * 50)
|
419
|
+
print(f"Total tests: {total_tests}")
|
420
|
+
print(f"Passed: {passed_tests}")
|
421
|
+
print(f"Failed: {total_tests - passed_tests}")
|
422
|
+
print(f"Success rate: {(passed_tests/total_tests)*100:.1f}%")
|
423
|
+
|
424
|
+
if total_tests - passed_tests > 0:
|
425
|
+
print(f"\n📋 DETAILED RESULTS")
|
426
|
+
print("-" * 30)
|
427
|
+
for result in self.test_results:
|
428
|
+
status = "✅ PASS" if result.success else "❌ FAIL"
|
429
|
+
print(f"{status} {result.test_name}")
|
430
|
+
if not result.success and result.error_message:
|
431
|
+
print(f" Error: {result.error_message}")
|
432
|
+
|
283
433
|
|
284
434
|
async def main():
|
285
435
|
"""Main function."""
|
436
|
+
import argparse
|
437
|
+
|
438
|
+
parser = argparse.ArgumentParser(description="Security Testing Suite for MCP Proxy Adapter")
|
439
|
+
parser.add_argument("--full-config", help="Path to full configuration file for variant testing")
|
440
|
+
parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
|
441
|
+
|
442
|
+
args = parser.parse_args()
|
443
|
+
|
286
444
|
runner = SecurityTestRunner()
|
287
445
|
try:
|
288
|
-
|
446
|
+
if args.full_config:
|
447
|
+
# Test full configuration variants
|
448
|
+
success = await runner.run_full_config_tests(args.full_config)
|
449
|
+
else:
|
450
|
+
# Run standard tests
|
451
|
+
success = await runner.run_all_tests()
|
289
452
|
sys.exit(0 if success else 1)
|
290
453
|
except KeyboardInterrupt:
|
291
454
|
print("\n⚠️ Testing interrupted by user")
|
@@ -83,6 +83,7 @@ class SecurityTestClient:
|
|
83
83
|
self.auth_methods = []
|
84
84
|
self.api_keys = {}
|
85
85
|
self.roles_file = None
|
86
|
+
self.roles = {}
|
86
87
|
|
87
88
|
if self._security_available:
|
88
89
|
# For testing purposes, we don't initialize SecurityManager
|
@@ -99,7 +100,7 @@ class SecurityTestClient:
|
|
99
100
|
self.test_tokens = {
|
100
101
|
"admin": "admin-secret-key",
|
101
102
|
"user": "user-secret-key",
|
102
|
-
"readonly": "readonly-
|
103
|
+
"readonly": "readonly-secret-key",
|
103
104
|
"guest": "guest-token-123",
|
104
105
|
"proxy": "proxy-token-123",
|
105
106
|
"invalid": "invalid-token-999",
|
@@ -123,9 +124,11 @@ class SecurityTestClient:
|
|
123
124
|
def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
|
124
125
|
"""Create SSL context for mTLS connections."""
|
125
126
|
# For mTLS, we need client certificates - check if they exist
|
126
|
-
|
127
|
-
|
128
|
-
|
127
|
+
# Paths are relative to project root (../../ from examples directory)
|
128
|
+
# Use admin certificates to match server configuration
|
129
|
+
cert_file = "../../certs/admin.crt"
|
130
|
+
key_file = "../../certs/client_admin.key"
|
131
|
+
ca_cert_file = "../../certs/localhost_server.crt"
|
129
132
|
|
130
133
|
# CRITICAL: For mTLS, certificates are REQUIRED
|
131
134
|
if not os.path.exists(cert_file):
|
@@ -152,7 +155,13 @@ class SecurityTestClient:
|
|
152
155
|
timeout = ClientTimeout(total=30)
|
153
156
|
# Create SSL context only for HTTPS URLs
|
154
157
|
if self.base_url.startswith('https://'):
|
155
|
-
|
158
|
+
# Check if this is mTLS (ports 20006, 20007, 20008 are mTLS test ports)
|
159
|
+
if any(port in self.base_url for port in ['20006', '20007', '20008']):
|
160
|
+
# Use mTLS context with client certificates
|
161
|
+
ssl_context = self.create_ssl_context_for_mtls()
|
162
|
+
else:
|
163
|
+
# Use regular HTTPS context
|
164
|
+
ssl_context = self.create_ssl_context()
|
156
165
|
connector = TCPConnector(ssl=ssl_context)
|
157
166
|
else:
|
158
167
|
# For HTTP URLs, use default connector without SSL
|
@@ -209,6 +218,11 @@ class SecurityTestClient:
|
|
209
218
|
# Provide both common header styles to maximize compatibility
|
210
219
|
headers["X-API-Key"] = token
|
211
220
|
headers["Authorization"] = f"Bearer {token}"
|
221
|
+
|
222
|
+
# Add role header if provided
|
223
|
+
role = kwargs.get("role")
|
224
|
+
if role:
|
225
|
+
headers["X-Role"] = role
|
212
226
|
elif auth_type == "basic":
|
213
227
|
username = kwargs.get("username")
|
214
228
|
password = kwargs.get("password")
|
@@ -983,7 +997,7 @@ class SecurityTestClient:
|
|
983
997
|
|
984
998
|
async def test_role_based_access(self, server_url: str, auth_type: str, role: str = "admin") -> TestResult:
|
985
999
|
"""Test role-based access control."""
|
986
|
-
if not self.roles_file:
|
1000
|
+
if not self.roles_file and not self.roles:
|
987
1001
|
return TestResult(
|
988
1002
|
test_name="Role-Based Access Test",
|
989
1003
|
server_url=server_url,
|
@@ -1001,7 +1015,7 @@ class SecurityTestClient:
|
|
1001
1015
|
|
1002
1016
|
async def test_role_permissions(self, server_url: str, auth_type: str, role: str = "admin", action: str = "read") -> TestResult:
|
1003
1017
|
"""Test role permissions."""
|
1004
|
-
if not self.roles_file:
|
1018
|
+
if not self.roles_file and not self.roles:
|
1005
1019
|
return TestResult(
|
1006
1020
|
test_name="Role Permissions Test",
|
1007
1021
|
server_url=server_url,
|