mcp-security-framework 0.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 +49 -20
- mcp_security_framework/core/cert_manager.py +398 -104
- mcp_security_framework/core/permission_manager.py +13 -9
- mcp_security_framework/core/rate_limiter.py +10 -0
- mcp_security_framework/core/security_manager.py +286 -229
- mcp_security_framework/examples/__init__.py +6 -0
- mcp_security_framework/examples/comprehensive_example.py +954 -0
- mcp_security_framework/examples/django_example.py +276 -202
- mcp_security_framework/examples/fastapi_example.py +897 -393
- mcp_security_framework/examples/flask_example.py +311 -200
- mcp_security_framework/examples/gateway_example.py +373 -214
- mcp_security_framework/examples/microservice_example.py +337 -172
- mcp_security_framework/examples/standalone_example.py +719 -478
- mcp_security_framework/examples/test_all_examples.py +572 -0
- 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 +179 -110
- mcp_security_framework/middleware/fastapi_middleware.py +156 -148
- mcp_security_framework/middleware/flask_auth_middleware.py +267 -107
- 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 +19 -6
- mcp_security_framework/utils/cert_utils.py +14 -8
- mcp_security_framework/utils/datetime_compat.py +116 -0
- {mcp_security_framework-0.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 +303 -0
- tests/test_cli/test_cert_cli.py +194 -174
- tests/test_cli/test_security_cli.py +274 -247
- tests/test_core/test_cert_manager.py +33 -19
- tests/test_core/test_security_manager.py +2 -2
- tests/test_examples/test_comprehensive_example.py +613 -0
- tests/test_examples/test_fastapi_example.py +290 -169
- tests/test_examples/test_flask_example.py +304 -162
- tests/test_examples/test_standalone_example.py +106 -168
- tests/test_integration/test_auth_flow.py +214 -198
- tests/test_integration/test_certificate_flow.py +181 -150
- tests/test_integration/test_fastapi_integration.py +140 -149
- tests/test_integration/test_flask_integration.py +144 -141
- tests/test_integration/test_standalone_integration.py +331 -300
- tests/test_middleware/test_fastapi_auth_middleware.py +745 -0
- tests/test_middleware/test_fastapi_middleware.py +147 -132
- tests/test_middleware/test_flask_auth_middleware.py +696 -0
- tests/test_middleware/test_flask_middleware.py +201 -179
- tests/test_middleware/test_security_middleware.py +151 -130
- tests/test_utils/test_datetime_compat.py +147 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +0 -76
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/WHEEL +0 -0
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.1.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,85 +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
|
-
#
|
232
|
-
|
233
|
-
|
234
|
-
|
244
|
+
# Mock security manager to raise exception
|
245
|
+
self.mock_security_manager.authenticate_user.side_effect = Exception(
|
246
|
+
"Authentication error"
|
247
|
+
)
|
248
|
+
|
249
|
+
result = self.middleware._authenticate_request(self.mock_request)
|
250
|
+
|
251
|
+
# Should handle exception gracefully
|
252
|
+
assert result.is_valid is False
|
253
|
+
assert result.error_code == -32005
|
254
|
+
assert "All authentication methods failed" in result.error_message
|
255
|
+
|
235
256
|
def test_validate_permissions_success(self):
|
236
257
|
"""Test successful permission validation."""
|
237
258
|
auth_result = AuthResult(
|
@@ -239,17 +260,16 @@ class TestSecurityMiddleware:
|
|
239
260
|
status=AuthStatus.SUCCESS,
|
240
261
|
username="test_user",
|
241
262
|
roles=["admin"],
|
242
|
-
auth_method=AuthMethod.API_KEY
|
263
|
+
auth_method=AuthMethod.API_KEY,
|
243
264
|
)
|
244
|
-
|
265
|
+
|
245
266
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
246
|
-
is_valid=True,
|
247
|
-
status=ValidationStatus.VALID
|
267
|
+
is_valid=True, status=ValidationStatus.VALID
|
248
268
|
)
|
249
|
-
|
269
|
+
|
250
270
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
251
271
|
assert result is True
|
252
|
-
|
272
|
+
|
253
273
|
def test_validate_permissions_failure(self):
|
254
274
|
"""Test permission validation failure."""
|
255
275
|
auth_result = AuthResult(
|
@@ -257,19 +277,19 @@ class TestSecurityMiddleware:
|
|
257
277
|
status=AuthStatus.SUCCESS,
|
258
278
|
username="test_user",
|
259
279
|
roles=["user"],
|
260
|
-
auth_method=AuthMethod.API_KEY
|
280
|
+
auth_method=AuthMethod.API_KEY,
|
261
281
|
)
|
262
|
-
|
282
|
+
|
263
283
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
264
284
|
is_valid=False,
|
265
285
|
status=ValidationStatus.INVALID,
|
266
286
|
error_code=-32007,
|
267
|
-
error_message="Insufficient permissions"
|
287
|
+
error_message="Insufficient permissions",
|
268
288
|
)
|
269
|
-
|
289
|
+
|
270
290
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
271
291
|
assert result is False
|
272
|
-
|
292
|
+
|
273
293
|
def test_validate_permissions_no_auth(self):
|
274
294
|
"""Test permission validation with invalid auth."""
|
275
295
|
auth_result = AuthResult(
|
@@ -279,12 +299,12 @@ class TestSecurityMiddleware:
|
|
279
299
|
roles=[],
|
280
300
|
auth_method=None,
|
281
301
|
error_code=-32005,
|
282
|
-
error_message="Authentication failed"
|
302
|
+
error_message="Authentication failed",
|
283
303
|
)
|
284
|
-
|
304
|
+
|
285
305
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
286
306
|
assert result is False
|
287
|
-
|
307
|
+
|
288
308
|
def test_validate_permissions_no_required_permissions(self):
|
289
309
|
"""Test permission validation with no required permissions."""
|
290
310
|
auth_result = AuthResult(
|
@@ -292,13 +312,13 @@ class TestSecurityMiddleware:
|
|
292
312
|
status=AuthStatus.SUCCESS,
|
293
313
|
username="test_user",
|
294
314
|
roles=["user"],
|
295
|
-
auth_method=AuthMethod.API_KEY
|
315
|
+
auth_method=AuthMethod.API_KEY,
|
296
316
|
)
|
297
|
-
|
317
|
+
|
298
318
|
self.mock_request.required_permissions = []
|
299
319
|
result = self.middleware._validate_permissions(self.mock_request, auth_result)
|
300
320
|
assert result is True
|
301
|
-
|
321
|
+
|
302
322
|
def test_validate_permissions_exception(self):
|
303
323
|
"""Test permission validation with exception."""
|
304
324
|
auth_result = AuthResult(
|
@@ -306,75 +326,77 @@ class TestSecurityMiddleware:
|
|
306
326
|
status=AuthStatus.SUCCESS,
|
307
327
|
username="test_user",
|
308
328
|
roles=["user"],
|
309
|
-
auth_method=AuthMethod.API_KEY
|
329
|
+
auth_method=AuthMethod.API_KEY,
|
330
|
+
)
|
331
|
+
|
332
|
+
self.mock_security_manager.check_permissions.side_effect = Exception(
|
333
|
+
"Permission error"
|
310
334
|
)
|
311
|
-
|
312
|
-
self.mock_security_manager.check_permissions.side_effect = Exception("Permission error")
|
313
|
-
|
335
|
+
|
314
336
|
with pytest.raises(SecurityMiddlewareError) as exc_info:
|
315
337
|
self.middleware._validate_permissions(self.mock_request, auth_result)
|
316
|
-
|
338
|
+
|
317
339
|
assert "Permission validation failed" in str(exc_info.value)
|
318
340
|
assert exc_info.value.error_code == -32007
|
319
|
-
|
341
|
+
|
320
342
|
def test_is_public_path_true(self):
|
321
343
|
"""Test public path check returning True."""
|
322
344
|
self.mock_request.path = "/health"
|
323
345
|
result = self.middleware._is_public_path(self.mock_request)
|
324
346
|
assert result is True
|
325
|
-
|
347
|
+
|
326
348
|
def test_is_public_path_false(self):
|
327
349
|
"""Test public path check returning False."""
|
328
350
|
self.mock_request.path = "/api/private"
|
329
351
|
result = self.middleware._is_public_path(self.mock_request)
|
330
352
|
assert result is False
|
331
|
-
|
353
|
+
|
332
354
|
def test_is_public_path_no_path(self):
|
333
355
|
"""Test public path check with no path."""
|
334
356
|
self.mock_request.path = None
|
335
357
|
result = self.middleware._is_public_path(self.mock_request)
|
336
358
|
assert result is False
|
337
|
-
|
359
|
+
|
338
360
|
def test_is_public_path_exception(self):
|
339
361
|
"""Test public path check with exception."""
|
340
362
|
self.mock_request.path = Exception("Path error")
|
341
363
|
result = self.middleware._is_public_path(self.mock_request)
|
342
364
|
assert result is False
|
343
|
-
|
365
|
+
|
344
366
|
def test_add_security_headers_success(self):
|
345
367
|
"""Test adding security headers successfully."""
|
346
368
|
mock_response = Mock()
|
347
369
|
mock_response.headers = {}
|
348
|
-
|
370
|
+
|
349
371
|
self.middleware._add_security_headers(mock_response)
|
350
|
-
|
372
|
+
|
351
373
|
# Check that standard security headers were added
|
352
374
|
assert "X-Content-Type-Options" in mock_response.headers
|
353
375
|
assert "X-Frame-Options" in mock_response.headers
|
354
376
|
assert "X-XSS-Protection" in mock_response.headers
|
355
|
-
|
377
|
+
|
356
378
|
def test_add_security_headers_with_custom_headers(self):
|
357
379
|
"""Test adding security headers with custom headers from config."""
|
358
380
|
self.mock_security_manager.config.auth.security_headers = {
|
359
381
|
"Custom-Security-Header": "custom_value"
|
360
382
|
}
|
361
|
-
|
383
|
+
|
362
384
|
mock_response = Mock()
|
363
385
|
mock_response.headers = {}
|
364
|
-
|
386
|
+
|
365
387
|
self.middleware._add_security_headers(mock_response)
|
366
|
-
|
388
|
+
|
367
389
|
assert "Custom-Security-Header" in mock_response.headers
|
368
390
|
assert mock_response.headers["Custom-Security-Header"] == "custom_value"
|
369
|
-
|
391
|
+
|
370
392
|
def test_add_security_headers_exception(self):
|
371
393
|
"""Test adding security headers with exception."""
|
372
394
|
mock_response = Mock()
|
373
395
|
mock_response.headers = Exception("Header error")
|
374
|
-
|
396
|
+
|
375
397
|
# Should not raise exception, just log error
|
376
398
|
self.middleware._add_security_headers(mock_response)
|
377
|
-
|
399
|
+
|
378
400
|
def test_log_security_event_success(self):
|
379
401
|
"""Test logging security event successfully."""
|
380
402
|
details = {
|
@@ -382,112 +404,111 @@ class TestSecurityMiddleware:
|
|
382
404
|
"ip_address": "127.0.0.1",
|
383
405
|
"username": "test_user",
|
384
406
|
"path": "/api/test",
|
385
|
-
"method": "GET"
|
407
|
+
"method": "GET",
|
386
408
|
}
|
387
|
-
|
388
|
-
with patch.object(self.middleware.logger,
|
409
|
+
|
410
|
+
with patch.object(self.middleware.logger, "info") as mock_logger:
|
389
411
|
self.middleware._log_security_event("test_event", details)
|
390
412
|
mock_logger.assert_called_once()
|
391
|
-
|
413
|
+
|
392
414
|
def test_log_security_event_exception(self):
|
393
415
|
"""Test logging security event with exception."""
|
394
416
|
details = Exception("Event error")
|
395
|
-
|
417
|
+
|
396
418
|
# Should not raise exception, just log error
|
397
419
|
self.middleware._log_security_event("test_event", details)
|
398
|
-
|
420
|
+
|
399
421
|
def test_middleware_call_success(self):
|
400
422
|
"""Test successful middleware call."""
|
401
423
|
mock_call_next = Mock()
|
402
424
|
mock_response = Mock()
|
403
425
|
mock_response.headers = {}
|
404
426
|
mock_call_next.return_value = mock_response
|
405
|
-
|
427
|
+
|
406
428
|
self.mock_request.api_key = "valid_key"
|
407
429
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
408
430
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
409
|
-
is_valid=True,
|
410
|
-
status=ValidationStatus.VALID
|
431
|
+
is_valid=True, status=ValidationStatus.VALID
|
411
432
|
)
|
412
|
-
|
433
|
+
|
413
434
|
result = self.middleware(self.mock_request, mock_call_next)
|
414
|
-
|
435
|
+
|
415
436
|
assert result == mock_response
|
416
437
|
mock_call_next.assert_called_once_with(self.mock_request)
|
417
|
-
|
438
|
+
|
418
439
|
def test_middleware_call_rate_limit_exceeded(self):
|
419
440
|
"""Test middleware call with rate limit exceeded."""
|
420
441
|
mock_call_next = Mock()
|
421
|
-
|
442
|
+
|
422
443
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = False
|
423
|
-
|
444
|
+
|
424
445
|
result = self.middleware(self.mock_request, mock_call_next)
|
425
|
-
|
446
|
+
|
426
447
|
assert result.status_code == 429
|
427
448
|
assert "Rate limit exceeded" in result.body
|
428
449
|
mock_call_next.assert_not_called()
|
429
|
-
|
450
|
+
|
430
451
|
def test_middleware_call_public_path(self):
|
431
452
|
"""Test middleware call with public path."""
|
432
453
|
mock_call_next = Mock()
|
433
454
|
mock_response = Mock()
|
434
455
|
mock_response.headers = {}
|
435
456
|
mock_call_next.return_value = mock_response
|
436
|
-
|
457
|
+
|
437
458
|
self.mock_request.path = "/health"
|
438
459
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
439
|
-
|
460
|
+
|
440
461
|
result = self.middleware(self.mock_request, mock_call_next)
|
441
|
-
|
462
|
+
|
442
463
|
assert result == mock_response
|
443
464
|
mock_call_next.assert_called_once_with(self.mock_request)
|
444
|
-
|
465
|
+
|
445
466
|
def test_middleware_call_authentication_failed(self):
|
446
467
|
"""Test middleware call with authentication failure."""
|
447
468
|
mock_call_next = Mock()
|
448
|
-
|
469
|
+
|
449
470
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
450
471
|
self.mock_request.api_key = "invalid_key"
|
451
|
-
|
472
|
+
|
452
473
|
result = self.middleware(self.mock_request, mock_call_next)
|
453
|
-
|
474
|
+
|
454
475
|
assert result.status_code == 401
|
455
476
|
assert "Authentication failed" in result.body
|
456
477
|
mock_call_next.assert_not_called()
|
457
|
-
|
478
|
+
|
458
479
|
def test_middleware_call_permission_denied(self):
|
459
480
|
"""Test middleware call with permission denied."""
|
460
481
|
mock_call_next = Mock()
|
461
|
-
|
482
|
+
|
462
483
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
463
484
|
self.mock_request.api_key = "valid_key"
|
464
485
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
465
486
|
is_valid=False,
|
466
487
|
status=ValidationStatus.INVALID,
|
467
488
|
error_code=-32007,
|
468
|
-
error_message="Permission denied"
|
489
|
+
error_message="Permission denied",
|
469
490
|
)
|
470
|
-
|
491
|
+
|
471
492
|
result = self.middleware(self.mock_request, mock_call_next)
|
472
|
-
|
493
|
+
|
473
494
|
assert result.status_code == 403
|
474
495
|
assert "Permission denied" in result.body
|
475
496
|
mock_call_next.assert_not_called()
|
476
|
-
|
497
|
+
|
477
498
|
def test_abstract_methods_validation(self):
|
478
499
|
"""Test that abstract methods are properly defined."""
|
479
500
|
# Verify that all abstract methods are defined in the base class
|
480
501
|
abstract_methods = [
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
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",
|
487
508
|
]
|
488
|
-
|
509
|
+
|
489
510
|
for method_name in abstract_methods:
|
490
511
|
assert hasattr(SecurityMiddleware, method_name)
|
491
512
|
method = getattr(SecurityMiddleware, method_name)
|
492
|
-
assert hasattr(method,
|
513
|
+
assert hasattr(method, "__isabstractmethod__")
|
493
514
|
assert method.__isabstractmethod__ is True
|