mcp-security-framework 1.1.0__py3-none-any.whl → 1.1.1__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_security_framework/__init__.py +26 -15
- mcp_security_framework/cli/__init__.py +1 -1
- mcp_security_framework/cli/cert_cli.py +233 -197
- mcp_security_framework/cli/security_cli.py +324 -234
- mcp_security_framework/constants.py +21 -27
- mcp_security_framework/core/auth_manager.py +41 -22
- mcp_security_framework/core/cert_manager.py +210 -147
- mcp_security_framework/core/permission_manager.py +9 -9
- mcp_security_framework/core/rate_limiter.py +2 -2
- mcp_security_framework/core/security_manager.py +284 -229
- mcp_security_framework/examples/__init__.py +6 -0
- mcp_security_framework/examples/comprehensive_example.py +349 -279
- mcp_security_framework/examples/django_example.py +247 -206
- mcp_security_framework/examples/fastapi_example.py +315 -283
- mcp_security_framework/examples/flask_example.py +274 -203
- mcp_security_framework/examples/gateway_example.py +304 -237
- mcp_security_framework/examples/microservice_example.py +258 -189
- mcp_security_framework/examples/standalone_example.py +255 -230
- mcp_security_framework/examples/test_all_examples.py +151 -135
- mcp_security_framework/middleware/__init__.py +46 -55
- mcp_security_framework/middleware/auth_middleware.py +62 -63
- mcp_security_framework/middleware/fastapi_auth_middleware.py +119 -118
- mcp_security_framework/middleware/fastapi_middleware.py +156 -148
- mcp_security_framework/middleware/flask_auth_middleware.py +160 -147
- mcp_security_framework/middleware/flask_middleware.py +183 -157
- mcp_security_framework/middleware/mtls_middleware.py +106 -117
- mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
- mcp_security_framework/middleware/security_middleware.py +109 -124
- mcp_security_framework/schemas/config.py +2 -1
- mcp_security_framework/schemas/models.py +18 -6
- mcp_security_framework/utils/cert_utils.py +14 -8
- mcp_security_framework/utils/datetime_compat.py +116 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/METADATA +2 -1
- mcp_security_framework-1.1.1.dist-info/RECORD +84 -0
- tests/conftest.py +63 -66
- tests/test_cli/test_cert_cli.py +184 -146
- tests/test_cli/test_security_cli.py +274 -247
- tests/test_core/test_cert_manager.py +24 -10
- tests/test_core/test_security_manager.py +2 -2
- tests/test_examples/test_comprehensive_example.py +190 -137
- tests/test_examples/test_fastapi_example.py +124 -101
- tests/test_examples/test_flask_example.py +124 -101
- tests/test_examples/test_standalone_example.py +73 -80
- tests/test_integration/test_auth_flow.py +213 -197
- tests/test_integration/test_certificate_flow.py +180 -149
- tests/test_integration/test_fastapi_integration.py +108 -111
- tests/test_integration/test_flask_integration.py +141 -140
- tests/test_integration/test_standalone_integration.py +290 -259
- tests/test_middleware/test_fastapi_auth_middleware.py +195 -174
- tests/test_middleware/test_fastapi_middleware.py +147 -132
- tests/test_middleware/test_flask_auth_middleware.py +260 -202
- tests/test_middleware/test_flask_middleware.py +201 -179
- tests/test_middleware/test_security_middleware.py +145 -130
- tests/test_utils/test_datetime_compat.py +147 -0
- mcp_security_framework-1.1.0.dist-info/RECORD +0 -82
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/top_level.txt +0 -0
@@ -8,21 +8,32 @@ email: vasilyvz@gmail.com
|
|
8
8
|
"""
|
9
9
|
|
10
10
|
import json
|
11
|
-
import pytest
|
12
11
|
import os
|
13
|
-
from unittest.mock import
|
12
|
+
from unittest.mock import AsyncMock, Mock, patch
|
13
|
+
|
14
|
+
import pytest
|
14
15
|
from flask import Request
|
15
16
|
|
16
|
-
from mcp_security_framework.middleware.flask_auth_middleware import FlaskAuthMiddleware
|
17
|
-
from mcp_security_framework.middleware.auth_middleware import AuthMiddlewareError, AuthMiddleware
|
18
17
|
from mcp_security_framework.core.security_manager import SecurityManager
|
19
|
-
from mcp_security_framework.
|
20
|
-
|
18
|
+
from mcp_security_framework.middleware.auth_middleware import (
|
19
|
+
AuthMiddleware,
|
20
|
+
AuthMiddlewareError,
|
21
|
+
)
|
22
|
+
from mcp_security_framework.middleware.flask_auth_middleware import FlaskAuthMiddleware
|
23
|
+
from mcp_security_framework.schemas.config import (
|
24
|
+
AuthConfig,
|
25
|
+
LoggingConfig,
|
26
|
+
PermissionConfig,
|
27
|
+
RateLimitConfig,
|
28
|
+
SecurityConfig,
|
29
|
+
SSLConfig,
|
30
|
+
)
|
31
|
+
from mcp_security_framework.schemas.models import AuthMethod, AuthResult, AuthStatus
|
21
32
|
|
22
33
|
|
23
34
|
class TestFlaskAuthMiddleware:
|
24
35
|
"""Test suite for FlaskAuthMiddleware class."""
|
25
|
-
|
36
|
+
|
26
37
|
def setup_method(self):
|
27
38
|
"""Set up test fixtures before each test method."""
|
28
39
|
# Create test configuration
|
@@ -32,12 +43,12 @@ class TestFlaskAuthMiddleware:
|
|
32
43
|
methods=["api_key", "jwt", "certificate"],
|
33
44
|
api_keys={
|
34
45
|
"test_key_123": {"username": "testuser", "roles": ["user"]},
|
35
|
-
"admin_key_456": {"username": "admin", "roles": ["admin"]}
|
46
|
+
"admin_key_456": {"username": "admin", "roles": ["admin"]},
|
36
47
|
},
|
37
48
|
jwt_secret="test-super-secret-jwt-key-for-testing-purposes-only",
|
38
49
|
jwt_algorithm="HS256",
|
39
50
|
jwt_expiry_hours=24,
|
40
|
-
public_paths=["/health", "/metrics"]
|
51
|
+
public_paths=["/health", "/metrics"],
|
41
52
|
),
|
42
53
|
ssl=SSLConfig(enabled=False),
|
43
54
|
certificate=None,
|
@@ -46,454 +57,487 @@ class TestFlaskAuthMiddleware:
|
|
46
57
|
logging=LoggingConfig(enabled=True),
|
47
58
|
debug=False,
|
48
59
|
environment="test",
|
49
|
-
version="1.0.0"
|
60
|
+
version="1.0.0",
|
50
61
|
)
|
51
|
-
|
52
|
-
|
62
|
+
|
63
|
+
# Create temporary roles file for testing
|
53
64
|
import json
|
54
|
-
import tempfile
|
55
65
|
import os
|
56
|
-
|
66
|
+
import tempfile
|
67
|
+
|
57
68
|
roles_data = {
|
58
69
|
"admin": {
|
59
70
|
"permissions": ["read:own", "write:own", "delete:own", "admin", "*"],
|
60
|
-
"description": "Administrator role"
|
71
|
+
"description": "Administrator role",
|
61
72
|
},
|
62
73
|
"user": {
|
63
74
|
"permissions": ["read:own", "write:own"],
|
64
|
-
"description": "Regular user role"
|
75
|
+
"description": "Regular user role",
|
65
76
|
},
|
66
77
|
"readonly": {
|
67
78
|
"permissions": ["read:own"],
|
68
|
-
"description": "Read-only user role"
|
69
|
-
}
|
79
|
+
"description": "Read-only user role",
|
80
|
+
},
|
70
81
|
}
|
71
|
-
|
82
|
+
|
72
83
|
# Create temporary file
|
73
|
-
with tempfile.NamedTemporaryFile(mode=
|
84
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
74
85
|
json.dump({"roles": roles_data}, f)
|
75
86
|
temp_roles_file = f.name
|
76
|
-
|
87
|
+
|
77
88
|
# Update config with temporary file path
|
78
89
|
self.config.permissions.roles_file = temp_roles_file
|
79
|
-
|
90
|
+
|
80
91
|
# Create real security manager
|
81
92
|
from mcp_security_framework.core.security_manager import SecurityManager
|
93
|
+
|
82
94
|
self.security_manager = SecurityManager(self.config)
|
83
|
-
|
95
|
+
|
84
96
|
# Store temp file path for cleanup
|
85
97
|
self.temp_roles_file = temp_roles_file
|
86
|
-
|
87
|
-
|
98
|
+
|
99
|
+
# Create middleware instance
|
88
100
|
self.middleware = FlaskAuthMiddleware(self.security_manager)
|
89
101
|
|
90
102
|
# Create mock logger
|
91
103
|
self.mock_logger = Mock()
|
92
104
|
self.middleware.logger = self.mock_logger
|
93
|
-
|
105
|
+
|
94
106
|
def teardown_method(self):
|
95
107
|
"""Clean up after each test method."""
|
96
108
|
# Clean up temporary files
|
97
|
-
if hasattr(self,
|
109
|
+
if hasattr(self, "temp_roles_file") and os.path.exists(self.temp_roles_file):
|
98
110
|
try:
|
99
111
|
os.unlink(self.temp_roles_file)
|
100
112
|
except OSError:
|
101
113
|
pass
|
102
|
-
|
114
|
+
|
103
115
|
def test_flask_auth_middleware_initialization(self):
|
104
116
|
"""Test middleware initialization."""
|
105
117
|
assert isinstance(self.middleware, FlaskAuthMiddleware)
|
106
118
|
assert isinstance(self.middleware, AuthMiddleware)
|
107
119
|
assert self.middleware.config == self.config
|
108
120
|
assert self.middleware.security_manager == self.security_manager
|
109
|
-
|
121
|
+
|
110
122
|
def test_flask_auth_middleware_call_public_path(self):
|
111
123
|
"""Test middleware call with public path."""
|
112
124
|
# Create WSGI environment for public path
|
113
125
|
environ = {
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
126
|
+
"REQUEST_METHOD": "GET",
|
127
|
+
"PATH_INFO": "/health",
|
128
|
+
"HTTP_HOST": "localhost:5000",
|
129
|
+
"HTTP_USER_AGENT": "test-agent",
|
118
130
|
}
|
119
|
-
|
131
|
+
|
120
132
|
# Create mock start_response
|
121
133
|
mock_start_response = Mock()
|
122
|
-
|
134
|
+
|
123
135
|
# Call middleware
|
124
136
|
response = self.middleware(environ, mock_start_response)
|
125
|
-
|
137
|
+
|
126
138
|
# Assertions
|
127
139
|
assert response is not None
|
128
140
|
mock_start_response.assert_called_once()
|
129
|
-
|
141
|
+
|
130
142
|
def test_flask_auth_middleware_call_authentication_success(self):
|
131
143
|
"""Test middleware call with successful authentication."""
|
132
144
|
# Create WSGI environment
|
133
145
|
environ = {
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
146
|
+
"REQUEST_METHOD": "GET",
|
147
|
+
"PATH_INFO": "/api/v1/users/me",
|
148
|
+
"HTTP_HOST": "localhost:5000",
|
149
|
+
"HTTP_X_API_KEY": "test_key_123",
|
150
|
+
"HTTP_USER_AGENT": "test-agent",
|
139
151
|
}
|
140
|
-
|
152
|
+
|
141
153
|
# Mock successful authentication
|
142
154
|
auth_result = AuthResult(
|
143
155
|
is_valid=True,
|
144
156
|
status=AuthStatus.SUCCESS,
|
145
157
|
username="testuser",
|
146
158
|
roles=["user"],
|
147
|
-
auth_method=AuthMethod.API_KEY
|
159
|
+
auth_method=AuthMethod.API_KEY,
|
160
|
+
)
|
161
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
162
|
+
return_value=auth_result
|
148
163
|
)
|
149
|
-
|
150
|
-
|
164
|
+
|
151
165
|
# Create mock start_response
|
152
166
|
mock_start_response = Mock()
|
153
|
-
|
167
|
+
|
154
168
|
# Call middleware
|
155
169
|
response = self.middleware(environ, mock_start_response)
|
156
|
-
|
170
|
+
|
157
171
|
# Assertions
|
158
172
|
assert response is not None
|
159
173
|
mock_start_response.assert_called_once()
|
160
|
-
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
161
|
-
|
174
|
+
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
175
|
+
"test_key_123"
|
176
|
+
)
|
177
|
+
|
162
178
|
def test_flask_auth_middleware_call_authentication_failure(self):
|
163
179
|
"""Test middleware call with authentication failure."""
|
164
180
|
# Create WSGI environment
|
165
181
|
environ = {
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
182
|
+
"REQUEST_METHOD": "GET",
|
183
|
+
"PATH_INFO": "/api/v1/users/me",
|
184
|
+
"HTTP_HOST": "localhost:5000",
|
185
|
+
"HTTP_USER_AGENT": "test-agent",
|
170
186
|
}
|
171
|
-
|
187
|
+
|
172
188
|
# Mock failed authentication
|
173
189
|
auth_result = AuthResult(
|
174
190
|
is_valid=False,
|
175
191
|
status=AuthStatus.INVALID,
|
176
192
|
auth_method=AuthMethod.UNKNOWN,
|
177
193
|
error_code=-32001,
|
178
|
-
error_message="No authentication credentials provided"
|
194
|
+
error_message="No authentication credentials provided",
|
179
195
|
)
|
180
|
-
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
181
|
-
|
196
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
197
|
+
return_value=auth_result
|
198
|
+
)
|
199
|
+
|
182
200
|
# Create mock start_response
|
183
201
|
mock_start_response = Mock()
|
184
|
-
|
202
|
+
|
185
203
|
# Call middleware
|
186
204
|
response = self.middleware(environ, mock_start_response)
|
187
|
-
|
205
|
+
|
188
206
|
# Assertions
|
189
207
|
assert response is not None
|
190
208
|
mock_start_response.assert_called_once()
|
191
|
-
|
209
|
+
|
192
210
|
def test_flask_auth_middleware_call_exception_handling(self):
|
193
211
|
"""Test middleware call with exception handling."""
|
194
212
|
# This test is removed as the middleware properly handles exceptions
|
195
213
|
# by catching them and raising AuthMiddlewareError, which is the expected behavior
|
196
214
|
pass
|
197
|
-
|
215
|
+
|
198
216
|
def test_flask_auth_middleware_call_next(self):
|
199
217
|
"""Test _call_next method."""
|
200
218
|
environ = {
|
201
|
-
|
202
|
-
|
203
|
-
|
219
|
+
"REQUEST_METHOD": "GET",
|
220
|
+
"PATH_INFO": "/test",
|
221
|
+
"HTTP_HOST": "localhost:5000",
|
204
222
|
}
|
205
|
-
|
223
|
+
|
206
224
|
mock_start_response = Mock()
|
207
|
-
|
225
|
+
|
208
226
|
response = self.middleware._call_next(environ, mock_start_response)
|
209
|
-
|
227
|
+
|
210
228
|
assert response is not None
|
211
229
|
assert isinstance(response, list)
|
212
230
|
assert len(response) > 0
|
213
|
-
mock_start_response.assert_called_once_with(
|
214
|
-
|
231
|
+
mock_start_response.assert_called_once_with(
|
232
|
+
"200 OK", [("Content-Type", "application/json")]
|
233
|
+
)
|
234
|
+
|
215
235
|
def test_flask_auth_middleware_is_public_path_configured(self):
|
216
236
|
"""Test _is_public_path with configured public paths."""
|
217
237
|
mock_request = Mock(spec=Request)
|
218
238
|
mock_request.path = "/health"
|
219
|
-
|
239
|
+
|
220
240
|
result = self.middleware._is_public_path(mock_request)
|
221
241
|
assert result is True
|
222
|
-
|
242
|
+
|
223
243
|
def test_flask_auth_middleware_is_public_path_common(self):
|
224
244
|
"""Test _is_public_path with common public paths."""
|
225
245
|
mock_request = Mock(spec=Request)
|
226
246
|
mock_request.path = "/status"
|
227
|
-
|
247
|
+
|
228
248
|
result = self.middleware._is_public_path(mock_request)
|
229
249
|
assert result is True
|
230
|
-
|
250
|
+
|
231
251
|
def test_flask_auth_middleware_is_public_path_private(self):
|
232
252
|
"""Test _is_public_path with private path."""
|
233
253
|
mock_request = Mock(spec=Request)
|
234
254
|
mock_request.path = "/api/v1/users/me"
|
235
|
-
|
255
|
+
|
236
256
|
result = self.middleware._is_public_path(mock_request)
|
237
257
|
assert result is False
|
238
|
-
|
258
|
+
|
239
259
|
def test_flask_auth_middleware_get_client_ip_x_forwarded_for(self):
|
240
260
|
"""Test _get_client_ip with X-Forwarded-For header."""
|
241
261
|
mock_request = Mock(spec=Request)
|
242
262
|
mock_request.headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
243
|
-
|
263
|
+
|
244
264
|
ip = self.middleware._get_client_ip(mock_request)
|
245
265
|
assert ip == "192.168.1.1"
|
246
|
-
|
266
|
+
|
247
267
|
def test_flask_auth_middleware_get_client_ip_x_real_ip(self):
|
248
268
|
"""Test _get_client_ip with X-Real-IP header."""
|
249
269
|
mock_request = Mock(spec=Request)
|
250
270
|
mock_request.headers = {"X-Real-IP": "192.168.1.2"}
|
251
|
-
|
271
|
+
|
252
272
|
ip = self.middleware._get_client_ip(mock_request)
|
253
273
|
assert ip == "192.168.1.2"
|
254
|
-
|
274
|
+
|
255
275
|
def test_flask_auth_middleware_get_client_ip_remote_addr(self):
|
256
276
|
"""Test _get_client_ip with remote_addr."""
|
257
277
|
mock_request = Mock(spec=Request)
|
258
278
|
mock_request.headers = {}
|
259
279
|
mock_request.remote_addr = "192.168.1.3"
|
260
|
-
|
280
|
+
|
261
281
|
ip = self.middleware._get_client_ip(mock_request)
|
262
282
|
assert ip == "192.168.1.3"
|
263
|
-
|
283
|
+
|
264
284
|
def test_flask_auth_middleware_get_client_ip_default(self):
|
265
285
|
"""Test _get_client_ip with default fallback."""
|
266
286
|
mock_request = Mock(spec=Request)
|
267
287
|
mock_request.headers = {}
|
268
288
|
mock_request.remote_addr = None
|
269
|
-
|
289
|
+
|
270
290
|
ip = self.middleware._get_client_ip(mock_request)
|
271
291
|
assert ip == "127.0.0.1" # Default fallback
|
272
|
-
|
292
|
+
|
273
293
|
def test_flask_auth_middleware_get_cache_key(self):
|
274
294
|
"""Test _get_cache_key method."""
|
275
295
|
mock_request = Mock(spec=Request)
|
276
296
|
mock_request.headers = {"User-Agent": "test-browser/1.0"}
|
277
297
|
mock_request.remote_addr = "192.168.1.1"
|
278
|
-
|
298
|
+
|
279
299
|
cache_key = self.middleware._get_cache_key(mock_request)
|
280
300
|
assert cache_key.startswith("auth:192.168.1.1:")
|
281
301
|
assert isinstance(cache_key, str)
|
282
302
|
assert len(cache_key) > 0
|
283
|
-
|
303
|
+
|
284
304
|
def test_flask_auth_middleware_try_auth_method_api_key(self):
|
285
305
|
"""Test _try_auth_method with API key."""
|
286
306
|
mock_request = Mock(spec=Request)
|
287
307
|
mock_request.headers = {"X-API-Key": "test_key_123"}
|
288
|
-
|
308
|
+
|
289
309
|
auth_result = AuthResult(
|
290
310
|
is_valid=True,
|
291
311
|
status=AuthStatus.SUCCESS,
|
292
312
|
username="testuser",
|
293
313
|
roles=["user"],
|
294
|
-
auth_method=AuthMethod.API_KEY
|
314
|
+
auth_method=AuthMethod.API_KEY,
|
295
315
|
)
|
296
|
-
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
297
|
-
|
316
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
317
|
+
return_value=auth_result
|
318
|
+
)
|
319
|
+
|
298
320
|
result = self.middleware._try_auth_method(mock_request, "api_key")
|
299
|
-
|
321
|
+
|
300
322
|
assert result.is_valid is True
|
301
|
-
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
302
|
-
|
323
|
+
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
324
|
+
"test_key_123"
|
325
|
+
)
|
326
|
+
|
303
327
|
def test_flask_auth_middleware_try_auth_method_jwt(self):
|
304
328
|
"""Test _try_auth_method with JWT."""
|
305
329
|
mock_request = Mock(spec=Request)
|
306
330
|
mock_request.headers = {"Authorization": "Bearer test_jwt_token"}
|
307
|
-
|
331
|
+
|
308
332
|
auth_result = AuthResult(
|
309
333
|
is_valid=True,
|
310
334
|
status=AuthStatus.SUCCESS,
|
311
335
|
username="testuser",
|
312
336
|
roles=["user"],
|
313
|
-
auth_method=AuthMethod.JWT
|
337
|
+
auth_method=AuthMethod.JWT,
|
338
|
+
)
|
339
|
+
self.security_manager.auth_manager.authenticate_jwt_token = Mock(
|
340
|
+
return_value=auth_result
|
314
341
|
)
|
315
|
-
|
316
|
-
|
342
|
+
|
317
343
|
result = self.middleware._try_auth_method(mock_request, "jwt")
|
318
|
-
|
344
|
+
|
319
345
|
assert result.is_valid is True
|
320
|
-
self.security_manager.auth_manager.authenticate_jwt_token.assert_called_once_with(
|
321
|
-
|
346
|
+
self.security_manager.auth_manager.authenticate_jwt_token.assert_called_once_with(
|
347
|
+
"test_jwt_token"
|
348
|
+
)
|
349
|
+
|
322
350
|
def test_flask_auth_middleware_try_auth_method_certificate(self):
|
323
351
|
"""Test _try_auth_method with certificate."""
|
324
352
|
mock_request = Mock(spec=Request)
|
325
353
|
mock_request.headers = {"X-Client-Cert": "test_certificate_data"}
|
326
|
-
|
354
|
+
|
327
355
|
auth_result = AuthResult(
|
328
356
|
is_valid=True,
|
329
357
|
status=AuthStatus.SUCCESS,
|
330
358
|
username="testuser",
|
331
359
|
roles=["user"],
|
332
|
-
auth_method=AuthMethod.CERTIFICATE
|
360
|
+
auth_method=AuthMethod.CERTIFICATE,
|
361
|
+
)
|
362
|
+
self.security_manager.auth_manager.authenticate_certificate = Mock(
|
363
|
+
return_value=auth_result
|
333
364
|
)
|
334
|
-
|
335
|
-
|
365
|
+
|
336
366
|
result = self.middleware._try_auth_method(mock_request, "certificate")
|
337
|
-
|
367
|
+
|
338
368
|
assert result.is_valid is False
|
339
369
|
assert "not implemented" in result.error_message.lower()
|
340
|
-
|
370
|
+
|
341
371
|
def test_flask_auth_middleware_try_auth_method_unsupported(self):
|
342
372
|
"""Test _try_auth_method with unsupported method."""
|
343
373
|
mock_request = Mock(spec=Request)
|
344
|
-
|
374
|
+
|
345
375
|
result = self.middleware._try_auth_method(mock_request, "unsupported_method")
|
346
|
-
|
376
|
+
|
347
377
|
assert result.is_valid is False
|
348
378
|
assert result.error_code == -32022
|
349
379
|
assert "Unsupported authentication method" in result.error_message
|
350
|
-
|
380
|
+
|
351
381
|
def test_flask_auth_middleware_try_auth_method_exception(self):
|
352
382
|
"""Test _try_auth_method with exception."""
|
353
383
|
mock_request = Mock(spec=Request)
|
354
|
-
|
355
|
-
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
356
|
-
|
384
|
+
|
385
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
386
|
+
side_effect=Exception("Test auth error")
|
387
|
+
)
|
388
|
+
|
357
389
|
result = self.middleware._try_auth_method(mock_request, "api_key")
|
358
|
-
|
390
|
+
|
359
391
|
assert result.is_valid is False
|
360
392
|
assert result.error_code == -32023
|
361
393
|
assert "Authentication method api_key failed" in result.error_message
|
362
|
-
|
394
|
+
|
363
395
|
def test_flask_auth_middleware_try_api_key_auth_success(self):
|
364
396
|
"""Test _try_api_key_auth with successful authentication."""
|
365
397
|
mock_request = Mock(spec=Request)
|
366
398
|
mock_request.headers = {"X-API-Key": "test_key_123"}
|
367
|
-
|
399
|
+
|
368
400
|
auth_result = AuthResult(
|
369
401
|
is_valid=True,
|
370
402
|
status=AuthStatus.SUCCESS,
|
371
403
|
username="testuser",
|
372
404
|
roles=["user"],
|
373
|
-
auth_method=AuthMethod.API_KEY
|
405
|
+
auth_method=AuthMethod.API_KEY,
|
374
406
|
)
|
375
|
-
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
376
|
-
|
407
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
408
|
+
return_value=auth_result
|
409
|
+
)
|
410
|
+
|
377
411
|
result = self.middleware._try_api_key_auth(mock_request)
|
378
|
-
|
412
|
+
|
379
413
|
assert result.is_valid is True
|
380
|
-
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
381
|
-
|
414
|
+
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
415
|
+
"test_key_123"
|
416
|
+
)
|
417
|
+
|
382
418
|
def test_flask_auth_middleware_try_api_key_auth_from_authorization(self):
|
383
419
|
"""Test _try_api_key_auth with API key from Authorization header."""
|
384
420
|
mock_request = Mock(spec=Request)
|
385
421
|
mock_request.headers = {"Authorization": "Bearer api_key_123"}
|
386
|
-
|
422
|
+
|
387
423
|
auth_result = AuthResult(
|
388
424
|
is_valid=True,
|
389
425
|
status=AuthStatus.SUCCESS,
|
390
426
|
username="testuser",
|
391
427
|
roles=["user"],
|
392
|
-
auth_method=AuthMethod.API_KEY
|
428
|
+
auth_method=AuthMethod.API_KEY,
|
429
|
+
)
|
430
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
431
|
+
return_value=auth_result
|
393
432
|
)
|
394
|
-
|
395
|
-
|
433
|
+
|
396
434
|
result = self.middleware._try_api_key_auth(mock_request)
|
397
|
-
|
435
|
+
|
398
436
|
assert result.is_valid is True
|
399
|
-
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
400
|
-
|
437
|
+
self.security_manager.auth_manager.authenticate_api_key.assert_called_once_with(
|
438
|
+
"api_key_123"
|
439
|
+
)
|
440
|
+
|
401
441
|
def test_flask_auth_middleware_try_api_key_auth_no_key(self):
|
402
442
|
"""Test _try_api_key_auth with no API key."""
|
403
443
|
mock_request = Mock(spec=Request)
|
404
444
|
mock_request.headers = {}
|
405
|
-
|
445
|
+
|
406
446
|
result = self.middleware._try_api_key_auth(mock_request)
|
407
|
-
|
447
|
+
|
408
448
|
assert result.is_valid is False
|
409
449
|
assert result.error_code == -32012
|
410
450
|
assert "API key not found in request" in result.error_message
|
411
|
-
|
451
|
+
|
412
452
|
def test_flask_auth_middleware_try_jwt_auth_success(self):
|
413
453
|
"""Test _try_jwt_auth with successful authentication."""
|
414
454
|
mock_request = Mock(spec=Request)
|
415
455
|
mock_request.headers = {"Authorization": "Bearer test_jwt_token"}
|
416
|
-
|
456
|
+
|
417
457
|
auth_result = AuthResult(
|
418
458
|
is_valid=True,
|
419
459
|
status=AuthStatus.SUCCESS,
|
420
460
|
username="testuser",
|
421
461
|
roles=["user"],
|
422
|
-
auth_method=AuthMethod.JWT
|
462
|
+
auth_method=AuthMethod.JWT,
|
423
463
|
)
|
424
|
-
self.security_manager.auth_manager.authenticate_jwt_token = Mock(
|
425
|
-
|
464
|
+
self.security_manager.auth_manager.authenticate_jwt_token = Mock(
|
465
|
+
return_value=auth_result
|
466
|
+
)
|
467
|
+
|
426
468
|
result = self.middleware._try_jwt_auth(mock_request)
|
427
|
-
|
469
|
+
|
428
470
|
assert result.is_valid is True
|
429
|
-
self.security_manager.auth_manager.authenticate_jwt_token.assert_called_once_with(
|
430
|
-
|
471
|
+
self.security_manager.auth_manager.authenticate_jwt_token.assert_called_once_with(
|
472
|
+
"test_jwt_token"
|
473
|
+
)
|
474
|
+
|
431
475
|
def test_flask_auth_middleware_try_jwt_auth_no_token(self):
|
432
476
|
"""Test _try_jwt_auth with no JWT token."""
|
433
477
|
mock_request = Mock(spec=Request)
|
434
478
|
mock_request.headers = {}
|
435
|
-
|
479
|
+
|
436
480
|
result = self.middleware._try_jwt_auth(mock_request)
|
437
|
-
|
481
|
+
|
438
482
|
assert result.is_valid is False
|
439
483
|
assert result.error_code == -32013
|
440
484
|
assert "JWT token not found in Authorization header" in result.error_message
|
441
|
-
|
485
|
+
|
442
486
|
def test_flask_auth_middleware_try_jwt_auth_wrong_format(self):
|
443
487
|
"""Test _try_jwt_auth with wrong Authorization header format."""
|
444
488
|
mock_request = Mock(spec=Request)
|
445
489
|
mock_request.headers = {"Authorization": "Basic dGVzdDp0ZXN0"}
|
446
|
-
|
490
|
+
|
447
491
|
result = self.middleware._try_jwt_auth(mock_request)
|
448
|
-
|
492
|
+
|
449
493
|
assert result.is_valid is False
|
450
494
|
assert result.error_code == -32013
|
451
495
|
assert "JWT token not found in Authorization header" in result.error_message
|
452
|
-
|
496
|
+
|
453
497
|
def test_flask_auth_middleware_try_certificate_auth(self):
|
454
498
|
"""Test _try_certificate_auth (not implemented)."""
|
455
499
|
mock_request = Mock(spec=Request)
|
456
500
|
mock_request.headers = {"X-Client-Cert": "test_certificate_data"}
|
457
|
-
|
501
|
+
|
458
502
|
result = self.middleware._try_certificate_auth(mock_request)
|
459
|
-
|
503
|
+
|
460
504
|
assert result.is_valid is False
|
461
505
|
assert result.error_code == -32014
|
462
506
|
assert "Certificate authentication not implemented" in result.error_message
|
463
|
-
|
507
|
+
|
464
508
|
def test_flask_auth_middleware_try_basic_auth_no_credentials(self):
|
465
509
|
"""Test _try_basic_auth with no credentials."""
|
466
510
|
mock_request = Mock(spec=Request)
|
467
511
|
mock_request.headers = {}
|
468
|
-
|
512
|
+
|
469
513
|
result = self.middleware._try_basic_auth(mock_request)
|
470
|
-
|
514
|
+
|
471
515
|
assert result.is_valid is False
|
472
516
|
assert result.error_code == -32015
|
473
517
|
assert "Basic authentication credentials not found" in result.error_message
|
474
|
-
|
518
|
+
|
475
519
|
def test_flask_auth_middleware_try_basic_auth_wrong_format(self):
|
476
520
|
"""Test _try_basic_auth with wrong Authorization header format."""
|
477
521
|
mock_request = Mock(spec=Request)
|
478
522
|
mock_request.headers = {"Authorization": "Bearer token123"}
|
479
|
-
|
523
|
+
|
480
524
|
result = self.middleware._try_basic_auth(mock_request)
|
481
|
-
|
525
|
+
|
482
526
|
assert result.is_valid is False
|
483
527
|
assert result.error_code == -32015
|
484
528
|
assert "Basic authentication credentials not found" in result.error_message
|
485
|
-
|
529
|
+
|
486
530
|
def test_flask_auth_middleware_try_basic_auth_not_implemented(self):
|
487
531
|
"""Test _try_basic_auth (not implemented)."""
|
488
532
|
mock_request = Mock(spec=Request)
|
489
533
|
mock_request.headers = {"Authorization": "Basic dGVzdDp0ZXN0"}
|
490
|
-
|
534
|
+
|
491
535
|
result = self.middleware._try_basic_auth(mock_request)
|
492
|
-
|
536
|
+
|
493
537
|
assert result.is_valid is False
|
494
538
|
assert result.error_code == -32016
|
495
539
|
assert "Basic authentication not implemented" in result.error_message
|
496
|
-
|
540
|
+
|
497
541
|
def test_flask_auth_middleware_auth_error_response(self):
|
498
542
|
"""Test _auth_error_response method."""
|
499
543
|
auth_result = AuthResult(
|
@@ -501,61 +545,71 @@ class TestFlaskAuthMiddleware:
|
|
501
545
|
status=AuthStatus.INVALID,
|
502
546
|
auth_method=AuthMethod.API_KEY,
|
503
547
|
error_code=-32002,
|
504
|
-
error_message="Invalid API key"
|
548
|
+
error_message="Invalid API key",
|
505
549
|
)
|
506
|
-
|
550
|
+
|
507
551
|
mock_start_response = Mock()
|
508
|
-
|
509
|
-
response = self.middleware._auth_error_response(
|
510
|
-
|
552
|
+
|
553
|
+
response = self.middleware._auth_error_response(
|
554
|
+
auth_result, mock_start_response
|
555
|
+
)
|
556
|
+
|
511
557
|
assert response is not None
|
512
558
|
mock_start_response.assert_called_once()
|
513
|
-
|
559
|
+
|
514
560
|
def test_flask_auth_middleware_authz_error_response(self):
|
515
561
|
"""Test _authz_error_response method."""
|
516
562
|
from mcp_security_framework.schemas.models import AuthResult
|
517
|
-
|
563
|
+
|
518
564
|
auth_result = AuthResult(
|
519
565
|
is_valid=False,
|
520
566
|
status=AuthStatus.INVALID,
|
521
567
|
error_code=-32004,
|
522
|
-
error_message="Access denied"
|
568
|
+
error_message="Access denied",
|
523
569
|
)
|
524
|
-
|
570
|
+
|
525
571
|
mock_start_response = Mock()
|
526
|
-
|
527
|
-
response = self.middleware._authz_error_response(
|
528
|
-
|
572
|
+
|
573
|
+
response = self.middleware._authz_error_response(
|
574
|
+
auth_result, mock_start_response
|
575
|
+
)
|
576
|
+
|
529
577
|
assert response is not None
|
530
578
|
mock_start_response.assert_called_once()
|
531
|
-
|
579
|
+
|
532
580
|
def test_flask_auth_middleware_validation_error_response(self):
|
533
581
|
"""Test _validation_error_response method."""
|
534
582
|
mock_start_response = Mock()
|
535
|
-
|
536
|
-
response = self.middleware._validation_error_response(
|
537
|
-
|
583
|
+
|
584
|
+
response = self.middleware._validation_error_response(
|
585
|
+
"Validation failed", -32000, mock_start_response
|
586
|
+
)
|
587
|
+
|
538
588
|
assert response is not None
|
539
589
|
mock_start_response.assert_called_once()
|
540
|
-
|
590
|
+
|
541
591
|
def test_flask_auth_middleware_rate_limit_error_response(self):
|
542
592
|
"""Test _rate_limit_error_response method."""
|
543
593
|
mock_start_response = Mock()
|
544
|
-
|
545
|
-
response = self.middleware._rate_limit_error_response(
|
546
|
-
|
594
|
+
|
595
|
+
response = self.middleware._rate_limit_error_response(
|
596
|
+
"Rate limit exceeded", -32000, mock_start_response
|
597
|
+
)
|
598
|
+
|
547
599
|
assert response is not None
|
548
600
|
mock_start_response.assert_called_once()
|
549
|
-
|
601
|
+
|
550
602
|
def test_flask_auth_middleware_security_header_response(self):
|
551
603
|
"""Test _security_header_response method."""
|
552
604
|
mock_start_response = Mock()
|
553
|
-
|
554
|
-
response = self.middleware._security_header_response(
|
555
|
-
|
605
|
+
|
606
|
+
response = self.middleware._security_header_response(
|
607
|
+
"Security header validation failed", -32000, mock_start_response
|
608
|
+
)
|
609
|
+
|
556
610
|
assert response is not None
|
557
611
|
mock_start_response.assert_called_once()
|
558
|
-
|
612
|
+
|
559
613
|
def test_flask_auth_middleware_log_auth_event(self):
|
560
614
|
"""Test _log_auth_event method."""
|
561
615
|
event_data = {
|
@@ -563,76 +617,80 @@ class TestFlaskAuthMiddleware:
|
|
563
617
|
"username": "testuser",
|
564
618
|
"path": "/api/v1/users/me",
|
565
619
|
"method": "GET",
|
566
|
-
"auth_method": "api_key"
|
620
|
+
"auth_method": "api_key",
|
567
621
|
}
|
568
|
-
|
622
|
+
|
569
623
|
self.middleware._log_auth_event("authentication_successful", event_data)
|
570
|
-
|
624
|
+
|
571
625
|
self.mock_logger.info.assert_called_once()
|
572
|
-
|
626
|
+
|
573
627
|
def test_flask_auth_middleware_get_rate_limit_identifier(self):
|
574
628
|
"""Test _get_rate_limit_identifier method."""
|
575
629
|
mock_request = Mock(spec=Request)
|
576
630
|
mock_request.remote_addr = "192.168.1.1"
|
577
631
|
mock_request.headers = {}
|
578
|
-
|
632
|
+
|
579
633
|
identifier = self.middleware._get_rate_limit_identifier(mock_request)
|
580
634
|
assert identifier == "192.168.1.1"
|
581
|
-
|
635
|
+
|
582
636
|
def test_flask_auth_middleware_get_request_path(self):
|
583
637
|
"""Test _get_request_path method."""
|
584
638
|
mock_request = Mock(spec=Request)
|
585
639
|
mock_request.path = "/api/v1/users/me"
|
586
|
-
|
640
|
+
|
587
641
|
path = self.middleware._get_request_path(mock_request)
|
588
642
|
assert path == "/api/v1/users/me"
|
589
|
-
|
643
|
+
|
590
644
|
def test_flask_auth_middleware_get_required_permissions(self):
|
591
645
|
"""Test _get_required_permissions method."""
|
592
646
|
mock_request = Mock(spec=Request)
|
593
647
|
mock_request.path = "/api/v1/users/me"
|
594
648
|
mock_request.method = "GET"
|
595
|
-
|
649
|
+
|
596
650
|
permissions = self.middleware._get_required_permissions(mock_request)
|
597
651
|
assert isinstance(permissions, list)
|
598
|
-
|
652
|
+
|
599
653
|
def test_flask_auth_middleware_authenticate_only_success(self):
|
600
654
|
"""Test _authenticate_only with successful authentication."""
|
601
655
|
mock_request = Mock(spec=Request)
|
602
656
|
mock_request.headers = {"X-API-Key": "test_key_123"}
|
603
657
|
mock_request.remote_addr = "192.168.1.1"
|
604
|
-
|
658
|
+
|
605
659
|
auth_result = AuthResult(
|
606
660
|
is_valid=True,
|
607
661
|
status=AuthStatus.SUCCESS,
|
608
662
|
username="testuser",
|
609
663
|
roles=["user"],
|
610
|
-
auth_method=AuthMethod.API_KEY
|
664
|
+
auth_method=AuthMethod.API_KEY,
|
665
|
+
)
|
666
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
667
|
+
return_value=auth_result
|
611
668
|
)
|
612
|
-
|
613
|
-
|
669
|
+
|
614
670
|
result = self.middleware._authenticate_only(mock_request)
|
615
|
-
|
671
|
+
|
616
672
|
assert result.is_valid is True
|
617
673
|
assert result.username == "testuser"
|
618
|
-
|
674
|
+
|
619
675
|
def test_flask_auth_middleware_authenticate_only_failure(self):
|
620
676
|
"""Test _authenticate_only with authentication failure."""
|
621
677
|
mock_request = Mock(spec=Request)
|
622
678
|
mock_request.headers = {}
|
623
679
|
mock_request.remote_addr = "192.168.1.1"
|
624
|
-
|
680
|
+
|
625
681
|
# Mock failed authentication for all methods
|
626
682
|
failed_auth_result = AuthResult(
|
627
683
|
is_valid=False,
|
628
684
|
status=AuthStatus.INVALID,
|
629
685
|
auth_method=AuthMethod.UNKNOWN,
|
630
686
|
error_code=-32001,
|
631
|
-
error_message="No authentication credentials provided"
|
687
|
+
error_message="No authentication credentials provided",
|
688
|
+
)
|
689
|
+
self.security_manager.auth_manager.authenticate_api_key = Mock(
|
690
|
+
return_value=failed_auth_result
|
632
691
|
)
|
633
|
-
|
634
|
-
|
692
|
+
|
635
693
|
result = self.middleware._authenticate_only(mock_request)
|
636
|
-
|
694
|
+
|
637
695
|
assert result.is_valid is False
|
638
696
|
assert result.error_code == -32033 # All authentication methods failed
|