mcp-proxy-adapter 6.9.28__py3-none-any.whl → 6.9.29__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.

Potentially problematic release.


This version of mcp-proxy-adapter might be problematic. Click here for more details.

Files changed (212) hide show
  1. mcp_proxy_adapter/__init__.py +10 -0
  2. mcp_proxy_adapter/__main__.py +8 -21
  3. mcp_proxy_adapter/api/app.py +10 -913
  4. mcp_proxy_adapter/api/core/__init__.py +18 -0
  5. mcp_proxy_adapter/api/core/app_factory.py +243 -0
  6. mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
  7. mcp_proxy_adapter/api/core/registration_manager.py +166 -0
  8. mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
  9. mcp_proxy_adapter/api/handlers.py +78 -199
  10. mcp_proxy_adapter/api/middleware/__init__.py +1 -44
  11. mcp_proxy_adapter/api/middleware/base.py +0 -42
  12. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
  13. mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
  14. mcp_proxy_adapter/api/middleware/factory.py +0 -94
  15. mcp_proxy_adapter/api/middleware/logging.py +0 -112
  16. mcp_proxy_adapter/api/middleware/performance.py +0 -35
  17. mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
  18. mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
  19. mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
  20. mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
  21. mcp_proxy_adapter/api/openapi/__init__.py +21 -0
  22. mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
  23. mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
  24. mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
  25. mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
  26. mcp_proxy_adapter/api/schemas.py +0 -61
  27. mcp_proxy_adapter/api/tool_integration.py +0 -117
  28. mcp_proxy_adapter/api/tools.py +0 -46
  29. mcp_proxy_adapter/cli/__init__.py +12 -0
  30. mcp_proxy_adapter/cli/commands/__init__.py +15 -0
  31. mcp_proxy_adapter/cli/commands/client.py +100 -0
  32. mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
  33. mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
  34. mcp_proxy_adapter/cli/commands/generate.py +259 -0
  35. mcp_proxy_adapter/cli/commands/server.py +174 -0
  36. mcp_proxy_adapter/cli/commands/sets.py +128 -0
  37. mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
  38. mcp_proxy_adapter/cli/examples/__init__.py +8 -0
  39. mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
  40. mcp_proxy_adapter/cli/examples/https_token.py +96 -0
  41. mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
  42. mcp_proxy_adapter/cli/main.py +63 -0
  43. mcp_proxy_adapter/cli/parser.py +324 -0
  44. mcp_proxy_adapter/cli/validators.py +231 -0
  45. mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
  46. mcp_proxy_adapter/client/proxy.py +45 -0
  47. mcp_proxy_adapter/commands/__init__.py +44 -28
  48. mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
  49. mcp_proxy_adapter/commands/base.py +19 -43
  50. mcp_proxy_adapter/commands/builtin_commands.py +0 -75
  51. mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
  52. mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
  53. mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
  54. mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
  55. mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
  56. mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
  57. mcp_proxy_adapter/commands/catalog_manager.py +58 -928
  58. mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
  59. mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
  60. mcp_proxy_adapter/commands/command_registry.py +172 -904
  61. mcp_proxy_adapter/commands/config_command.py +0 -28
  62. mcp_proxy_adapter/commands/dependency_container.py +1 -70
  63. mcp_proxy_adapter/commands/dependency_manager.py +0 -128
  64. mcp_proxy_adapter/commands/echo_command.py +0 -34
  65. mcp_proxy_adapter/commands/health_command.py +0 -3
  66. mcp_proxy_adapter/commands/help_command.py +0 -159
  67. mcp_proxy_adapter/commands/hooks.py +0 -137
  68. mcp_proxy_adapter/commands/key_management_command.py +0 -25
  69. mcp_proxy_adapter/commands/load_command.py +7 -78
  70. mcp_proxy_adapter/commands/plugins_command.py +0 -16
  71. mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
  72. mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
  73. mcp_proxy_adapter/commands/queue_commands.py +750 -0
  74. mcp_proxy_adapter/commands/registration_status_command.py +0 -43
  75. mcp_proxy_adapter/commands/registry/__init__.py +18 -0
  76. mcp_proxy_adapter/commands/registry/command_info.py +103 -0
  77. mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
  78. mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
  79. mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
  80. mcp_proxy_adapter/commands/reload_command.py +0 -80
  81. mcp_proxy_adapter/commands/result.py +25 -77
  82. mcp_proxy_adapter/commands/role_test_command.py +0 -44
  83. mcp_proxy_adapter/commands/roles_management_command.py +0 -199
  84. mcp_proxy_adapter/commands/security_command.py +0 -30
  85. mcp_proxy_adapter/commands/settings_command.py +0 -68
  86. mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
  87. mcp_proxy_adapter/commands/token_management_command.py +0 -1
  88. mcp_proxy_adapter/commands/transport_management_command.py +0 -20
  89. mcp_proxy_adapter/commands/unload_command.py +0 -71
  90. mcp_proxy_adapter/config.py +15 -626
  91. mcp_proxy_adapter/core/__init__.py +5 -39
  92. mcp_proxy_adapter/core/app_factory.py +14 -36
  93. mcp_proxy_adapter/core/app_runner.py +0 -27
  94. mcp_proxy_adapter/core/auth_validator.py +1 -93
  95. mcp_proxy_adapter/core/certificate/__init__.py +20 -0
  96. mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
  97. mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
  98. mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
  99. mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
  100. mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
  101. mcp_proxy_adapter/core/certificate_utils.py +64 -903
  102. mcp_proxy_adapter/core/client.py +0 -6
  103. mcp_proxy_adapter/core/client_manager.py +0 -19
  104. mcp_proxy_adapter/core/client_security.py +0 -2
  105. mcp_proxy_adapter/core/config/__init__.py +18 -0
  106. mcp_proxy_adapter/core/config/config.py +195 -0
  107. mcp_proxy_adapter/core/config/config_factory.py +22 -0
  108. mcp_proxy_adapter/core/config/config_loader.py +66 -0
  109. mcp_proxy_adapter/core/config/feature_manager.py +31 -0
  110. mcp_proxy_adapter/core/config/simple_config.py +112 -0
  111. mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
  112. mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
  113. mcp_proxy_adapter/core/config_converter.py +0 -186
  114. mcp_proxy_adapter/core/config_validator.py +96 -1238
  115. mcp_proxy_adapter/core/errors.py +7 -42
  116. mcp_proxy_adapter/core/job_manager.py +54 -0
  117. mcp_proxy_adapter/core/logging.py +2 -22
  118. mcp_proxy_adapter/core/mtls_asgi.py +0 -20
  119. mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
  120. mcp_proxy_adapter/core/mtls_proxy.py +0 -80
  121. mcp_proxy_adapter/core/mtls_server.py +3 -173
  122. mcp_proxy_adapter/core/protocol_manager.py +1 -191
  123. mcp_proxy_adapter/core/proxy/__init__.py +22 -0
  124. mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
  125. mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
  126. mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
  127. mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
  128. mcp_proxy_adapter/core/proxy_client.py +0 -1
  129. mcp_proxy_adapter/core/proxy_registration.py +36 -913
  130. mcp_proxy_adapter/core/role_utils.py +0 -308
  131. mcp_proxy_adapter/core/security_adapter.py +1 -36
  132. mcp_proxy_adapter/core/security_factory.py +1 -150
  133. mcp_proxy_adapter/core/security_integration.py +0 -33
  134. mcp_proxy_adapter/core/server_adapter.py +1 -40
  135. mcp_proxy_adapter/core/server_engine.py +2 -173
  136. mcp_proxy_adapter/core/settings.py +0 -127
  137. mcp_proxy_adapter/core/signal_handler.py +0 -65
  138. mcp_proxy_adapter/core/ssl_utils.py +19 -137
  139. mcp_proxy_adapter/core/transport_manager.py +0 -151
  140. mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
  141. mcp_proxy_adapter/core/utils.py +1 -182
  142. mcp_proxy_adapter/core/validation/__init__.py +21 -0
  143. mcp_proxy_adapter/core/validation/config_validator.py +211 -0
  144. mcp_proxy_adapter/core/validation/file_validator.py +73 -0
  145. mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
  146. mcp_proxy_adapter/core/validation/security_validator.py +58 -0
  147. mcp_proxy_adapter/core/validation/validation_result.py +27 -0
  148. mcp_proxy_adapter/custom_openapi.py +33 -652
  149. mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
  150. mcp_proxy_adapter/examples/check_config.py +0 -2
  151. mcp_proxy_adapter/examples/client_usage_example.py +164 -0
  152. mcp_proxy_adapter/examples/config_builder.py +13 -2
  153. mcp_proxy_adapter/examples/config_cli.py +0 -1
  154. mcp_proxy_adapter/examples/create_test_configs.py +0 -46
  155. mcp_proxy_adapter/examples/debug_request_state.py +0 -1
  156. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
  157. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
  158. mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
  159. mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
  160. mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
  161. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
  162. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
  163. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
  164. mcp_proxy_adapter/examples/full_application/main.py +186 -150
  165. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
  166. mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
  167. mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
  168. mcp_proxy_adapter/examples/generate_config.py +65 -11
  169. mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
  170. mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
  171. mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
  172. mcp_proxy_adapter/examples/queue_server_example.py +85 -0
  173. mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
  174. mcp_proxy_adapter/examples/required_certificates.py +0 -2
  175. mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
  176. mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
  177. mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
  178. mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
  179. mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
  180. mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
  181. mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
  182. mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
  183. mcp_proxy_adapter/examples/security_test_client.py +24 -1075
  184. mcp_proxy_adapter/examples/setup/__init__.py +24 -0
  185. mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
  186. mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
  187. mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
  188. mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
  189. mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
  190. mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
  191. mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
  192. mcp_proxy_adapter/examples/test_config.py +0 -3
  193. mcp_proxy_adapter/examples/test_config_builder.py +25 -405
  194. mcp_proxy_adapter/examples/test_examples.py +0 -1
  195. mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
  196. mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
  197. mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
  198. mcp_proxy_adapter/examples/universal_client.py +0 -6
  199. mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
  200. mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
  201. mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
  202. mcp_proxy_adapter/integrations/__init__.py +25 -0
  203. mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
  204. mcp_proxy_adapter/main.py +70 -62
  205. mcp_proxy_adapter/openapi.py +0 -22
  206. mcp_proxy_adapter/version.py +1 -1
  207. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/METADATA +2 -1
  208. mcp_proxy_adapter-6.9.29.dist-info/RECORD +235 -0
  209. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/entry_points.txt +1 -1
  210. mcp_proxy_adapter-6.9.28.dist-info/RECORD +0 -149
  211. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/WHEEL +0 -0
  212. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/top_level.txt +0 -0
