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
@@ -19,25 +19,35 @@ Version: 1.0.0
|
|
19
19
|
License: MIT
|
20
20
|
"""
|
21
21
|
|
22
|
-
import pytest
|
23
22
|
import json
|
24
|
-
from
|
25
|
-
from
|
23
|
+
from typing import Any, Dict, List
|
24
|
+
from unittest.mock import MagicMock, Mock, patch
|
26
25
|
|
26
|
+
import pytest
|
27
27
|
from flask import Request, Response
|
28
28
|
|
29
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
29
30
|
from mcp_security_framework.middleware.flask_middleware import (
|
31
|
+
FlaskMiddlewareError,
|
30
32
|
FlaskSecurityMiddleware,
|
31
|
-
FlaskMiddlewareError
|
32
33
|
)
|
33
|
-
from mcp_security_framework.
|
34
|
-
|
35
|
-
|
34
|
+
from mcp_security_framework.schemas.config import (
|
35
|
+
AuthConfig,
|
36
|
+
RateLimitConfig,
|
37
|
+
SecurityConfig,
|
38
|
+
)
|
39
|
+
from mcp_security_framework.schemas.models import (
|
40
|
+
AuthMethod,
|
41
|
+
AuthResult,
|
42
|
+
AuthStatus,
|
43
|
+
ValidationResult,
|
44
|
+
ValidationStatus,
|
45
|
+
)
|
36
46
|
|
37
47
|
|
38
48
|
class TestFlaskSecurityMiddleware:
|
39
49
|
"""Test suite for FlaskSecurityMiddleware class."""
|
40
|
-
|
50
|
+
|
41
51
|
def setup_method(self):
|
42
52
|
"""Set up test fixtures before each test method."""
|
43
53
|
# Create mock security manager
|
@@ -47,26 +57,26 @@ class TestFlaskSecurityMiddleware:
|
|
47
57
|
enabled=True,
|
48
58
|
methods=["api_key", "jwt"],
|
49
59
|
public_paths=["/health", "/docs"],
|
50
|
-
jwt_secret="test_jwt_secret_key"
|
60
|
+
jwt_secret="test_jwt_secret_key",
|
51
61
|
),
|
52
62
|
rate_limit=RateLimitConfig(
|
53
|
-
enabled=True,
|
54
|
-
|
55
|
-
window_size_seconds=60
|
56
|
-
)
|
63
|
+
enabled=True, default_requests_per_minute=100, window_size_seconds=60
|
64
|
+
),
|
57
65
|
)
|
58
|
-
|
66
|
+
|
59
67
|
# Create mock auth manager
|
60
68
|
self.mock_auth_manager = Mock()
|
61
69
|
self.mock_security_manager.auth_manager = self.mock_auth_manager
|
62
|
-
|
70
|
+
|
63
71
|
# Setup rate_limiter mock
|
64
72
|
self.mock_security_manager.rate_limiter = Mock()
|
65
|
-
|
73
|
+
|
66
74
|
# Create middleware instance
|
67
75
|
self.middleware = FlaskSecurityMiddleware(self.mock_security_manager)
|
68
|
-
|
69
|
-
def create_mock_request(
|
76
|
+
|
77
|
+
def create_mock_request(
|
78
|
+
self, path: str = "/api/test", headers: Dict[str, str] = None
|
79
|
+
) -> Mock:
|
70
80
|
"""Create a mock Flask request for testing."""
|
71
81
|
mock_request = Mock(spec=Request)
|
72
82
|
mock_request.path = path
|
@@ -74,112 +84,114 @@ class TestFlaskSecurityMiddleware:
|
|
74
84
|
mock_request.headers = headers or {}
|
75
85
|
mock_request.remote_addr = "127.0.0.1"
|
76
86
|
return mock_request
|
77
|
-
|
78
|
-
def create_mock_environ(
|
87
|
+
|
88
|
+
def create_mock_environ(
|
89
|
+
self, path: str = "/api/test", headers: Dict[str, str] = None
|
90
|
+
) -> Dict[str, Any]:
|
79
91
|
"""Create a mock WSGI environ for testing."""
|
80
92
|
environ = {
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
93
|
+
"REQUEST_METHOD": "GET",
|
94
|
+
"PATH_INFO": path,
|
95
|
+
"QUERY_STRING": "",
|
96
|
+
"SERVER_NAME": "localhost",
|
97
|
+
"SERVER_PORT": "5000",
|
98
|
+
"HTTP_HOST": "localhost:5000",
|
99
|
+
"wsgi.url_scheme": "http",
|
100
|
+
"wsgi.input": Mock(),
|
101
|
+
"wsgi.errors": Mock(),
|
102
|
+
"wsgi.version": (1, 0),
|
103
|
+
"wsgi.run_once": False,
|
104
|
+
"wsgi.multithread": False,
|
105
|
+
"wsgi.multiprocess": False,
|
94
106
|
}
|
95
|
-
|
107
|
+
|
96
108
|
# Add headers to environ
|
97
109
|
if headers:
|
98
110
|
for key, value in headers.items():
|
99
111
|
environ[f'HTTP_{key.upper().replace("-", "_")}'] = value
|
100
|
-
|
112
|
+
|
101
113
|
return environ
|
102
|
-
|
114
|
+
|
103
115
|
def test_initialization_success(self):
|
104
116
|
"""Test successful Flask middleware initialization."""
|
105
117
|
assert isinstance(self.middleware, FlaskSecurityMiddleware)
|
106
118
|
assert self.middleware.security_manager == self.mock_security_manager
|
107
119
|
assert self.middleware.config == self.mock_security_manager.config
|
108
|
-
|
120
|
+
|
109
121
|
def test_call_success(self):
|
110
122
|
"""Test successful middleware call."""
|
111
123
|
environ = self.create_mock_environ()
|
112
124
|
mock_start_response = Mock()
|
113
|
-
|
125
|
+
|
114
126
|
# Mock successful authentication and authorization
|
115
127
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
116
|
-
|
128
|
+
|
117
129
|
# Mock successful authentication
|
118
130
|
auth_result = AuthResult(
|
119
131
|
is_valid=True,
|
120
132
|
status=AuthStatus.SUCCESS,
|
121
133
|
username="test_user",
|
122
134
|
roles=["user"],
|
123
|
-
auth_method=AuthMethod.API_KEY
|
135
|
+
auth_method=AuthMethod.API_KEY,
|
124
136
|
)
|
125
137
|
self.middleware._authenticate_request = Mock(return_value=auth_result)
|
126
|
-
|
138
|
+
|
127
139
|
self.middleware._validate_permissions = Mock(return_value=True)
|
128
|
-
|
140
|
+
|
129
141
|
# Mock successful response
|
130
142
|
mock_response = [b'{"message": "success"}']
|
131
143
|
self.middleware._process_request = Mock(return_value=mock_response)
|
132
|
-
|
144
|
+
|
133
145
|
result = self.middleware(environ, mock_start_response)
|
134
|
-
|
146
|
+
|
135
147
|
# Verify middleware processed successfully
|
136
148
|
assert result == mock_response
|
137
149
|
self.middleware._process_request.assert_called_once()
|
138
|
-
|
150
|
+
|
139
151
|
def test_call_rate_limit_exceeded(self):
|
140
152
|
"""Test middleware call with rate limit exceeded."""
|
141
153
|
environ = self.create_mock_environ()
|
142
154
|
mock_start_response = Mock()
|
143
|
-
|
155
|
+
|
144
156
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = False
|
145
|
-
|
157
|
+
|
146
158
|
result = self.middleware(environ, mock_start_response)
|
147
|
-
|
159
|
+
|
148
160
|
# Verify rate limit response
|
149
161
|
assert isinstance(result, list)
|
150
162
|
assert len(result) == 1
|
151
|
-
response_data = json.loads(result[0].decode(
|
163
|
+
response_data = json.loads(result[0].decode("utf-8"))
|
152
164
|
assert response_data["error"] == "Rate limit exceeded"
|
153
|
-
|
165
|
+
|
154
166
|
# Verify start_response was called with correct status
|
155
167
|
mock_start_response.assert_called_once()
|
156
168
|
call_args = mock_start_response.call_args[0]
|
157
|
-
assert call_args[0] ==
|
158
|
-
|
169
|
+
assert call_args[0] == "429 Too Many Requests"
|
170
|
+
|
159
171
|
def test_call_public_path(self):
|
160
172
|
"""Test middleware call with public path."""
|
161
173
|
environ = self.create_mock_environ(path="/health")
|
162
174
|
mock_start_response = Mock()
|
163
|
-
|
175
|
+
|
164
176
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
165
|
-
|
177
|
+
|
166
178
|
# Mock successful response for public path
|
167
179
|
mock_response = [b'{"status": "healthy"}']
|
168
180
|
self.middleware._process_request = Mock(return_value=mock_response)
|
169
|
-
|
181
|
+
|
170
182
|
result = self.middleware(environ, mock_start_response)
|
171
|
-
|
183
|
+
|
172
184
|
# Verify middleware processed successfully for public path
|
173
185
|
assert result == mock_response
|
174
186
|
self.middleware._process_request.assert_called_once()
|
175
|
-
|
187
|
+
|
176
188
|
def test_call_authentication_failed(self):
|
177
189
|
"""Test middleware call with authentication failure."""
|
178
190
|
environ = self.create_mock_environ()
|
179
191
|
mock_start_response = Mock()
|
180
|
-
|
192
|
+
|
181
193
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
182
|
-
|
194
|
+
|
183
195
|
# Mock failed authentication
|
184
196
|
auth_result = AuthResult(
|
185
197
|
is_valid=False,
|
@@ -188,91 +200,91 @@ class TestFlaskSecurityMiddleware:
|
|
188
200
|
roles=[],
|
189
201
|
auth_method=AuthMethod.API_KEY,
|
190
202
|
error_code=-32005,
|
191
|
-
error_message="Authentication failed"
|
203
|
+
error_message="Authentication failed",
|
192
204
|
)
|
193
205
|
self.middleware._authenticate_request = Mock(return_value=auth_result)
|
194
|
-
|
206
|
+
|
195
207
|
result = self.middleware(environ, mock_start_response)
|
196
|
-
|
208
|
+
|
197
209
|
# Verify authentication error response
|
198
210
|
assert isinstance(result, list)
|
199
211
|
assert len(result) == 1
|
200
|
-
response_data = json.loads(result[0].decode(
|
212
|
+
response_data = json.loads(result[0].decode("utf-8"))
|
201
213
|
assert response_data["error"] == "Authentication failed"
|
202
|
-
|
214
|
+
|
203
215
|
# Verify start_response was called with correct status
|
204
216
|
mock_start_response.assert_called_once()
|
205
217
|
call_args = mock_start_response.call_args[0]
|
206
|
-
assert call_args[0] ==
|
207
|
-
|
218
|
+
assert call_args[0] == "401 Unauthorized"
|
219
|
+
|
208
220
|
def test_call_permission_denied(self):
|
209
221
|
"""Test middleware call with permission denied."""
|
210
222
|
environ = self.create_mock_environ()
|
211
223
|
mock_start_response = Mock()
|
212
|
-
|
224
|
+
|
213
225
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
214
|
-
|
226
|
+
|
215
227
|
# Mock successful authentication
|
216
228
|
auth_result = AuthResult(
|
217
229
|
is_valid=True,
|
218
230
|
status=AuthStatus.SUCCESS,
|
219
231
|
username="test_user",
|
220
232
|
roles=["user"],
|
221
|
-
auth_method=AuthMethod.API_KEY
|
233
|
+
auth_method=AuthMethod.API_KEY,
|
222
234
|
)
|
223
235
|
self.middleware._authenticate_request = Mock(return_value=auth_result)
|
224
|
-
|
236
|
+
|
225
237
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
226
238
|
is_valid=False,
|
227
239
|
status=ValidationStatus.INVALID,
|
228
240
|
error_code=-32007,
|
229
|
-
error_message="Permission denied"
|
241
|
+
error_message="Permission denied",
|
230
242
|
)
|
231
|
-
|
243
|
+
|
232
244
|
# Mock permission error response
|
233
245
|
mock_response = [b'{"error": "Permission denied"}']
|
234
246
|
self.middleware._process_request = Mock(return_value=mock_response)
|
235
|
-
|
247
|
+
|
236
248
|
result = self.middleware(environ, mock_start_response)
|
237
|
-
|
249
|
+
|
238
250
|
# Verify permission error response
|
239
251
|
assert result == mock_response
|
240
252
|
self.middleware._process_request.assert_called_once()
|
241
|
-
|
253
|
+
|
242
254
|
def test_get_rate_limit_identifier(self):
|
243
255
|
"""Test getting rate limit identifier from request."""
|
244
256
|
mock_request = self.create_mock_request()
|
245
257
|
result = self.middleware._get_rate_limit_identifier(mock_request)
|
246
258
|
assert result == "127.0.0.1"
|
247
|
-
|
259
|
+
|
248
260
|
def test_get_rate_limit_identifier_with_forwarded_for(self):
|
249
261
|
"""Test getting rate limit identifier with X-Forwarded-For header."""
|
250
262
|
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
251
263
|
mock_request = self.create_mock_request(headers=headers)
|
252
264
|
result = self.middleware._get_rate_limit_identifier(mock_request)
|
253
265
|
assert result == "192.168.1.1"
|
254
|
-
|
266
|
+
|
255
267
|
def test_get_rate_limit_identifier_with_real_ip(self):
|
256
268
|
"""Test getting rate limit identifier with X-Real-IP header."""
|
257
269
|
headers = {"X-Real-IP": "192.168.1.100"}
|
258
270
|
mock_request = self.create_mock_request(headers=headers)
|
259
271
|
result = self.middleware._get_rate_limit_identifier(mock_request)
|
260
272
|
assert result == "192.168.1.100"
|
261
|
-
|
273
|
+
|
262
274
|
def test_get_request_path(self):
|
263
275
|
"""Test getting request path from Flask request."""
|
264
276
|
mock_request = self.create_mock_request(path="/api/users")
|
265
277
|
result = self.middleware._get_request_path(mock_request)
|
266
278
|
assert result == "/api/users"
|
267
|
-
|
279
|
+
|
268
280
|
def test_get_required_permissions_from_request(self):
|
269
281
|
"""Test getting required permissions from request."""
|
270
282
|
mock_request = self.create_mock_request()
|
271
283
|
mock_request.required_permissions = ["read", "write"]
|
272
|
-
|
284
|
+
|
273
285
|
result = self.middleware._get_required_permissions(mock_request)
|
274
286
|
assert result == ["read", "write"]
|
275
|
-
|
287
|
+
|
276
288
|
def test_get_required_permissions_default(self):
|
277
289
|
"""Test getting required permissions when not set."""
|
278
290
|
mock_request = self.create_mock_request()
|
@@ -284,178 +296,183 @@ class TestFlaskSecurityMiddleware:
|
|
284
296
|
mock_request.endpoint = mock_endpoint
|
285
297
|
result = self.middleware._get_required_permissions(mock_request)
|
286
298
|
assert result == []
|
287
|
-
|
299
|
+
|
288
300
|
def test_try_api_key_auth_success(self):
|
289
301
|
"""Test successful API key authentication."""
|
290
302
|
headers = {"X-API-Key": "valid_key"}
|
291
303
|
mock_request = self.create_mock_request(headers=headers)
|
292
|
-
|
304
|
+
|
293
305
|
expected_result = AuthResult(
|
294
306
|
is_valid=True,
|
295
307
|
status=AuthStatus.SUCCESS,
|
296
308
|
username="test_user",
|
297
309
|
roles=["user"],
|
298
|
-
auth_method=AuthMethod.API_KEY
|
310
|
+
auth_method=AuthMethod.API_KEY,
|
299
311
|
)
|
300
312
|
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
301
|
-
|
313
|
+
|
302
314
|
result = self.middleware._try_api_key_auth(mock_request)
|
303
|
-
|
315
|
+
|
304
316
|
assert result == expected_result
|
305
317
|
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("valid_key")
|
306
|
-
|
318
|
+
|
307
319
|
def test_try_api_key_auth_from_authorization_header(self):
|
308
320
|
"""Test API key authentication from Authorization header."""
|
309
321
|
headers = {"Authorization": "Bearer api_key_123"}
|
310
322
|
mock_request = self.create_mock_request(headers=headers)
|
311
|
-
|
323
|
+
|
312
324
|
expected_result = AuthResult(
|
313
325
|
is_valid=True,
|
314
326
|
status=AuthStatus.SUCCESS,
|
315
327
|
username="test_user",
|
316
328
|
roles=["user"],
|
317
|
-
auth_method=AuthMethod.API_KEY
|
329
|
+
auth_method=AuthMethod.API_KEY,
|
318
330
|
)
|
319
331
|
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
320
|
-
|
332
|
+
|
321
333
|
result = self.middleware._try_api_key_auth(mock_request)
|
322
|
-
|
334
|
+
|
323
335
|
assert result == expected_result
|
324
|
-
self.mock_auth_manager.authenticate_api_key.assert_called_once_with(
|
325
|
-
|
336
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with(
|
337
|
+
"api_key_123"
|
338
|
+
)
|
339
|
+
|
326
340
|
def test_try_api_key_auth_no_key(self):
|
327
341
|
"""Test API key authentication with no key provided."""
|
328
342
|
mock_request = self.create_mock_request()
|
329
|
-
|
343
|
+
|
330
344
|
result = self.middleware._try_api_key_auth(mock_request)
|
331
|
-
|
345
|
+
|
332
346
|
assert result.is_valid is False
|
333
347
|
assert result.error_code == -32024
|
334
348
|
assert "API key not found" in result.error_message
|
335
|
-
|
349
|
+
|
336
350
|
def test_try_jwt_auth_success(self):
|
337
351
|
"""Test successful JWT authentication."""
|
338
352
|
headers = {"Authorization": "Bearer jwt_token_123"}
|
339
353
|
mock_request = self.create_mock_request(headers=headers)
|
340
|
-
|
354
|
+
|
341
355
|
expected_result = AuthResult(
|
342
356
|
is_valid=True,
|
343
357
|
status=AuthStatus.SUCCESS,
|
344
358
|
username="test_user",
|
345
359
|
roles=["user"],
|
346
|
-
auth_method=AuthMethod.JWT
|
360
|
+
auth_method=AuthMethod.JWT,
|
347
361
|
)
|
348
362
|
self.mock_auth_manager.authenticate_jwt_token.return_value = expected_result
|
349
|
-
|
363
|
+
|
350
364
|
result = self.middleware._try_jwt_auth(mock_request)
|
351
|
-
|
365
|
+
|
352
366
|
assert result == expected_result
|
353
|
-
self.mock_auth_manager.authenticate_jwt_token.assert_called_once_with(
|
354
|
-
|
367
|
+
self.mock_auth_manager.authenticate_jwt_token.assert_called_once_with(
|
368
|
+
"jwt_token_123"
|
369
|
+
)
|
370
|
+
|
355
371
|
def test_try_jwt_auth_no_token(self):
|
356
372
|
"""Test JWT authentication with no token provided."""
|
357
373
|
mock_request = self.create_mock_request()
|
358
|
-
|
374
|
+
|
359
375
|
result = self.middleware._try_jwt_auth(mock_request)
|
360
|
-
|
376
|
+
|
361
377
|
assert result.is_valid is False
|
362
378
|
assert result.error_code == -32025
|
363
379
|
assert "JWT token not found" in result.error_message
|
364
|
-
|
380
|
+
|
365
381
|
def test_try_jwt_auth_invalid_header(self):
|
366
382
|
"""Test JWT authentication with invalid Authorization header."""
|
367
383
|
headers = {"Authorization": "Invalid jwt_token_123"}
|
368
384
|
mock_request = self.create_mock_request(headers=headers)
|
369
|
-
|
385
|
+
|
370
386
|
result = self.middleware._try_jwt_auth(mock_request)
|
371
|
-
|
387
|
+
|
372
388
|
assert result.is_valid is False
|
373
389
|
assert result.error_code == -32025
|
374
390
|
assert "JWT token not found" in result.error_message
|
375
|
-
|
391
|
+
|
376
392
|
def test_try_certificate_auth_not_implemented(self):
|
377
393
|
"""Test certificate authentication (not implemented)."""
|
378
394
|
mock_request = self.create_mock_request()
|
379
|
-
|
395
|
+
|
380
396
|
result = self.middleware._try_certificate_auth(mock_request)
|
381
|
-
|
397
|
+
|
382
398
|
assert result.is_valid is False
|
383
399
|
assert result.error_code == -32026
|
384
400
|
assert "not implemented" in result.error_message
|
385
|
-
|
401
|
+
|
386
402
|
def test_try_basic_auth_not_implemented(self):
|
387
403
|
"""Test basic authentication (not implemented)."""
|
388
404
|
mock_request = self.create_mock_request()
|
389
|
-
|
405
|
+
|
390
406
|
result = self.middleware._try_basic_auth(mock_request)
|
391
|
-
|
407
|
+
|
392
408
|
assert result.is_valid is False
|
393
409
|
assert result.error_code == -32027
|
394
410
|
assert "Basic authentication credentials not found" in result.error_message
|
395
|
-
|
411
|
+
|
396
412
|
def test_try_auth_method_unsupported(self):
|
397
413
|
"""Test authentication with unsupported method."""
|
398
414
|
mock_request = self.create_mock_request()
|
399
|
-
|
415
|
+
|
400
416
|
result = self.middleware._try_auth_method(mock_request, "unsupported_method")
|
401
|
-
|
417
|
+
|
402
418
|
assert result.is_valid is False
|
403
419
|
assert result.error_code == -32022
|
404
420
|
assert "Unsupported authentication method" in result.error_message
|
405
|
-
|
421
|
+
|
406
422
|
def test_apply_security_headers(self):
|
407
423
|
"""Test applying security headers to Flask response."""
|
408
424
|
mock_response = Mock(spec=Response)
|
409
425
|
mock_response.headers = {}
|
410
|
-
|
426
|
+
|
411
427
|
headers = {
|
412
428
|
"X-Content-Type-Options": "nosniff",
|
413
429
|
"X-Frame-Options": "DENY",
|
414
|
-
"X-XSS-Protection": "1; mode=block"
|
430
|
+
"X-XSS-Protection": "1; mode=block",
|
415
431
|
}
|
416
|
-
|
432
|
+
|
417
433
|
self.middleware._apply_security_headers(mock_response, headers)
|
418
|
-
|
434
|
+
|
419
435
|
assert mock_response.headers["X-Content-Type-Options"] == "nosniff"
|
420
436
|
assert mock_response.headers["X-Frame-Options"] == "DENY"
|
421
437
|
assert mock_response.headers["X-XSS-Protection"] == "1; mode=block"
|
422
|
-
|
438
|
+
|
423
439
|
def test_create_error_response(self):
|
424
440
|
"""Test creating error response."""
|
425
441
|
from flask import Flask
|
442
|
+
|
426
443
|
app = Flask(__name__)
|
427
|
-
|
444
|
+
|
428
445
|
with app.app_context():
|
429
446
|
result = self.middleware._create_error_response(400, "Bad request")
|
430
|
-
|
447
|
+
|
431
448
|
assert isinstance(result, Response)
|
432
449
|
assert result.status_code == 400
|
433
450
|
response_data = json.loads(result.get_data(as_text=True))
|
434
451
|
assert response_data["error"] == "Security violation"
|
435
452
|
assert response_data["message"] == "Bad request"
|
436
|
-
|
453
|
+
|
437
454
|
def test_rate_limit_response(self):
|
438
455
|
"""Test creating rate limit response."""
|
439
456
|
mock_start_response = Mock()
|
440
|
-
|
457
|
+
|
441
458
|
result = self.middleware._rate_limit_response(mock_start_response)
|
442
|
-
|
459
|
+
|
443
460
|
assert isinstance(result, list)
|
444
461
|
assert len(result) == 1
|
445
|
-
response_data = json.loads(result[0].decode(
|
462
|
+
response_data = json.loads(result[0].decode("utf-8"))
|
446
463
|
assert response_data["error"] == "Rate limit exceeded"
|
447
|
-
|
464
|
+
|
448
465
|
# Verify start_response was called
|
449
466
|
mock_start_response.assert_called_once()
|
450
467
|
call_args = mock_start_response.call_args[0]
|
451
|
-
assert call_args[0] ==
|
452
|
-
|
468
|
+
assert call_args[0] == "429 Too Many Requests"
|
469
|
+
|
453
470
|
# Check for Retry-After header
|
454
471
|
headers = call_args[1]
|
455
|
-
retry_after_header = next((h for h in headers if h[0] ==
|
472
|
+
retry_after_header = next((h for h in headers if h[0] == "Retry-After"), None)
|
456
473
|
assert retry_after_header is not None
|
457
|
-
assert retry_after_header[1] ==
|
458
|
-
|
474
|
+
assert retry_after_header[1] == "60"
|
475
|
+
|
459
476
|
def test_auth_error_response(self):
|
460
477
|
"""Test creating authentication error response."""
|
461
478
|
auth_result = AuthResult(
|
@@ -465,118 +482,123 @@ class TestFlaskSecurityMiddleware:
|
|
465
482
|
roles=[],
|
466
483
|
auth_method=AuthMethod.API_KEY,
|
467
484
|
error_code=-32005,
|
468
|
-
error_message="Invalid API key"
|
485
|
+
error_message="Invalid API key",
|
469
486
|
)
|
470
|
-
|
487
|
+
|
471
488
|
mock_start_response = Mock()
|
472
489
|
result = self.middleware._auth_error_response(auth_result, mock_start_response)
|
473
|
-
|
490
|
+
|
474
491
|
assert isinstance(result, list)
|
475
492
|
assert len(result) == 1
|
476
|
-
response_data = json.loads(result[0].decode(
|
493
|
+
response_data = json.loads(result[0].decode("utf-8"))
|
477
494
|
assert response_data["error"] == "Authentication failed"
|
478
495
|
assert response_data["message"] == "Invalid API key"
|
479
|
-
|
496
|
+
|
480
497
|
# Verify start_response was called
|
481
498
|
mock_start_response.assert_called_once()
|
482
499
|
call_args = mock_start_response.call_args[0]
|
483
|
-
assert call_args[0] ==
|
484
|
-
|
500
|
+
assert call_args[0] == "401 Unauthorized"
|
501
|
+
|
485
502
|
# Check for WWW-Authenticate header
|
486
503
|
headers = call_args[1]
|
487
|
-
www_auth_header = next((h for h in headers if h[0] ==
|
504
|
+
www_auth_header = next((h for h in headers if h[0] == "WWW-Authenticate"), None)
|
488
505
|
assert www_auth_header is not None
|
489
|
-
assert www_auth_header[1] ==
|
490
|
-
|
506
|
+
assert www_auth_header[1] == "Bearer, ApiKey"
|
507
|
+
|
491
508
|
def test_permission_error_response(self):
|
492
509
|
"""Test creating permission error response."""
|
493
510
|
mock_start_response = Mock()
|
494
|
-
|
511
|
+
|
495
512
|
result = self.middleware._permission_error_response(mock_start_response)
|
496
|
-
|
513
|
+
|
497
514
|
assert isinstance(result, list)
|
498
515
|
assert len(result) == 1
|
499
|
-
response_data = json.loads(result[0].decode(
|
516
|
+
response_data = json.loads(result[0].decode("utf-8"))
|
500
517
|
assert response_data["error"] == "Permission denied"
|
501
|
-
assert
|
502
|
-
|
518
|
+
assert (
|
519
|
+
response_data["message"]
|
520
|
+
== "Insufficient permissions to access this resource"
|
521
|
+
)
|
522
|
+
|
503
523
|
# Verify start_response was called
|
504
524
|
mock_start_response.assert_called_once()
|
505
525
|
call_args = mock_start_response.call_args[0]
|
506
|
-
assert call_args[0] ==
|
507
|
-
|
526
|
+
assert call_args[0] == "403 Forbidden"
|
527
|
+
|
508
528
|
def test_get_client_ip_from_forwarded_for(self):
|
509
529
|
"""Test getting client IP from X-Forwarded-For header."""
|
510
530
|
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
511
531
|
mock_request = self.create_mock_request(headers=headers)
|
512
|
-
|
532
|
+
|
513
533
|
result = self.middleware._get_client_ip(mock_request)
|
514
534
|
assert result == "192.168.1.1"
|
515
|
-
|
535
|
+
|
516
536
|
def test_get_client_ip_from_real_ip(self):
|
517
537
|
"""Test getting client IP from X-Real-IP header."""
|
518
538
|
headers = {"X-Real-IP": "192.168.1.100"}
|
519
539
|
mock_request = self.create_mock_request(headers=headers)
|
520
|
-
|
540
|
+
|
521
541
|
result = self.middleware._get_client_ip(mock_request)
|
522
542
|
assert result == "192.168.1.100"
|
523
|
-
|
543
|
+
|
524
544
|
def test_get_client_ip_from_remote_addr(self):
|
525
545
|
"""Test getting client IP from remote_addr."""
|
526
546
|
mock_request = self.create_mock_request()
|
527
547
|
mock_request.remote_addr = "192.168.1.50"
|
528
|
-
|
548
|
+
|
529
549
|
result = self.middleware._get_client_ip(mock_request)
|
530
550
|
assert result == "192.168.1.50"
|
531
|
-
|
551
|
+
|
532
552
|
def test_get_client_ip_fallback(self):
|
533
553
|
"""Test getting client IP with fallback."""
|
534
554
|
mock_request = self.create_mock_request()
|
535
555
|
mock_request.remote_addr = None
|
536
|
-
|
556
|
+
|
537
557
|
result = self.middleware._get_client_ip(mock_request)
|
538
558
|
assert result == "127.0.0.1"
|
539
|
-
|
559
|
+
|
540
560
|
def test_get_security_headers(self):
|
541
561
|
"""Test getting security headers."""
|
542
562
|
result = self.middleware._get_security_headers()
|
543
|
-
|
563
|
+
|
544
564
|
assert isinstance(result, list)
|
545
565
|
assert all(isinstance(header, tuple) and len(header) == 2 for header in result)
|
546
|
-
|
566
|
+
|
547
567
|
# Check for standard security headers
|
548
568
|
header_names = [header[0] for header in result]
|
549
|
-
assert
|
550
|
-
assert
|
551
|
-
assert
|
552
|
-
assert
|
553
|
-
assert
|
554
|
-
assert
|
555
|
-
|
569
|
+
assert "X-Content-Type-Options" in header_names
|
570
|
+
assert "X-Frame-Options" in header_names
|
571
|
+
assert "X-XSS-Protection" in header_names
|
572
|
+
assert "Strict-Transport-Security" in header_names
|
573
|
+
assert "Content-Security-Policy" in header_names
|
574
|
+
assert "Referrer-Policy" in header_names
|
575
|
+
|
556
576
|
def test_get_security_headers_with_custom_headers(self):
|
557
577
|
"""Test getting security headers with custom headers from config."""
|
558
578
|
self.mock_security_manager.config.auth.security_headers = {
|
559
579
|
"Custom-Security-Header": "custom_value"
|
560
580
|
}
|
561
|
-
|
581
|
+
|
562
582
|
result = self.middleware._get_security_headers()
|
563
|
-
|
583
|
+
|
564
584
|
header_names = [header[0] for header in result]
|
565
|
-
assert
|
566
|
-
|
567
|
-
custom_header = next(h for h in result if h[0] ==
|
568
|
-
assert custom_header[1] ==
|
569
|
-
|
585
|
+
assert "Custom-Security-Header" in header_names
|
586
|
+
|
587
|
+
custom_header = next(h for h in result if h[0] == "Custom-Security-Header")
|
588
|
+
assert custom_header[1] == "custom_value"
|
589
|
+
|
570
590
|
def test_call_with_exception(self):
|
571
591
|
"""Test middleware call with exception."""
|
572
592
|
environ = self.create_mock_environ()
|
573
593
|
mock_start_response = Mock()
|
574
|
-
|
594
|
+
|
575
595
|
# Mock an exception during processing
|
576
|
-
self.mock_security_manager.rate_limiter.check_rate_limit.side_effect =
|
577
|
-
|
596
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.side_effect = (
|
597
|
+
Exception("Test error")
|
598
|
+
)
|
599
|
+
|
578
600
|
with pytest.raises(FlaskMiddlewareError) as exc_info:
|
579
601
|
self.middleware(environ, mock_start_response)
|
580
|
-
|
602
|
+
|
581
603
|
assert "Middleware processing failed" in str(exc_info.value)
|
582
604
|
assert exc_info.value.error_code == -32003
|