mcp-security-framework 1.1.0__py3-none-any.whl → 1.1.2__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.2.dist-info}/METADATA +4 -3
- mcp_security_framework-1.1.2.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.2.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/top_level.txt +0 -0
@@ -19,76 +19,87 @@ Version: 1.0.0
|
|
19
19
|
License: MIT
|
20
20
|
"""
|
21
21
|
|
22
|
+
from typing import Any, Dict, List
|
23
|
+
from unittest.mock import MagicMock, Mock, patch
|
24
|
+
|
22
25
|
import pytest
|
23
|
-
from unittest.mock import Mock, patch, MagicMock
|
24
|
-
from typing import Dict, List, Any
|
25
26
|
|
27
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
26
28
|
from mcp_security_framework.middleware.security_middleware import (
|
27
29
|
SecurityMiddleware,
|
28
|
-
SecurityMiddlewareError
|
30
|
+
SecurityMiddlewareError,
|
31
|
+
)
|
32
|
+
from mcp_security_framework.schemas.config import (
|
33
|
+
AuthConfig,
|
34
|
+
RateLimitConfig,
|
35
|
+
SecurityConfig,
|
36
|
+
)
|
37
|
+
from mcp_security_framework.schemas.models import (
|
38
|
+
AuthMethod,
|
39
|
+
AuthResult,
|
40
|
+
AuthStatus,
|
41
|
+
ValidationResult,
|
42
|
+
ValidationStatus,
|
29
43
|
)
|
30
|
-
from mcp_security_framework.core.security_manager import SecurityManager
|
31
|
-
from mcp_security_framework.schemas.config import SecurityConfig, AuthConfig, RateLimitConfig
|
32
|
-
from mcp_security_framework.schemas.models import AuthResult, ValidationResult, ValidationStatus, AuthStatus, AuthMethod
|
33
44
|
|
34
45
|
|
35
46
|
class MockSecurityMiddleware(SecurityMiddleware):
|
36
47
|
"""
|
37
48
|
Mock implementation of SecurityMiddleware for testing.
|
38
|
-
|
49
|
+
|
39
50
|
This class implements all abstract methods to allow testing
|
40
51
|
of the base SecurityMiddleware functionality.
|
41
52
|
"""
|
42
|
-
|
53
|
+
|
43
54
|
def __call__(self, request: Any, call_next: Any) -> Any:
|
44
55
|
"""Mock implementation of __call__ method."""
|
45
56
|
# Check rate limit
|
46
57
|
if not self._check_rate_limit(request):
|
47
58
|
return self._create_error_response(429, "Rate limit exceeded")
|
48
|
-
|
59
|
+
|
49
60
|
# Check if public path
|
50
61
|
if self._is_public_path(request):
|
51
62
|
response = call_next(request)
|
52
63
|
self._add_security_headers(response)
|
53
64
|
return response
|
54
|
-
|
65
|
+
|
55
66
|
# Authenticate request
|
56
67
|
auth_result = self._authenticate_request(request)
|
57
68
|
if not auth_result.is_valid:
|
58
69
|
return self._create_error_response(401, "Authentication failed")
|
59
|
-
|
70
|
+
|
60
71
|
# Validate permissions
|
61
72
|
if not self._validate_permissions(request, auth_result):
|
62
73
|
return self._create_error_response(403, "Permission denied")
|
63
|
-
|
74
|
+
|
64
75
|
# Process request
|
65
76
|
response = call_next(request)
|
66
77
|
self._add_security_headers(response)
|
67
78
|
return response
|
68
|
-
|
79
|
+
|
69
80
|
def _get_rate_limit_identifier(self, request: Any) -> str:
|
70
81
|
"""Mock implementation to get rate limit identifier."""
|
71
|
-
return getattr(request,
|
72
|
-
|
82
|
+
return getattr(request, "client_ip", "127.0.0.1")
|
83
|
+
|
73
84
|
def _get_request_path(self, request: Any) -> str:
|
74
85
|
"""Mock implementation to get request path."""
|
75
|
-
return getattr(request,
|
76
|
-
|
86
|
+
return getattr(request, "path", "/")
|
87
|
+
|
77
88
|
def _get_required_permissions(self, request: Any) -> List[str]:
|
78
89
|
"""Mock implementation to get required permissions."""
|
79
|
-
return getattr(request,
|
80
|
-
|
90
|
+
return getattr(request, "required_permissions", [])
|
91
|
+
|
81
92
|
def _try_auth_method(self, request: Any, method: str) -> AuthResult:
|
82
93
|
"""Mock implementation to try authentication method."""
|
83
94
|
if method == "api_key":
|
84
|
-
api_key = getattr(request,
|
95
|
+
api_key = getattr(request, "api_key", None)
|
85
96
|
if api_key == "valid_key":
|
86
97
|
return AuthResult(
|
87
98
|
is_valid=True,
|
88
99
|
status=AuthStatus.SUCCESS,
|
89
100
|
username="test_user",
|
90
101
|
roles=["user"],
|
91
|
-
auth_method=AuthMethod.API_KEY
|
102
|
+
auth_method=AuthMethod.API_KEY,
|
92
103
|
)
|
93
104
|
return AuthResult(
|
94
105
|
is_valid=False,
|
@@ -97,14 +108,14 @@ class MockSecurityMiddleware(SecurityMiddleware):
|
|
97
108
|
roles=[],
|
98
109
|
auth_method=AuthMethod.API_KEY if method == "api_key" else None,
|
99
110
|
error_code=-32005,
|
100
|
-
error_message="Authentication failed"
|
111
|
+
error_message="Authentication failed",
|
101
112
|
)
|
102
|
-
|
113
|
+
|
103
114
|
def _apply_security_headers(self, response: Any, headers: Dict[str, str]) -> None:
|
104
115
|
"""Mock implementation to apply security headers."""
|
105
|
-
if hasattr(response,
|
116
|
+
if hasattr(response, "headers"):
|
106
117
|
response.headers.update(headers)
|
107
|
-
|
118
|
+
|
108
119
|
def _create_error_response(self, status_code: int, message: str) -> Any:
|
109
120
|
"""Mock implementation to create error response."""
|
110
121
|
response = Mock()
|
@@ -115,7 +126,7 @@ class MockSecurityMiddleware(SecurityMiddleware):
|
|
115
126
|
|
116
127
|
class TestSecurityMiddleware:
|
117
128
|
"""Test suite for SecurityMiddleware class."""
|
118
|
-
|
129
|
+
|
119
130
|
def setup_method(self):
|
120
131
|
"""Set up test fixtures before each test method."""
|
121
132
|
# Create mock security manager
|
@@ -125,27 +136,25 @@ class TestSecurityMiddleware:
|
|
125
136
|
enabled=True,
|
126
137
|
methods=["api_key", "jwt"],
|
127
138
|
public_paths=["/health", "/docs"],
|
128
|
-
jwt_secret="test_jwt_secret_key"
|
139
|
+
jwt_secret="test_jwt_secret_key",
|
129
140
|
),
|
130
141
|
rate_limit=RateLimitConfig(
|
131
|
-
enabled=True,
|
132
|
-
|
133
|
-
window_size_seconds=60
|
134
|
-
)
|
142
|
+
enabled=True, default_requests_per_minute=100, window_size_seconds=60
|
143
|
+
),
|
135
144
|
)
|
136
|
-
|
145
|
+
|
137
146
|
# Setup rate_limiter mock
|
138
147
|
self.mock_security_manager.rate_limiter = Mock()
|
139
|
-
|
148
|
+
|
140
149
|
# Create mock request
|
141
150
|
self.mock_request = Mock()
|
142
151
|
self.mock_request.client_ip = "127.0.0.1"
|
143
152
|
self.mock_request.path = "/api/test"
|
144
153
|
self.mock_request.required_permissions = ["read"]
|
145
|
-
|
154
|
+
|
146
155
|
# Create middleware instance
|
147
156
|
self.middleware = MockSecurityMiddleware(self.mock_security_manager)
|
148
|
-
|
157
|
+
|
149
158
|
def test_initialization_success(self):
|
150
159
|
"""Test successful middleware initialization."""
|
151
160
|
assert self.middleware.security_manager == self.mock_security_manager
|
@@ -153,91 +162,97 @@ class TestSecurityMiddleware:
|
|
153
162
|
assert len(self.middleware._public_paths) == 2
|
154
163
|
assert "/health" in self.middleware._public_paths
|
155
164
|
assert "/docs" in self.middleware._public_paths
|
156
|
-
|
165
|
+
|
157
166
|
def test_initialization_invalid_security_manager(self):
|
158
167
|
"""Test initialization with invalid security manager."""
|
159
168
|
with pytest.raises(SecurityMiddlewareError) as exc_info:
|
160
169
|
MockSecurityMiddleware("invalid_manager")
|
161
|
-
|
170
|
+
|
162
171
|
assert "Invalid security manager" in str(exc_info.value)
|
163
172
|
assert exc_info.value.error_code == -32003
|
164
|
-
|
173
|
+
|
165
174
|
def test_check_rate_limit_disabled(self):
|
166
175
|
"""Test rate limiting when disabled."""
|
167
176
|
self.mock_security_manager.config.rate_limit.enabled = False
|
168
177
|
result = self.middleware._check_rate_limit(self.mock_request)
|
169
178
|
assert result is True
|
170
|
-
|
179
|
+
|
171
180
|
def test_check_rate_limit_enabled_success(self):
|
172
181
|
"""Test successful rate limit check."""
|
173
182
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
174
183
|
result = self.middleware._check_rate_limit(self.mock_request)
|
175
184
|
assert result is True
|
176
|
-
self.mock_security_manager.rate_limiter.check_rate_limit.assert_called_once_with(
|
177
|
-
|
185
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.assert_called_once_with(
|
186
|
+
"127.0.0.1"
|
187
|
+
)
|
188
|
+
|
178
189
|
def test_check_rate_limit_enabled_exceeded(self):
|
179
190
|
"""Test rate limit exceeded."""
|
180
191
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = False
|
181
192
|
result = self.middleware._check_rate_limit(self.mock_request)
|
182
193
|
assert result is False
|
183
|
-
|
194
|
+
|
184
195
|
def test_check_rate_limit_no_identifier(self):
|
185
196
|
"""Test rate limiting with no identifier."""
|
186
197
|
self.mock_request.client_ip = None
|
187
198
|
result = self.middleware._check_rate_limit(self.mock_request)
|
188
199
|
assert result is True
|
189
|
-
|
200
|
+
|
190
201
|
def test_check_rate_limit_exception(self):
|
191
202
|
"""Test rate limiting with exception."""
|
192
|
-
self.mock_security_manager.rate_limiter.check_rate_limit.side_effect =
|
193
|
-
|
203
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.side_effect = (
|
204
|
+
Exception("Rate limit error")
|
205
|
+
)
|
206
|
+
|
194
207
|
with pytest.raises(SecurityMiddlewareError) as exc_info:
|
195
208
|
self.middleware._check_rate_limit(self.mock_request)
|
196
|
-
|
209
|
+
|
197
210
|
assert "Rate limit check failed" in str(exc_info.value)
|
198
211
|
assert exc_info.value.error_code == -32004
|
199
|
-
|
212
|
+
|
200
213
|
def test_authenticate_request_disabled(self):
|
201
214
|
"""Test authentication when disabled."""
|
202
215
|
self.mock_security_manager.config.auth.enabled = False
|
203
216
|
result = self.middleware._authenticate_request(self.mock_request)
|
204
|
-
|
217
|
+
|
205
218
|
assert result.is_valid is True
|
206
219
|
assert result.status == AuthStatus.SUCCESS
|
207
220
|
assert result.username == "anonymous"
|
208
221
|
assert result.auth_method is None
|
209
|
-
|
222
|
+
|
210
223
|
def test_authenticate_request_success(self):
|
211
224
|
"""Test successful authentication."""
|
212
225
|
self.mock_request.api_key = "valid_key"
|
213
226
|
result = self.middleware._authenticate_request(self.mock_request)
|
214
|
-
|
227
|
+
|
215
228
|
assert result.is_valid is True
|
216
229
|
assert result.username == "test_user"
|
217
230
|
assert result.auth_method == "api_key"
|
218
231
|
assert result.roles == ["user"]
|
219
|
-
|
232
|
+
|
220
233
|
def test_authenticate_request_failure(self):
|
221
234
|
"""Test authentication failure."""
|
222
235
|
self.mock_request.api_key = "invalid_key"
|
223
236
|
result = self.middleware._authenticate_request(self.mock_request)
|
224
|
-
|
237
|
+
|
225
238
|
assert result.is_valid is False
|
226
239
|
assert result.error_code == -32005
|
227
240
|
assert "All authentication methods failed" in result.error_message
|
228
|
-
|
241
|
+
|
229
242
|
def test_authenticate_request_exception(self):
|
230
243
|
"""Test authentication with exception."""
|
231
244
|
# Mock security manager to raise exception
|
232
|
-
self.mock_security_manager.authenticate_user.side_effect = Exception(
|
233
|
-
|
245
|
+
self.mock_security_manager.authenticate_user.side_effect = Exception(
|
246
|
+
"Authentication error"
|
247
|
+
)
|
248
|
+
|
234
249
|
result = self.middleware._authenticate_request(self.mock_request)
|
235
|
-
|
250
|
+
|
236
251
|
# Should handle exception gracefully
|
237
252
|
assert result.is_valid is False
|
238
253
|
assert result.error_code == -32005
|
239
254
|
assert "All authentication methods failed" in result.error_message
|
240
|
-
|
255
|
+
|
241
256
|
def test_validate_permissions_success(self):
|
242
257
|
"""Test successful permission validation."""
|
243
258
|
auth_result = AuthResult(
|
@@ -245,17 +260,16 @@ class TestSecurityMiddleware:
|
|
245
260
|
status=AuthStatus.SUCCESS,
|
246
261
|
username="test_user",
|
247
262
|
roles=["admin"],
|
248
|
-
auth_method=AuthMethod.API_KEY
|
263
|
+
auth_method=AuthMethod.API_KEY,
|
249
264
|
)
|
250
|
-
|
265
|
+
|
251
266
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
252
|
-
is_valid=True,
|
253
|
-
status=ValidationStatus.VALID
|
267
|
+
is_valid=True, status=ValidationStatus.VALID
|
254
268
|
)
|
255
|
-
|
269
|
+
|
256
270
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
257
271
|
assert result is True
|
258
|
-
|
272
|
+
|
259
273
|
def test_validate_permissions_failure(self):
|
260
274
|
"""Test permission validation failure."""
|
261
275
|
auth_result = AuthResult(
|
@@ -263,19 +277,19 @@ class TestSecurityMiddleware:
|
|
263
277
|
status=AuthStatus.SUCCESS,
|
264
278
|
username="test_user",
|
265
279
|
roles=["user"],
|
266
|
-
auth_method=AuthMethod.API_KEY
|
280
|
+
auth_method=AuthMethod.API_KEY,
|
267
281
|
)
|
268
|
-
|
282
|
+
|
269
283
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
270
284
|
is_valid=False,
|
271
285
|
status=ValidationStatus.INVALID,
|
272
286
|
error_code=-32007,
|
273
|
-
error_message="Insufficient permissions"
|
287
|
+
error_message="Insufficient permissions",
|
274
288
|
)
|
275
|
-
|
289
|
+
|
276
290
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
277
291
|
assert result is False
|
278
|
-
|
292
|
+
|
279
293
|
def test_validate_permissions_no_auth(self):
|
280
294
|
"""Test permission validation with invalid auth."""
|
281
295
|
auth_result = AuthResult(
|
@@ -285,12 +299,12 @@ class TestSecurityMiddleware:
|
|
285
299
|
roles=[],
|
286
300
|
auth_method=None,
|
287
301
|
error_code=-32005,
|
288
|
-
error_message="Authentication failed"
|
302
|
+
error_message="Authentication failed",
|
289
303
|
)
|
290
|
-
|
304
|
+
|
291
305
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
292
306
|
assert result is False
|
293
|
-
|
307
|
+
|
294
308
|
def test_validate_permissions_no_required_permissions(self):
|
295
309
|
"""Test permission validation with no required permissions."""
|
296
310
|
auth_result = AuthResult(
|
@@ -298,13 +312,13 @@ class TestSecurityMiddleware:
|
|
298
312
|
status=AuthStatus.SUCCESS,
|
299
313
|
username="test_user",
|
300
314
|
roles=["user"],
|
301
|
-
auth_method=AuthMethod.API_KEY
|
315
|
+
auth_method=AuthMethod.API_KEY,
|
302
316
|
)
|
303
|
-
|
317
|
+
|
304
318
|
self.mock_request.required_permissions = []
|
305
319
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
306
320
|
assert result is True
|
307
|
-
|
321
|
+
|
308
322
|
def test_validate_permissions_exception(self):
|
309
323
|
"""Test permission validation with exception."""
|
310
324
|
auth_result = AuthResult(
|
@@ -312,75 +326,77 @@ class TestSecurityMiddleware:
|
|
312
326
|
status=AuthStatus.SUCCESS,
|
313
327
|
username="test_user",
|
314
328
|
roles=["user"],
|
315
|
-
auth_method=AuthMethod.API_KEY
|
329
|
+
auth_method=AuthMethod.API_KEY,
|
316
330
|
)
|
317
|
-
|
318
|
-
self.mock_security_manager.check_permissions.side_effect = Exception(
|
319
|
-
|
331
|
+
|
332
|
+
self.mock_security_manager.check_permissions.side_effect = Exception(
|
333
|
+
"Permission error"
|
334
|
+
)
|
335
|
+
|
320
336
|
with pytest.raises(SecurityMiddlewareError) as exc_info:
|
321
337
|
self.middleware._validate_permissions(self.mock_request, auth_result)
|
322
|
-
|
338
|
+
|
323
339
|
assert "Permission validation failed" in str(exc_info.value)
|
324
340
|
assert exc_info.value.error_code == -32007
|
325
|
-
|
341
|
+
|
326
342
|
def test_is_public_path_true(self):
|
327
343
|
"""Test public path check returning True."""
|
328
344
|
self.mock_request.path = "/health"
|
329
345
|
result = self.middleware._is_public_path(self.mock_request)
|
330
346
|
assert result is True
|
331
|
-
|
347
|
+
|
332
348
|
def test_is_public_path_false(self):
|
333
349
|
"""Test public path check returning False."""
|
334
350
|
self.mock_request.path = "/api/private"
|
335
351
|
result = self.middleware._is_public_path(self.mock_request)
|
336
352
|
assert result is False
|
337
|
-
|
353
|
+
|
338
354
|
def test_is_public_path_no_path(self):
|
339
355
|
"""Test public path check with no path."""
|
340
356
|
self.mock_request.path = None
|
341
357
|
result = self.middleware._is_public_path(self.mock_request)
|
342
358
|
assert result is False
|
343
|
-
|
359
|
+
|
344
360
|
def test_is_public_path_exception(self):
|
345
361
|
"""Test public path check with exception."""
|
346
362
|
self.mock_request.path = Exception("Path error")
|
347
363
|
result = self.middleware._is_public_path(self.mock_request)
|
348
364
|
assert result is False
|
349
|
-
|
365
|
+
|
350
366
|
def test_add_security_headers_success(self):
|
351
367
|
"""Test adding security headers successfully."""
|
352
368
|
mock_response = Mock()
|
353
369
|
mock_response.headers = {}
|
354
|
-
|
370
|
+
|
355
371
|
self.middleware._add_security_headers(mock_response)
|
356
|
-
|
372
|
+
|
357
373
|
# Check that standard security headers were added
|
358
374
|
assert "X-Content-Type-Options" in mock_response.headers
|
359
375
|
assert "X-Frame-Options" in mock_response.headers
|
360
376
|
assert "X-XSS-Protection" in mock_response.headers
|
361
|
-
|
377
|
+
|
362
378
|
def test_add_security_headers_with_custom_headers(self):
|
363
379
|
"""Test adding security headers with custom headers from config."""
|
364
380
|
self.mock_security_manager.config.auth.security_headers = {
|
365
381
|
"Custom-Security-Header": "custom_value"
|
366
382
|
}
|
367
|
-
|
383
|
+
|
368
384
|
mock_response = Mock()
|
369
385
|
mock_response.headers = {}
|
370
|
-
|
386
|
+
|
371
387
|
self.middleware._add_security_headers(mock_response)
|
372
|
-
|
388
|
+
|
373
389
|
assert "Custom-Security-Header" in mock_response.headers
|
374
390
|
assert mock_response.headers["Custom-Security-Header"] == "custom_value"
|
375
|
-
|
391
|
+
|
376
392
|
def test_add_security_headers_exception(self):
|
377
393
|
"""Test adding security headers with exception."""
|
378
394
|
mock_response = Mock()
|
379
395
|
mock_response.headers = Exception("Header error")
|
380
|
-
|
396
|
+
|
381
397
|
# Should not raise exception, just log error
|
382
398
|
self.middleware._add_security_headers(mock_response)
|
383
|
-
|
399
|
+
|
384
400
|
def test_log_security_event_success(self):
|
385
401
|
"""Test logging security event successfully."""
|
386
402
|
details = {
|
@@ -388,112 +404,111 @@ class TestSecurityMiddleware:
|
|
388
404
|
"ip_address": "127.0.0.1",
|
389
405
|
"username": "test_user",
|
390
406
|
"path": "/api/test",
|
391
|
-
"method": "GET"
|
407
|
+
"method": "GET",
|
392
408
|
}
|
393
|
-
|
394
|
-
with patch.object(self.middleware.logger,
|
409
|
+
|
410
|
+
with patch.object(self.middleware.logger, "info") as mock_logger:
|
395
411
|
self.middleware._log_security_event("test_event", details)
|
396
412
|
mock_logger.assert_called_once()
|
397
|
-
|
413
|
+
|
398
414
|
def test_log_security_event_exception(self):
|
399
415
|
"""Test logging security event with exception."""
|
400
416
|
details = Exception("Event error")
|
401
|
-
|
417
|
+
|
402
418
|
# Should not raise exception, just log error
|
403
419
|
self.middleware._log_security_event("test_event", details)
|
404
|
-
|
420
|
+
|
405
421
|
def test_middleware_call_success(self):
|
406
422
|
"""Test successful middleware call."""
|
407
423
|
mock_call_next = Mock()
|
408
424
|
mock_response = Mock()
|
409
425
|
mock_response.headers = {}
|
410
426
|
mock_call_next.return_value = mock_response
|
411
|
-
|
427
|
+
|
412
428
|
self.mock_request.api_key = "valid_key"
|
413
429
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
414
430
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
415
|
-
is_valid=True,
|
416
|
-
status=ValidationStatus.VALID
|
431
|
+
is_valid=True, status=ValidationStatus.VALID
|
417
432
|
)
|
418
|
-
|
433
|
+
|
419
434
|
result = self.middleware(self.mock_request, mock_call_next)
|
420
|
-
|
435
|
+
|
421
436
|
assert result == mock_response
|
422
437
|
mock_call_next.assert_called_once_with(self.mock_request)
|
423
|
-
|
438
|
+
|
424
439
|
def test_middleware_call_rate_limit_exceeded(self):
|
425
440
|
"""Test middleware call with rate limit exceeded."""
|
426
441
|
mock_call_next = Mock()
|
427
|
-
|
442
|
+
|
428
443
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = False
|
429
|
-
|
444
|
+
|
430
445
|
result = self.middleware(self.mock_request, mock_call_next)
|
431
|
-
|
446
|
+
|
432
447
|
assert result.status_code == 429
|
433
448
|
assert "Rate limit exceeded" in result.body
|
434
449
|
mock_call_next.assert_not_called()
|
435
|
-
|
450
|
+
|
436
451
|
def test_middleware_call_public_path(self):
|
437
452
|
"""Test middleware call with public path."""
|
438
453
|
mock_call_next = Mock()
|
439
454
|
mock_response = Mock()
|
440
455
|
mock_response.headers = {}
|
441
456
|
mock_call_next.return_value = mock_response
|
442
|
-
|
457
|
+
|
443
458
|
self.mock_request.path = "/health"
|
444
459
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
445
|
-
|
460
|
+
|
446
461
|
result = self.middleware(self.mock_request, mock_call_next)
|
447
|
-
|
462
|
+
|
448
463
|
assert result == mock_response
|
449
464
|
mock_call_next.assert_called_once_with(self.mock_request)
|
450
|
-
|
465
|
+
|
451
466
|
def test_middleware_call_authentication_failed(self):
|
452
467
|
"""Test middleware call with authentication failure."""
|
453
468
|
mock_call_next = Mock()
|
454
|
-
|
469
|
+
|
455
470
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
456
471
|
self.mock_request.api_key = "invalid_key"
|
457
|
-
|
472
|
+
|
458
473
|
result = self.middleware(self.mock_request, mock_call_next)
|
459
|
-
|
474
|
+
|
460
475
|
assert result.status_code == 401
|
461
476
|
assert "Authentication failed" in result.body
|
462
477
|
mock_call_next.assert_not_called()
|
463
|
-
|
478
|
+
|
464
479
|
def test_middleware_call_permission_denied(self):
|
465
480
|
"""Test middleware call with permission denied."""
|
466
481
|
mock_call_next = Mock()
|
467
|
-
|
482
|
+
|
468
483
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
469
484
|
self.mock_request.api_key = "valid_key"
|
470
485
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
471
486
|
is_valid=False,
|
472
487
|
status=ValidationStatus.INVALID,
|
473
488
|
error_code=-32007,
|
474
|
-
error_message="Permission denied"
|
489
|
+
error_message="Permission denied",
|
475
490
|
)
|
476
|
-
|
491
|
+
|
477
492
|
result = self.middleware(self.mock_request, mock_call_next)
|
478
|
-
|
493
|
+
|
479
494
|
assert result.status_code == 403
|
480
495
|
assert "Permission denied" in result.body
|
481
496
|
mock_call_next.assert_not_called()
|
482
|
-
|
497
|
+
|
483
498
|
def test_abstract_methods_validation(self):
|
484
499
|
"""Test that abstract methods are properly defined."""
|
485
500
|
# Verify that all abstract methods are defined in the base class
|
486
501
|
abstract_methods = [
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
502
|
+
"_get_rate_limit_identifier",
|
503
|
+
"_get_request_path",
|
504
|
+
"_get_required_permissions",
|
505
|
+
"_try_auth_method",
|
506
|
+
"_apply_security_headers",
|
507
|
+
"_create_error_response",
|
493
508
|
]
|
494
|
-
|
509
|
+
|
495
510
|
for method_name in abstract_methods:
|
496
511
|
assert hasattr(SecurityMiddleware, method_name)
|
497
512
|
method = getattr(SecurityMiddleware, method_name)
|
498
|
-
assert hasattr(method,
|
513
|
+
assert hasattr(method, "__isabstractmethod__")
|
499
514
|
assert method.__isabstractmethod__ is True
|