@@ -7,32 +7,23 @@ This client tests various security configurations including:
7
7
  - HTTPS
8
8
  - HTTPS + Token authentication
9
9
  - mTLS with certificate authentication
10
+
10
11
  Author: Vasiliy Zdanovskiy
11
12
  email: vasilyvz@gmail.com
12
13
  """
13
14
  import asyncio
14
- import os
15
- import ssl
16
15
  import sys
17
- import time
18
16
  from pathlib import Path
19
- from typing import Dict, List, Optional
20
- from dataclasses import dataclass
21
- from aiohttp import ClientSession, ClientTimeout, TCPConnector
22
17
 
23
18
  # Add project root to path for imports
24
19
  current_dir = Path(__file__).parent
25
20
  parent_dir = current_dir.parent
26
- # Only add paths that are likely to exist and be useful
27
21
  if parent_dir.exists():
28
22
  sys.path.insert(0, str(parent_dir))
29
23
  sys.path.insert(0, str(current_dir))
30
24
 
31
25
  # Import mcp_security_framework components
32
26
  try:
33
- from mcp_security_framework import SecurityManager, SecurityConfig, AuthConfig, PermissionConfig, SSLConfig
34
- from mcp_security_framework.schemas.config import SSLConfig as SSLConfigSchema
35
-
36
27
  _MCP_SECURITY_AVAILABLE = True
37
28
  print("āœ… mcp_security_framework available")
38
29
  except ImportError as e:
@@ -49,1074 +40,32 @@ except ImportError:
49
40
  _CRYPTOGRAPHY_AVAILABLE = False
50
41
  print("āš ļø cryptography not available, SSL validation will be limited")
51
42
 
52
-
53
- @dataclass
54
- class TestResult:
55
- """Test result data class."""
56
-
57
- test_name: str
58
- server_url: str
59
- auth_type: str
60
- success: bool
61
- status_code: Optional[int] = None
62
- response_data: Optional[Dict] = None
63
- error_message: Optional[str] = None
64
- duration: float = 0.0
65
-
66
-
67
- class SecurityTestClient:
68
- """Security test client for comprehensive testing."""
69
-
70
- def __init__(self, base_url: str = "http://localhost:8000"):
71
- """Initialize security test client."""
72
- self.base_url = base_url
73
- self.session: Optional[ClientSession] = None
74
-
75
- # Initialize security managers if available
76
- self.ssl_manager = None
77
- self.cert_manager = None
78
- self._security_available = _MCP_SECURITY_AVAILABLE
79
- self._crypto_available = _CRYPTOGRAPHY_AVAILABLE
80
-
81
- # Authentication configuration
82
- self.auth_enabled = False
83
- self.auth_methods = []
84
- self.api_keys = {}
85
- self.roles_file = None
86
- self.roles = {}
87
-
88
- if self._security_available:
89
- # For testing purposes, we don't initialize SecurityManager
90
- # because we're testing server configurations, not the framework itself
91
- # SecurityManager will be used only when actually needed by the server
92
- self.ssl_manager = None
93
- print("āœ… mcp_security_framework available for testing")
94
-
95
- if not self._security_available:
96
- print("ā„¹ļø Using standard SSL library for testing")
97
- self.ssl_manager = None
98
- self.test_results: List[TestResult] = []
99
- # Test tokens
100
- self.test_tokens = {
101
- "admin": "admin-secret-key",
102
- "user": "user-secret-key",
103
- "readonly": "readonly-secret-key",
104
- "guest": "guest-token-123",
105
- "proxy": "proxy-token-123",
106
- "invalid": "invalid-token-999",
107
- }
108
- # Test certificates - use relative paths
109
- self.test_certificates = {
110
- "admin": {
111
- "cert": "certs/admin_cert.pem",
112
- "key": "certs/admin_key.pem",
113
- },
114
- "user": {
115
- "cert": "certs/user_cert.pem",
116
- "key": "keys/user_key.pem",
117
- },
118
- "readonly": {
119
- "cert": "certs/readonly_cert.pem",
120
- "key": "certs/readonly_key.pem",
121
- },
122
- }
123
-
124
- def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
125
- """Create SSL context for mTLS connections."""
126
- # For mTLS, we need client certificates - check if they exist
127
- # Use absolute paths to avoid issues with working directory
128
- # Use newly generated admin_client_client.crt which is signed by the same CA as server
129
- project_root = Path(__file__).parent.parent.parent
130
- cert_file = str(project_root / "certs" / "admin_client_client.crt")
131
- key_file = str(project_root / "keys" / "admin-client_client.key")
132
- ca_cert_file = str(project_root / "certs" / "localhost_server.crt")
133
-
134
- # CRITICAL: For mTLS, certificates are REQUIRED
135
- if not os.path.exists(cert_file):
136
- raise FileNotFoundError(f"CRITICAL ERROR: mTLS client certificate not found: {cert_file}")
137
- if not os.path.exists(key_file):
138
- raise FileNotFoundError(f"CRITICAL ERROR: mTLS client key not found: {key_file}")
139
-
140
- # For testing, we use standard SSL library
141
- # SecurityManager is not initialized for testing purposes
142
-
143
- # Use standard SSL library for testing
144
- ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
145
- # For mTLS testing - client needs to present certificate to server
146
- ssl_context.check_hostname = False
147
- ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
148
- # Load client certificate and key for mTLS (we already checked they exist)
149
- ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
150
- if os.path.exists(ca_cert_file):
151
- ssl_context.load_verify_locations(cafile=ca_cert_file)
152
- return ssl_context
153
-
154
- async def __aenter__(self):
155
- """Async context manager entry."""
156
- timeout = ClientTimeout(total=30)
157
- # Create SSL context only for HTTPS URLs
158
- if self.base_url.startswith('https://'):
159
- # Check if this is mTLS (ports 20006, 20007, 20008 are mTLS test ports)
160
- if any(port in self.base_url for port in ['20006', '20007', '20008']):
161
- # Use mTLS context with client certificates
162
- ssl_context = self.create_ssl_context_for_mtls()
163
- else:
164
- # Use regular HTTPS context
165
- ssl_context = self.create_ssl_context()
166
- connector = TCPConnector(ssl=ssl_context)
167
- else:
168
- # For HTTP URLs, use default connector without SSL
169
- connector = TCPConnector()
170
-
171
- # Create session
172
- self.session = ClientSession(timeout=timeout, connector=connector)
173
- return self
174
-
175
- async def __aexit__(self, exc_type, exc_val, exc_tb):
176
- """Async context manager exit."""
177
- if self.session:
178
- await self.session.close()
179
-
180
- def create_ssl_context(
181
- self,
182
- cert_file: Optional[str] = None,
183
- key_file: Optional[str] = None,
184
- ca_cert_file: Optional[str] = None,
185
- ) -> ssl.SSLContext:
186
- """Create SSL context for client."""
187
- # If certificates are provided, they must exist
188
- if cert_file and not os.path.exists(cert_file):
189
- raise FileNotFoundError(f"CRITICAL ERROR: SSL certificate not found: {cert_file}")
190
- if key_file and not os.path.exists(key_file):
191
- raise FileNotFoundError(f"CRITICAL ERROR: SSL key not found: {key_file}")
192
- if ca_cert_file and not os.path.exists(ca_cert_file):
193
- raise FileNotFoundError(f"CRITICAL ERROR: SSL CA certificate not found: {ca_cert_file}")
194
-
195
- # For testing, we use standard SSL library
196
- # SecurityManager is not initialized for testing purposes
197
-
198
- # Use standard SSL library for testing
199
- ssl_context = ssl.create_default_context()
200
- # For testing with self-signed certificates
201
- ssl_context.check_hostname = False
202
- ssl_context.verify_mode = ssl.CERT_NONE
203
- if cert_file and key_file:
204
- ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
205
- if ca_cert_file:
206
- ssl_context.load_verify_locations(cafile=ca_cert_file)
207
- # For testing, still don't verify
208
- ssl_context.verify_mode = ssl.CERT_NONE
209
- return ssl_context
210
-
211
- def create_auth_headers(self, auth_type: str, **kwargs) -> Dict[str, str]:
212
- """Create authentication headers."""
213
- headers = {"Content-Type": "application/json"}
214
- if auth_type == "api_key":
215
- token = kwargs.get("token")
216
- if not token:
217
- raise ValueError("token is required for api_key authentication")
218
- print(f"šŸ” DEBUG: Using token: {token}")
219
- # Provide both common header styles to maximize compatibility
220
- headers["X-API-Key"] = token
221
- headers["Authorization"] = f"Bearer {token}"
222
-
223
- # Add role header if provided
224
- role = kwargs.get("role")
225
- if role:
226
- headers["X-Role"] = role
227
- elif auth_type == "basic":
228
- username = kwargs.get("username")
229
- password = kwargs.get("password")
230
- if not username or not password:
231
- raise ValueError("username and password are required for basic authentication")
232
- import base64
233
-
234
- credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
235
- headers["Authorization"] = f"Basic {credentials}"
236
- elif auth_type == "certificate":
237
- # For mTLS, we need to use client certificates
238
- # This is handled by SSL context, not headers
239
- pass
240
- return headers
241
-
242
- async def test_health_check(
243
- self, server_url: str, auth_type: str = "none", **kwargs
244
- ) -> TestResult:
245
- """Test health check endpoint."""
246
- start_time = time.time()
247
- test_name = f"Health Check ({auth_type})"
248
- try:
249
- headers = self.create_auth_headers(auth_type, **kwargs)
250
- async with self.session.get(
251
- f"{server_url}/health", headers=headers
252
- ) as response:
253
- duration = time.time() - start_time
254
- if response.status == 200:
255
- data = await response.json()
256
- return TestResult(
257
- test_name=test_name,
258
- server_url=server_url,
259
- auth_type=auth_type,
260
- success=True,
261
- status_code=response.status,
262
- response_data=data,
263
- duration=duration,
264
- )
265
- else:
266
- error_text = await response.text()
267
- return TestResult(
268
- test_name=test_name,
269
- server_url=server_url,
270
- auth_type=auth_type,
271
- success=False,
272
- status_code=response.status,
273
- error_message=f"Health check failed: {error_text}",
274
- duration=duration,
275
- )
276
- except Exception as e:
277
- duration = time.time() - start_time
278
- return TestResult(
279
- test_name=test_name,
280
- server_url=server_url,
281
- auth_type=auth_type,
282
- success=False,
283
- error_message=f"Health check error: {str(e)}",
284
- duration=duration,
285
- )
286
-
287
- async def test_echo_command(
288
- self, server_url: str, auth_type: str = "none", **kwargs
289
- ) -> TestResult:
290
- """Test echo command."""
291
- start_time = time.time()
292
- test_name = f"Echo Command ({auth_type})"
293
- try:
294
- headers = self.create_auth_headers(auth_type, **kwargs)
295
- data = {
296
- "jsonrpc": "2.0",
297
- "method": "echo",
298
- "params": {"message": "Hello from security test client!"},
299
- "id": 1,
300
- }
301
- async with self.session.post(
302
- f"{server_url}/cmd", headers=headers, json=data
303
- ) as response:
304
- duration = time.time() - start_time
305
- if response.status == 200:
306
- data = await response.json()
307
- return TestResult(
308
- test_name=test_name,
309
- server_url=server_url,
310
- auth_type=auth_type,
311
- success=True,
312
- status_code=response.status,
313
- response_data=data,
314
- duration=duration,
315
- )
316
- else:
317
- error_text = await response.text()
318
- return TestResult(
319
- test_name=test_name,
320
- server_url=server_url,
321
- auth_type=auth_type,
322
- success=False,
323
- status_code=response.status,
324
- error_message=f"Echo command failed: {error_text}",
325
- duration=duration,
326
- )
327
- except Exception as e:
328
- duration = time.time() - start_time
329
- return TestResult(
330
- test_name=test_name,
331
- server_url=server_url,
332
- auth_type=auth_type,
333
- success=False,
334
- error_message=f"Echo command error: {str(e)}",
335
- duration=duration,
336
- )
337
-
338
- async def test_security_command(
339
- self, server_url: str, auth_type: str = "none", **kwargs
340
- ) -> TestResult:
341
- """Test security command."""
342
- start_time = time.time()
343
- test_name = f"Security Command ({auth_type})"
344
- try:
345
- headers = self.create_auth_headers(auth_type, **kwargs)
346
- data = {"jsonrpc": "2.0", "method": "health", "params": {}, "id": 2}
347
- async with self.session.post(
348
- f"{server_url}/cmd", headers=headers, json=data
349
- ) as response:
350
- duration = time.time() - start_time
351
- if response.status == 200:
352
- data = await response.json()
353
- return TestResult(
354
- test_name=test_name,
355
- server_url=server_url,
356
- auth_type=auth_type,
357
- success=True,
358
- status_code=response.status,
359
- response_data=data,
360
- duration=duration,
361
- )
362
- else:
363
- error_text = await response.text()
364
- return TestResult(
365
- test_name=test_name,
366
- server_url=server_url,
367
- auth_type=auth_type,
368
- success=False,
369
- status_code=response.status,
370
- error_message=f"Security command failed: {error_text}",
371
- duration=duration,
372
- )
373
- except Exception as e:
374
- duration = time.time() - start_time
375
- return TestResult(
376
- test_name=test_name,
377
- server_url=server_url,
378
- auth_type=auth_type,
379
- success=False,
380
- error_message=f"Security command error: {str(e)}",
381
- duration=duration,
382
- )
383
-
384
- async def test_health(self) -> TestResult:
385
- """Test health endpoint."""
386
- return await self.test_health_check(self.base_url, "none")
387
-
388
- async def test_command_execution(self) -> TestResult:
389
- """Test command execution."""
390
- return await self.test_echo_command(self.base_url, "none")
391
-
392
- async def test_authentication(self) -> TestResult:
393
- """Test authentication."""
394
- if "api_key" in self.auth_methods:
395
- # Use admin API key value, not the key name
396
- api_key_value = self.api_keys.get("admin", "admin-secret-key")
397
- return await self.test_echo_command(self.base_url, "api_key", token=api_key_value)
398
- elif "certificate" in self.auth_methods:
399
- # For certificate auth, test with client certificate
400
- return await self.test_echo_command(self.base_url, "certificate")
401
- else:
402
- return TestResult(
403
- test_name="Authentication Test",
404
- server_url=self.base_url,
405
- auth_type="none",
406
- success=False,
407
- error_message="No authentication method available",
408
- )
409
-
410
- async def test_negative_authentication(self) -> TestResult:
411
- """Test negative authentication (should fail)."""
412
- start_time = time.time()
413
- test_name = "Negative Authentication Test"
414
- try:
415
- headers = self.create_auth_headers("api_key", token="invalid-token")
416
- data = {
417
- "jsonrpc": "2.0",
418
- "method": "echo",
419
- "params": {"message": "Should fail with invalid token"},
420
- "id": 1,
421
- }
422
- async with self.session.post(
423
- f"{self.base_url}/cmd", headers=headers, json=data
424
- ) as response:
425
- duration = time.time() - start_time
426
-
427
- # Check if API key authentication is enabled
428
- api_key_auth_enabled = self.auth_enabled and "api_key" in self.auth_methods
429
-
430
- if api_key_auth_enabled:
431
- # For configurations with API key auth, 401 is expected (success)
432
- if response.status == 401:
433
- return TestResult(
434
- test_name=test_name,
435
- server_url=self.base_url,
436
- auth_type="api_key",
437
- success=True,
438
- status_code=response.status,
439
- response_data={"expected": "authentication_failure"},
440
- duration=duration,
441
- )
442
- else:
443
- return TestResult(
444
- test_name=test_name,
445
- server_url=self.base_url,
446
- auth_type="api_key",
447
- success=False,
448
- status_code=response.status,
449
- error_message=f"Expected 401 Unauthorized, got {response.status}",
450
- duration=duration,
451
- )
452
- else:
453
- # For configurations without API key auth, 200 is expected (success)
454
- if response.status == 200:
455
- return TestResult(
456
- test_name=test_name,
457
- server_url=self.base_url,
458
- auth_type="api_key",
459
- success=True,
460
- status_code=response.status,
461
- response_data={"expected": "no_auth_required"},
462
- duration=duration,
463
- )
464
- else:
465
- return TestResult(
466
- test_name=test_name,
467
- server_url=self.base_url,
468
- auth_type="api_key",
469
- success=False,
470
- status_code=response.status,
471
- error_message=f"Expected 200 OK (no auth required), got {response.status}",
472
- duration=duration,
473
- )
474
- except Exception as e:
475
- duration = time.time() - start_time
476
- return TestResult(
477
- test_name=test_name,
478
- server_url=self.base_url,
479
- auth_type="api_key",
480
- success=False,
481
- error_message=f"Negative auth test error: {str(e)}",
482
- duration=duration,
483
- )
484
-
485
- async def test_no_auth_required(self) -> TestResult:
486
- """Test that no authentication is required."""
487
- return await self.test_echo_command(self.base_url, "none")
488
-
489
- async def test_negative_auth(
490
- self, server_url: str, auth_type: str = "none", **kwargs
491
- ) -> TestResult:
492
- """Test negative authentication scenarios."""
493
- start_time = time.time()
494
- test_name = f"Negative Auth ({auth_type})"
495
- try:
496
- if auth_type == "certificate":
497
- # For mTLS, test with invalid/expired certificate or no certificate
498
- from aiohttp import ClientTimeout, TCPConnector
499
-
500
- # Create SSL context with wrong certificate (should be rejected)
501
- ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
502
- ssl_context.check_hostname = False
503
- # Don't load any client certificate - this should cause rejection
504
- # Load CA certificate for server verification
505
- ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
506
- if os.path.exists(ca_cert_file):
507
- ssl_context.load_verify_locations(cafile=ca_cert_file)
508
- ssl_context.verify_mode = (
509
- ssl.CERT_NONE
510
- ) # Don't verify server cert for testing
511
-
512
- connector = TCPConnector(ssl=ssl_context)
513
- timeout = ClientTimeout(total=10) # Shorter timeout
514
-
515
- try:
516
- import aiohttp
517
-
518
- async with aiohttp.ClientSession(
519
- timeout=timeout, connector=connector
520
- ) as temp_session:
521
- data = {
522
- "jsonrpc": "2.0",
523
- "method": "echo",
524
- "params": {"message": "Should fail without certificate"},
525
- "id": 3,
526
- }
527
- async with temp_session.post(
528
- f"{server_url}/cmd", json=data
529
- ) as response:
530
- duration = time.time() - start_time
531
- # If we get here, the server accepted the connection without proper certificate
532
- # This is actually a security issue - server should reject
533
- return TestResult(
534
- test_name=test_name,
535
- server_url=server_url,
536
- auth_type=auth_type,
537
- success=False,
538
- status_code=response.status,
539
- error_message=f"SECURITY ISSUE: mTLS server accepted connection without client certificate (status: {response.status})",
540
- duration=duration,
541
- )
542
- except (Exception,) as e:
543
- # This is expected - server should reject connections without proper certificate
544
- duration = time.time() - start_time
545
- return TestResult(
546
- test_name=test_name,
547
- server_url=server_url,
548
- auth_type=auth_type,
549
- success=True,
550
- status_code=0,
551
- response_data={
552
- "expected": "connection_rejected",
553
- "error": str(e),
554
- },
555
- duration=duration,
556
- )
557
- else:
558
- # For other auth types, use invalid token
559
- headers = self.create_auth_headers("api_key", token="invalid-token-999")
560
- data = {
561
- "jsonrpc": "2.0",
562
- "method": "echo",
563
- "params": {"message": "Should fail"},
564
- "id": 3,
565
- }
566
- async with self.session.post(
567
- f"{server_url}/cmd", headers=headers, json=data
568
- ) as response:
569
- duration = time.time() - start_time
570
- # Expect 401 only when auth is enforced
571
- expects_auth = auth_type in ("api_key", "certificate", "basic")
572
- if expects_auth and response.status == 401:
573
- return TestResult(
574
- test_name=test_name,
575
- server_url=server_url,
576
- auth_type=auth_type,
577
- success=True,
578
- status_code=response.status,
579
- response_data={"expected": "authentication_failure"},
580
- duration=duration,
581
- )
582
- elif not expects_auth and response.status == 200:
583
- # Security disabled: negative auth should not fail
584
- return TestResult(
585
- test_name=test_name,
586
- server_url=server_url,
587
- auth_type=auth_type,
588
- success=True,
589
- status_code=response.status,
590
- response_data={"expected": "no_auth_required"},
591
- duration=duration,
592
- )
593
- else:
594
- return TestResult(
595
- test_name=test_name,
596
- server_url=server_url,
597
- auth_type=auth_type,
598
- success=False,
599
- status_code=response.status,
600
- error_message=f"Unexpected status for negative auth: {response.status}",
601
- duration=duration,
602
- )
603
- except Exception as e:
604
- duration = time.time() - start_time
605
- return TestResult(
606
- test_name=test_name,
607
- server_url=server_url,
608
- auth_type=auth_type,
609
- success=False,
610
- error_message=f"Negative auth error: {str(e)}",
611
- duration=duration,
612
- )
613
-
614
- async def test_role_based_access(
615
- self, server_url: str, auth_type: str = "none", **kwargs
616
- ) -> TestResult:
617
- """Test role-based access control."""
618
- start_time = time.time()
619
- test_name = f"Role-Based Access ({auth_type})"
620
- try:
621
- # Test with different roles
622
- role = kwargs.get("role")
623
- if not role:
624
- raise ValueError("role is required for role-based access test")
625
- token = self.test_tokens.get(role)
626
- if not token:
627
- raise ValueError(f"token for role '{role}' is not configured")
628
- headers = self.create_auth_headers("api_key", token=token)
629
- data = {
630
- "jsonrpc": "2.0",
631
- "method": "echo",
632
- "params": {"message": f"Testing {role} role"},
633
- "id": 4,
634
- }
635
- async with self.session.post(
636
- f"{server_url}/cmd", headers=headers, json=data
637
- ) as response:
638
- duration = time.time() - start_time
639
- if response.status == 200:
640
- data = await response.json()
641
- return TestResult(
642
- test_name=test_name,
643
- server_url=server_url,
644
- auth_type=auth_type,
645
- success=True,
646
- status_code=response.status,
647
- response_data=data,
648
- duration=duration,
649
- )
650
- else:
651
- error_text = await response.text()
652
- return TestResult(
653
- test_name=test_name,
654
- server_url=server_url,
655
- auth_type=auth_type,
656
- success=False,
657
- status_code=response.status,
658
- error_message=f"Role-based access failed: {error_text}",
659
- duration=duration,
660
- )
661
- except Exception as e:
662
- duration = time.time() - start_time
663
- return TestResult(
664
- test_name=test_name,
665
- server_url=server_url,
666
- auth_type=auth_type,
667
- success=False,
668
- error_message=f"Role-based access error: {str(e)}",
669
- duration=duration,
670
- )
671
-
672
- async def test_role_permissions(
673
- self, server_url: str, auth_type: str = "none", **kwargs
674
- ) -> TestResult:
675
- """Test role permissions with role_test command."""
676
- start_time = time.time()
677
- test_name = f"Role Permissions Test ({auth_type})"
678
- try:
679
- # Test with different roles and actions
680
- role = kwargs.get("role")
681
- action = kwargs.get("action")
682
- if not role:
683
- raise ValueError("role is required for role permissions test")
684
- if not action:
685
- raise ValueError("action is required for role permissions test")
686
- token = self.test_tokens.get(role)
687
- if not token:
688
- raise ValueError(f"token for role '{role}' is not configured")
689
- headers = self.create_auth_headers("api_key", token=token)
690
- data = {
691
- "jsonrpc": "2.0",
692
- "method": "role_test",
693
- "params": {"action": action},
694
- "id": 5,
695
- }
696
- async with self.session.post(
697
- f"{server_url}/cmd", headers=headers, json=data
698
- ) as response:
699
- duration = time.time() - start_time
700
- if response.status == 200:
701
- data = await response.json()
702
- return TestResult(
703
- test_name=test_name,
704
- server_url=server_url,
705
- auth_type=auth_type,
706
- success=True,
707
- status_code=response.status,
708
- response_data=data,
709
- duration=duration,
710
- )
711
- else:
712
- error_text = await response.text()
713
- return TestResult(
714
- test_name=test_name,
715
- server_url=server_url,
716
- auth_type=auth_type,
717
- success=False,
718
- status_code=response.status,
719
- error_message=f"Role permissions test failed: {error_text}",
720
- duration=duration,
721
- )
722
- except Exception as e:
723
- duration = time.time() - start_time
724
- return TestResult(
725
- test_name=test_name,
726
- server_url=server_url,
727
- auth_type=auth_type,
728
- success=False,
729
- error_message=f"Role permissions test error: {str(e)}",
730
- duration=duration,
731
- )
732
-
733
- async def test_multiple_roles(
734
- self, server_url: str, auth_type: str = "none", **kwargs
735
- ) -> TestResult:
736
- """Test multiple roles with different permissions."""
737
- start_time = time.time()
738
- test_name = f"Multiple Roles Test ({auth_type})"
739
- try:
740
- # Test admin role (should have all permissions)
741
- admin_token = self.test_tokens.get("admin")
742
- if not admin_token:
743
- raise ValueError("admin token is not configured")
744
- admin_headers = self.create_auth_headers("api_key", token=admin_token)
745
- admin_data = {
746
- "jsonrpc": "2.0",
747
- "method": "role_test",
748
- "params": {"action": "manage"},
749
- "id": 6,
750
- }
751
- async with self.session.post(
752
- f"{server_url}/cmd", headers=admin_headers, json=admin_data
753
- ) as response:
754
- if response.status != 200:
755
- return TestResult(
756
- test_name=test_name,
757
- server_url=server_url,
758
- auth_type=auth_type,
759
- success=False,
760
- status_code=response.status,
761
- error_message="Admin role test failed",
762
- duration=time.time() - start_time,
763
- )
764
- # Test readonly role (should only have read permission)
765
- readonly_token = self.test_tokens.get("readonly")
766
- if not readonly_token:
767
- raise ValueError("readonly token is not configured")
768
- readonly_headers = self.create_auth_headers(
769
- "api_key", token=readonly_token
770
- )
771
- readonly_data = {
772
- "jsonrpc": "2.0",
773
- "method": "role_test",
774
- "params": {"action": "write"},
775
- }
776
- async with self.session.post(
777
- f"{server_url}/cmd", headers=readonly_headers, json=readonly_data
778
- ) as response:
779
- duration = time.time() - start_time
780
- # Readonly should be denied write access
781
- if response.status == 403:
782
- return TestResult(
783
- test_name=test_name,
784
- server_url=server_url,
785
- auth_type=auth_type,
786
- success=True,
787
- status_code=response.status,
788
- response_data={
789
- "message": "Correctly denied write access to readonly role"
790
- },
791
- duration=duration,
792
- )
793
- else:
794
- return TestResult(
795
- test_name=test_name,
796
- server_url=server_url,
797
- auth_type=auth_type,
798
- success=False,
799
- status_code=response.status,
800
- error_message="Readonly role incorrectly allowed write access",
801
- duration=duration,
802
- )
803
- except Exception as e:
804
- duration = time.time() - start_time
805
- return TestResult(
806
- test_name=test_name,
807
- server_url=server_url,
808
- auth_type=auth_type,
809
- success=False,
810
- error_message=f"Multiple roles test error: {str(e)}",
811
- duration=duration,
812
- )
813
-
814
- async def run_security_tests(
815
- self, server_url: str, auth_type: str = "none", **kwargs
816
- ) -> List[TestResult]:
817
- """Run comprehensive security tests."""
818
- print(f"\nšŸ”’ Running security tests for {server_url} ({auth_type})")
819
- print("=" * 60)
820
- tests = [
821
- self.test_health_check(server_url, auth_type, **kwargs),
822
- self.test_echo_command(server_url, auth_type, **kwargs),
823
- self.test_security_command(server_url, auth_type, **kwargs),
824
- self.test_negative_auth(server_url, auth_type, **kwargs),
825
- self.test_role_based_access(server_url, auth_type, role="admin", **kwargs),
826
- ]
827
- results = []
828
- for test in tests:
829
- result = await test
830
- results.append(result)
831
- self.test_results.append(result)
832
- # Print result
833
- status = "āœ… PASS" if result.success else "āŒ FAIL"
834
- print(f"{status} {result.test_name}")
835
- print(f" Duration: {result.duration:.3f}s")
836
- if result.status_code:
837
- print(f" Status: {result.status_code}")
838
- if result.error_message:
839
- print(f" Error: {result.error_message}")
840
- print()
841
- return results
842
-
843
- async def test_all_scenarios(self) -> Dict[str, List[TestResult]]:
844
- """Test all security scenarios."""
845
- scenarios = {
846
- "basic_http": {"url": "http://localhost:8000", "auth": "none"},
847
- "http_token": {"url": "http://localhost:8001", "auth": "api_key"},
848
- "https": {"url": "https://localhost:8443", "auth": "none"},
849
- "https_token": {"url": "https://localhost:8444", "auth": "api_key"},
850
- "mtls": {"url": "https://localhost:8445", "auth": "certificate"},
851
- }
852
- all_results = {}
853
- for scenario_name, config in scenarios.items():
854
- print(f"\nšŸš€ Testing scenario: {scenario_name.upper()}")
855
- print("=" * 60)
856
- try:
857
- results = await self.run_security_tests(config["url"], config["auth"])
858
- all_results[scenario_name] = results
859
- except Exception as e:
860
- print(f"āŒ Failed to test {scenario_name}: {e}")
861
- all_results[scenario_name] = []
862
- return all_results
863
-
864
- def print_summary(self):
865
- """Print test summary."""
866
- print("\n" + "=" * 80)
867
- print("šŸ“Š SECURITY TEST SUMMARY")
868
- print("=" * 80)
869
- total_tests = len(self.test_results)
870
- passed_tests = sum(1 for r in self.test_results if r.success)
871
- failed_tests = total_tests - passed_tests
872
- print(f"Total Tests: {total_tests}")
873
- print(f"Passed: {passed_tests} āœ…")
874
- print(f"Failed: {failed_tests} āŒ")
875
- print(f"Success Rate: {(passed_tests / total_tests * 100):.1f}%")
876
- if failed_tests > 0:
877
- print("\nāŒ Failed Tests:")
878
- for result in self.test_results:
879
- if not result.success:
880
- print(f" - {result.test_name} ({result.server_url})")
881
- if result.error_message:
882
- print(f" Error: {result.error_message}")
883
- print("\nāœ… Passed Tests:")
884
- for result in self.test_results:
885
- if result.success:
886
- print(f" - {result.test_name} ({result.server_url})")
887
-
888
-
889
- async def test_health(self) -> TestResult:
890
- """Test health check endpoint."""
891
- return await self.test_health_check(self.base_url, "none")
892
-
893
- async def test_command_execution(self) -> TestResult:
894
- """Test command execution."""
895
- if self.auth_enabled and "api_key" in self.auth_methods:
896
- # Use admin API key value, not the key name
897
- api_key_value = self.api_keys.get("admin", "admin-secret-key")
898
- return await self.test_echo_command(self.base_url, "api_key", token=api_key_value)
899
- else:
900
- return await self.test_echo_command(self.base_url, "none")
901
-
902
- async def test_authentication(self) -> TestResult:
903
- """Test authentication."""
904
- if "api_key" in self.auth_methods:
905
- # Use admin API key value, not the key name
906
- api_key_value = self.api_keys.get("admin", "admin-secret-key")
907
- return await self.test_echo_command(self.base_url, "api_key", token=api_key_value)
908
- elif "certificate" in self.auth_methods:
909
- # For certificate auth, test with client certificate
910
- return await self.test_echo_command(self.base_url, "certificate")
911
- else:
912
- return TestResult(
913
- test_name="Authentication Test",
914
- server_url=self.base_url,
915
- auth_type="none",
916
- success=False,
917
- error_message="No authentication method available",
918
- )
919
-
920
- async def test_negative_authentication(self) -> TestResult:
921
- """Test negative authentication (should fail)."""
922
- start_time = time.time()
923
- test_name = "Negative Authentication Test"
924
- try:
925
- headers = self.create_auth_headers("api_key", token="invalid-token")
926
- data = {
927
- "jsonrpc": "2.0",
928
- "method": "echo",
929
- "params": {"message": "Should fail with invalid token"},
930
- "id": 1,
931
- }
932
- async with self.session.post(
933
- f"{self.base_url}/cmd", headers=headers, json=data
934
- ) as response:
935
- duration = time.time() - start_time
936
-
937
- # Check if API key authentication is enabled
938
- api_key_auth_enabled = self.auth_enabled and "api_key" in self.auth_methods
939
-
940
- if api_key_auth_enabled:
941
- # For configurations with API key auth, 401 is expected (success)
942
- if response.status == 401:
943
- return TestResult(
944
- test_name=test_name,
945
- server_url=self.base_url,
946
- auth_type="api_key",
947
- success=True,
948
- status_code=response.status,
949
- response_data={"expected": "authentication_failure"},
950
- duration=duration,
951
- )
952
- else:
953
- return TestResult(
954
- test_name=test_name,
955
- server_url=self.base_url,
956
- auth_type="api_key",
957
- success=False,
958
- status_code=response.status,
959
- error_message=f"Expected 401 Unauthorized, got {response.status}",
960
- duration=duration,
961
- )
962
- else:
963
- # For configurations without API key auth, 200 is expected (success)
964
- if response.status == 200:
965
- return TestResult(
966
- test_name=test_name,
967
- server_url=self.base_url,
968
- auth_type="api_key",
969
- success=True,
970
- status_code=response.status,
971
- response_data={"expected": "no_auth_required"},
972
- duration=duration,
973
- )
974
- else:
975
- return TestResult(
976
- test_name=test_name,
977
- server_url=self.base_url,
978
- auth_type="api_key",
979
- success=False,
980
- status_code=response.status,
981
- error_message=f"Expected 200 OK (no auth required), got {response.status}",
982
- duration=duration,
983
- )
984
- except Exception as e:
985
- duration = time.time() - start_time
986
- return TestResult(
987
- test_name=test_name,
988
- server_url=self.base_url,
989
- auth_type="api_key",
990
- success=False,
991
- error_message=f"Negative auth test error: {str(e)}",
992
- duration=duration,
993
- )
994
-
995
- async def test_no_auth_required(self) -> TestResult:
996
- """Test that no authentication is required."""
997
- return await self.test_echo_command(self.base_url, "none")
998
-
999
- async def test_role_based_access(self, server_url: str, auth_type: str, role: str = "admin") -> TestResult:
1000
- """Test role-based access control."""
1001
- if not self.roles_file and not self.roles:
1002
- return TestResult(
1003
- test_name="Role-Based Access Test",
1004
- server_url=server_url,
1005
- auth_type=auth_type,
1006
- success=False,
1007
- error_message="Role-based access error: role is required for role-based access test",
1008
- )
1009
-
1010
- # Use admin role for testing
1011
- if auth_type == "api_key":
1012
- api_key_value = self.api_keys.get("admin", "admin-secret-key")
1013
- return await self.test_echo_command(server_url, auth_type, token=api_key_value, role=role)
1014
- else:
1015
- return await self.test_echo_command(server_url, auth_type, role=role)
1016
-
1017
- async def test_role_permissions(self, server_url: str, auth_type: str, role: str = "admin", action: str = "read") -> TestResult:
1018
- """Test role permissions."""
1019
- if not self.roles_file and not self.roles:
1020
- return TestResult(
1021
- test_name="Role Permissions Test",
1022
- server_url=server_url,
1023
- auth_type=auth_type,
1024
- success=False,
1025
- error_message="Role permissions test error: role is required for role permissions test",
1026
- )
1027
-
1028
- # Test with admin role
1029
- if auth_type == "api_key":
1030
- api_key_value = self.api_keys.get("admin", "admin-secret-key")
1031
- return await self.test_echo_command(server_url, auth_type, token=api_key_value, role=role)
1032
- else:
1033
- return await self.test_echo_command(server_url, auth_type, role=role)
1034
-
1035
- async def test_multiple_roles(self, server_url: str, auth_type: str) -> TestResult:
1036
- """Test multiple roles."""
1037
- # Test with readonly role (should have read access)
1038
- if auth_type == "api_key":
1039
- api_key_value = self.api_keys.get("readonly", "readonly-token-123")
1040
- result = await self.test_echo_command(server_url, auth_type, token=api_key_value, role="readonly")
1041
- if result.success:
1042
- return TestResult(
1043
- test_name="Multiple Roles Test",
1044
- server_url=server_url,
1045
- auth_type=auth_type,
1046
- success=True,
1047
- response_data={"message": "Readonly role correctly has read access"},
1048
- )
1049
- else:
1050
- return TestResult(
1051
- test_name="Multiple Roles Test",
1052
- server_url=server_url,
1053
- auth_type=auth_type,
1054
- success=False,
1055
- error_message="Readonly role incorrectly denied read access",
1056
- )
1057
- elif auth_type == "certificate":
1058
- # For certificate auth, test with user certificate (should have read access)
1059
- result = await self.test_echo_command(server_url, auth_type, role="user")
1060
- if result.success:
1061
- return TestResult(
1062
- test_name="Multiple Roles Test",
1063
- server_url=server_url,
1064
- auth_type=auth_type,
1065
- success=True,
1066
- response_data={"message": "User certificate correctly has read access"},
1067
- )
1068
- else:
1069
- return TestResult(
1070
- test_name="Multiple Roles Test",
1071
- server_url=server_url,
1072
- auth_type=auth_type,
1073
- success=False,
1074
- error_message="User certificate incorrectly denied read access",
1075
- )
1076
- else:
1077
- return TestResult(
1078
- test_name="Multiple Roles Test",
1079
- server_url=server_url,
1080
- auth_type=auth_type,
1081
- success=False,
1082
- error_message="Multiple roles test not implemented for this auth type",
1083
- )
43
+ # Import security test components
44
+ from .security_test import SecurityTestClient
1084
45
 
1085
46
 
1086
47
  async def main():
1087
- """Main function."""
1088
- import argparse
1089
-
1090
- parser = argparse.ArgumentParser(
1091
- description="Security Test Client for MCP Proxy Adapter"
1092
- )
1093
- parser.add_argument(
1094
- "--server", default="http://localhost:8000", help="Server URL to test"
1095
- )
1096
- parser.add_argument(
1097
- "--auth",
1098
- choices=["none", "api_key", "basic", "certificate"],
1099
- default="none",
1100
- help="Authentication type",
1101
- )
1102
- parser.add_argument(
1103
- "--all-scenarios", action="store_true", help="Test all security scenarios"
1104
- )
1105
- parser.add_argument("--token", help="API token for authentication")
1106
- parser.add_argument("--cert", help="Client certificate file")
1107
- parser.add_argument("--key", help="Client private key file")
1108
- parser.add_argument("--ca-cert", help="CA certificate file")
1109
- args = parser.parse_args()
1110
- if args.all_scenarios:
1111
- # Test all scenarios
1112
- async with SecurityTestClient() as client:
1113
- await client.test_all_scenarios()
1114
- client.print_summary()
1115
- else:
1116
- # Test single server
1117
- async with SecurityTestClient(args.server) as client:
1118
- await client.run_security_tests(args.server, args.auth, token=args.token)
1119
- client.print_summary()
48
+ """Main function to run security tests."""
49
+ print("šŸš€ Starting MCP Proxy Adapter Security Tests")
50
+ print("=" * 50)
51
+
52
+ # Define test servers
53
+ test_servers = [
54
+ "http://localhost:8080", # HTTP Basic
55
+ "http://localhost:8080", # HTTP + Token
56
+ "https://localhost:8443", # HTTPS Basic
57
+ "https://localhost:8443", # HTTPS + Token
58
+ "https://localhost:20006", # mTLS Basic
59
+ "https://localhost:20007", # mTLS + Token
60
+ "https://localhost:20008", # mTLS + Roles
61
+ ]
62
+
63
+ # Run security tests
64
+ async with SecurityTestClient() as client:
65
+ results = await client.run_security_tests(test_servers)
66
+ client.print_summary()
67
+
68
+ print("\\nšŸŽ‰ Security tests completed!")
1120
69
 
1121
70
 
1122
71
  if __name__ == "__main__":