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,25 +19,35 @@ Version: 1.0.0
|
|
19
19
|
License: MIT
|
20
20
|
"""
|
21
21
|
|
22
|
-
import
|
23
|
-
from unittest.mock import
|
24
|
-
from typing import Dict, List, Any
|
22
|
+
from typing import Any, Dict, List
|
23
|
+
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
25
24
|
|
26
|
-
|
25
|
+
import pytest
|
26
|
+
from fastapi import HTTPException, Request, Response, status
|
27
27
|
from fastapi.responses import JSONResponse
|
28
28
|
|
29
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
29
30
|
from mcp_security_framework.middleware.fastapi_middleware import (
|
31
|
+
FastAPIMiddlewareError,
|
30
32
|
FastAPISecurityMiddleware,
|
31
|
-
FastAPIMiddlewareError
|
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 TestFastAPISecurityMiddleware:
|
39
49
|
"""Test suite for FastAPISecurityMiddleware 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 TestFastAPISecurityMiddleware:
|
|
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 = FastAPISecurityMiddleware(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 FastAPI request for testing."""
|
71
81
|
mock_request = Mock(spec=Request)
|
72
82
|
mock_request.url.path = path
|
@@ -75,13 +85,13 @@ class TestFastAPISecurityMiddleware:
|
|
75
85
|
mock_request.client = Mock()
|
76
86
|
mock_request.client.host = "127.0.0.1"
|
77
87
|
return mock_request
|
78
|
-
|
88
|
+
|
79
89
|
def test_initialization_success(self):
|
80
90
|
"""Test successful FastAPI middleware initialization."""
|
81
91
|
assert isinstance(self.middleware, FastAPISecurityMiddleware)
|
82
92
|
assert self.middleware.security_manager == self.mock_security_manager
|
83
93
|
assert self.middleware.config == self.mock_security_manager.config
|
84
|
-
|
94
|
+
|
85
95
|
@pytest.mark.asyncio
|
86
96
|
async def test_call_success(self):
|
87
97
|
"""Test successful middleware call."""
|
@@ -91,48 +101,47 @@ class TestFastAPISecurityMiddleware:
|
|
91
101
|
mock_response.status_code = 200
|
92
102
|
mock_response.headers = {}
|
93
103
|
mock_call_next.return_value = mock_response
|
94
|
-
|
104
|
+
|
95
105
|
# Mock successful authentication and authorization
|
96
106
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
97
|
-
|
107
|
+
|
98
108
|
# Mock the _authenticate_request method directly
|
99
109
|
auth_result = AuthResult(
|
100
110
|
is_valid=True,
|
101
111
|
status=AuthStatus.SUCCESS,
|
102
112
|
username="test_user",
|
103
113
|
roles=["user"],
|
104
|
-
auth_method=AuthMethod.API_KEY
|
114
|
+
auth_method=AuthMethod.API_KEY,
|
105
115
|
)
|
106
116
|
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
107
|
-
|
117
|
+
|
108
118
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
109
|
-
is_valid=True,
|
110
|
-
status=ValidationStatus.VALID
|
119
|
+
is_valid=True, status=ValidationStatus.VALID
|
111
120
|
)
|
112
|
-
|
121
|
+
|
113
122
|
result = await self.middleware(mock_request, mock_call_next)
|
114
|
-
|
123
|
+
|
115
124
|
assert result == mock_response
|
116
125
|
mock_call_next.assert_called_once_with(mock_request)
|
117
|
-
|
126
|
+
|
118
127
|
@pytest.mark.asyncio
|
119
128
|
async def test_call_rate_limit_exceeded(self):
|
120
129
|
"""Test middleware call with rate limit exceeded."""
|
121
130
|
mock_request = self.create_mock_request()
|
122
131
|
mock_call_next = AsyncMock()
|
123
|
-
|
132
|
+
|
124
133
|
# Mock _is_public_path to return False so rate limiting is checked
|
125
134
|
self.middleware._is_public_path = Mock(return_value=False)
|
126
135
|
# Mock the security manager's check_rate_limit method
|
127
136
|
self.mock_security_manager.check_rate_limit.return_value = False
|
128
|
-
|
137
|
+
|
129
138
|
result = await self.middleware(mock_request, mock_call_next)
|
130
|
-
|
139
|
+
|
131
140
|
assert isinstance(result, JSONResponse)
|
132
141
|
assert result.status_code == status.HTTP_429_TOO_MANY_REQUESTS
|
133
142
|
assert "Rate limit exceeded" in result.body.decode()
|
134
143
|
mock_call_next.assert_not_called()
|
135
|
-
|
144
|
+
|
136
145
|
@pytest.mark.asyncio
|
137
146
|
async def test_call_public_path(self):
|
138
147
|
"""Test middleware call with public path."""
|
@@ -142,20 +151,20 @@ class TestFastAPISecurityMiddleware:
|
|
142
151
|
mock_response.status_code = 200
|
143
152
|
mock_response.headers = {}
|
144
153
|
mock_call_next.return_value = mock_response
|
145
|
-
|
154
|
+
|
146
155
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
147
|
-
|
156
|
+
|
148
157
|
result = await self.middleware(mock_request, mock_call_next)
|
149
|
-
|
158
|
+
|
150
159
|
assert result == mock_response
|
151
160
|
mock_call_next.assert_called_once_with(mock_request)
|
152
|
-
|
161
|
+
|
153
162
|
@pytest.mark.asyncio
|
154
163
|
async def test_call_authentication_failed(self):
|
155
164
|
"""Test middleware call with authentication failure."""
|
156
165
|
mock_request = self.create_mock_request()
|
157
166
|
mock_call_next = AsyncMock()
|
158
|
-
|
167
|
+
|
159
168
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
160
169
|
self.mock_auth_manager.authenticate_api_key.return_value = AuthResult(
|
161
170
|
is_valid=False,
|
@@ -164,249 +173,255 @@ class TestFastAPISecurityMiddleware:
|
|
164
173
|
roles=[],
|
165
174
|
auth_method=AuthMethod.API_KEY,
|
166
175
|
error_code=-32005,
|
167
|
-
error_message="Authentication failed"
|
176
|
+
error_message="Authentication failed",
|
168
177
|
)
|
169
|
-
|
178
|
+
|
170
179
|
result = await self.middleware(mock_request, mock_call_next)
|
171
|
-
|
180
|
+
|
172
181
|
assert isinstance(result, JSONResponse)
|
173
182
|
assert result.status_code == status.HTTP_401_UNAUTHORIZED
|
174
183
|
assert "Authentication failed" in result.body.decode()
|
175
184
|
mock_call_next.assert_not_called()
|
176
|
-
|
185
|
+
|
177
186
|
@pytest.mark.asyncio
|
178
187
|
async def test_call_permission_denied(self):
|
179
188
|
"""Test middleware call with permission denied."""
|
180
189
|
mock_request = self.create_mock_request()
|
181
190
|
mock_call_next = AsyncMock()
|
182
|
-
|
191
|
+
|
183
192
|
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
184
|
-
|
193
|
+
|
185
194
|
# Mock successful authentication
|
186
195
|
auth_result = AuthResult(
|
187
196
|
is_valid=True,
|
188
197
|
status=AuthStatus.SUCCESS,
|
189
198
|
username="test_user",
|
190
199
|
roles=["user"],
|
191
|
-
auth_method=AuthMethod.API_KEY
|
200
|
+
auth_method=AuthMethod.API_KEY,
|
192
201
|
)
|
193
202
|
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
194
|
-
|
203
|
+
|
195
204
|
# Mock the _validate_permissions method directly
|
196
205
|
self.middleware._validate_permissions = Mock(return_value=False)
|
197
|
-
|
206
|
+
|
198
207
|
result = await self.middleware(mock_request, mock_call_next)
|
199
|
-
|
208
|
+
|
200
209
|
assert isinstance(result, JSONResponse)
|
201
210
|
assert result.status_code == status.HTTP_403_FORBIDDEN
|
202
211
|
assert "Permission denied" in result.body.decode()
|
203
212
|
mock_call_next.assert_not_called()
|
204
|
-
|
213
|
+
|
205
214
|
def test_get_rate_limit_identifier(self):
|
206
215
|
"""Test getting rate limit identifier from request."""
|
207
216
|
mock_request = self.create_mock_request()
|
208
217
|
result = self.middleware._get_rate_limit_identifier(mock_request)
|
209
218
|
assert result == "127.0.0.1"
|
210
|
-
|
219
|
+
|
211
220
|
def test_get_rate_limit_identifier_with_forwarded_for(self):
|
212
221
|
"""Test getting rate limit identifier with X-Forwarded-For header."""
|
213
222
|
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
214
223
|
mock_request = self.create_mock_request(headers=headers)
|
215
224
|
result = self.middleware._get_rate_limit_identifier(mock_request)
|
216
225
|
assert result == "192.168.1.1"
|
217
|
-
|
226
|
+
|
218
227
|
def test_get_rate_limit_identifier_with_real_ip(self):
|
219
228
|
"""Test getting rate limit identifier with X-Real-IP header."""
|
220
229
|
headers = {"X-Real-IP": "192.168.1.100"}
|
221
230
|
mock_request = self.create_mock_request(headers=headers)
|
222
231
|
result = self.middleware._get_rate_limit_identifier(mock_request)
|
223
232
|
assert result == "192.168.1.100"
|
224
|
-
|
233
|
+
|
225
234
|
def test_get_request_path(self):
|
226
235
|
"""Test getting request path from FastAPI request."""
|
227
236
|
mock_request = self.create_mock_request(path="/api/users")
|
228
237
|
result = self.middleware._get_request_path(mock_request)
|
229
238
|
assert result == "/api/users"
|
230
|
-
|
239
|
+
|
231
240
|
def test_get_required_permissions_from_state(self):
|
232
241
|
"""Test getting required permissions from request state."""
|
233
242
|
mock_request = self.create_mock_request()
|
234
243
|
mock_request.state.required_permissions = ["read", "write"]
|
235
|
-
|
244
|
+
|
236
245
|
result = self.middleware._get_required_permissions(mock_request)
|
237
246
|
assert result == ["read", "write"]
|
238
|
-
|
247
|
+
|
239
248
|
def test_get_required_permissions_default(self):
|
240
249
|
"""Test getting required permissions when not set."""
|
241
250
|
mock_request = self.create_mock_request()
|
242
251
|
# Ensure state doesn't have required_permissions
|
243
|
-
if hasattr(mock_request,
|
244
|
-
delattr(mock_request.state,
|
252
|
+
if hasattr(mock_request, "state"):
|
253
|
+
delattr(mock_request.state, "required_permissions")
|
245
254
|
result = self.middleware._get_required_permissions(mock_request)
|
246
255
|
assert result == []
|
247
|
-
|
256
|
+
|
248
257
|
@pytest.mark.asyncio
|
249
258
|
async def test_try_api_key_auth_success(self):
|
250
259
|
"""Test successful API key authentication."""
|
251
260
|
headers = {"X-API-Key": "valid_key"}
|
252
261
|
mock_request = self.create_mock_request(headers=headers)
|
253
|
-
|
262
|
+
|
254
263
|
expected_result = AuthResult(
|
255
264
|
is_valid=True,
|
256
265
|
status=AuthStatus.SUCCESS,
|
257
266
|
username="test_user",
|
258
267
|
roles=["user"],
|
259
|
-
auth_method=AuthMethod.API_KEY
|
268
|
+
auth_method=AuthMethod.API_KEY,
|
260
269
|
)
|
261
270
|
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
262
|
-
|
271
|
+
|
263
272
|
result = await self.middleware._try_api_key_auth(mock_request)
|
264
|
-
|
273
|
+
|
265
274
|
assert result == expected_result
|
266
275
|
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("valid_key")
|
267
|
-
|
276
|
+
|
268
277
|
@pytest.mark.asyncio
|
269
278
|
async def test_try_api_key_auth_from_authorization_header(self):
|
270
279
|
"""Test API key authentication from Authorization header."""
|
271
280
|
headers = {"Authorization": "Bearer api_key_123"}
|
272
281
|
mock_request = self.create_mock_request(headers=headers)
|
273
|
-
|
282
|
+
|
274
283
|
expected_result = AuthResult(
|
275
284
|
is_valid=True,
|
276
285
|
status=AuthStatus.SUCCESS,
|
277
286
|
username="test_user",
|
278
287
|
roles=["user"],
|
279
|
-
auth_method=AuthMethod.API_KEY
|
288
|
+
auth_method=AuthMethod.API_KEY,
|
280
289
|
)
|
281
290
|
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
282
|
-
|
291
|
+
|
283
292
|
result = await self.middleware._try_api_key_auth(mock_request)
|
284
|
-
|
293
|
+
|
285
294
|
assert result == expected_result
|
286
|
-
self.mock_auth_manager.authenticate_api_key.assert_called_once_with(
|
287
|
-
|
295
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with(
|
296
|
+
"api_key_123"
|
297
|
+
)
|
298
|
+
|
288
299
|
@pytest.mark.asyncio
|
289
300
|
async def test_try_api_key_auth_no_key(self):
|
290
301
|
"""Test API key authentication with no key provided."""
|
291
302
|
mock_request = self.create_mock_request()
|
292
|
-
|
303
|
+
|
293
304
|
result = await self.middleware._try_api_key_auth(mock_request)
|
294
|
-
|
305
|
+
|
295
306
|
assert result.is_valid is False
|
296
307
|
assert result.error_code == -32012
|
297
308
|
assert "API key not found" in result.error_message
|
298
|
-
|
309
|
+
|
299
310
|
@pytest.mark.asyncio
|
300
311
|
async def test_try_jwt_auth_success(self):
|
301
312
|
"""Test successful JWT authentication."""
|
302
313
|
headers = {"Authorization": "Bearer jwt_token_123"}
|
303
314
|
mock_request = self.create_mock_request(headers=headers)
|
304
|
-
|
315
|
+
|
305
316
|
expected_result = AuthResult(
|
306
317
|
is_valid=True,
|
307
318
|
status=AuthStatus.SUCCESS,
|
308
319
|
username="test_user",
|
309
320
|
roles=["user"],
|
310
|
-
auth_method=AuthMethod.JWT
|
321
|
+
auth_method=AuthMethod.JWT,
|
311
322
|
)
|
312
323
|
self.mock_auth_manager.authenticate_jwt_token.return_value = expected_result
|
313
|
-
|
324
|
+
|
314
325
|
result = await self.middleware._try_jwt_auth(mock_request)
|
315
|
-
|
326
|
+
|
316
327
|
assert result == expected_result
|
317
|
-
self.mock_auth_manager.authenticate_jwt_token.assert_called_once_with(
|
318
|
-
|
328
|
+
self.mock_auth_manager.authenticate_jwt_token.assert_called_once_with(
|
329
|
+
"jwt_token_123"
|
330
|
+
)
|
331
|
+
|
319
332
|
@pytest.mark.asyncio
|
320
333
|
async def test_try_jwt_auth_no_token(self):
|
321
334
|
"""Test JWT authentication with no token provided."""
|
322
335
|
mock_request = self.create_mock_request()
|
323
|
-
|
336
|
+
|
324
337
|
result = await self.middleware._try_jwt_auth(mock_request)
|
325
|
-
|
338
|
+
|
326
339
|
assert result.is_valid is False
|
327
340
|
assert result.error_code == -32013
|
328
341
|
assert "JWT token not found" in result.error_message
|
329
|
-
|
342
|
+
|
330
343
|
@pytest.mark.asyncio
|
331
344
|
async def test_try_jwt_auth_invalid_header(self):
|
332
345
|
"""Test JWT authentication with invalid Authorization header."""
|
333
346
|
headers = {"Authorization": "Invalid jwt_token_123"}
|
334
347
|
mock_request = self.create_mock_request(headers=headers)
|
335
|
-
|
348
|
+
|
336
349
|
result = await self.middleware._try_jwt_auth(mock_request)
|
337
|
-
|
350
|
+
|
338
351
|
assert result.is_valid is False
|
339
352
|
assert result.error_code == -32013
|
340
353
|
assert "JWT token not found" in result.error_message
|
341
|
-
|
354
|
+
|
342
355
|
@pytest.mark.asyncio
|
343
356
|
async def test_try_certificate_auth_not_implemented(self):
|
344
357
|
"""Test certificate authentication (not implemented)."""
|
345
358
|
mock_request = self.create_mock_request()
|
346
|
-
|
359
|
+
|
347
360
|
result = await self.middleware._try_certificate_auth(mock_request)
|
348
|
-
|
361
|
+
|
349
362
|
assert result.is_valid is False
|
350
363
|
assert result.error_code == -32014
|
351
364
|
assert "not implemented" in result.error_message
|
352
|
-
|
365
|
+
|
353
366
|
@pytest.mark.asyncio
|
354
367
|
async def test_try_basic_auth_not_implemented(self):
|
355
368
|
"""Test basic authentication (not implemented)."""
|
356
369
|
mock_request = self.create_mock_request()
|
357
|
-
|
370
|
+
|
358
371
|
result = await self.middleware._try_basic_auth(mock_request)
|
359
|
-
|
372
|
+
|
360
373
|
assert result.is_valid is False
|
361
374
|
assert result.error_code == -32015
|
362
375
|
assert "not found" in result.error_message
|
363
|
-
|
376
|
+
|
364
377
|
@pytest.mark.asyncio
|
365
378
|
async def test_try_auth_method_unsupported(self):
|
366
379
|
"""Test authentication with unsupported method."""
|
367
380
|
mock_request = self.create_mock_request()
|
368
|
-
|
369
|
-
result = await self.middleware._try_auth_method(
|
370
|
-
|
381
|
+
|
382
|
+
result = await self.middleware._try_auth_method(
|
383
|
+
mock_request, "unsupported_method"
|
384
|
+
)
|
385
|
+
|
371
386
|
assert result.is_valid is False
|
372
387
|
assert result.error_code == -32010
|
373
388
|
assert "Unsupported authentication method" in result.error_message
|
374
|
-
|
389
|
+
|
375
390
|
def test_apply_security_headers(self):
|
376
391
|
"""Test applying security headers to FastAPI response."""
|
377
392
|
mock_response = Mock(spec=Response)
|
378
393
|
mock_response.headers = {}
|
379
|
-
|
394
|
+
|
380
395
|
headers = {
|
381
396
|
"X-Content-Type-Options": "nosniff",
|
382
397
|
"X-Frame-Options": "DENY",
|
383
|
-
"X-XSS-Protection": "1; mode=block"
|
398
|
+
"X-XSS-Protection": "1; mode=block",
|
384
399
|
}
|
385
|
-
|
400
|
+
|
386
401
|
self.middleware._apply_security_headers(mock_response, headers)
|
387
|
-
|
402
|
+
|
388
403
|
assert mock_response.headers["X-Content-Type-Options"] == "nosniff"
|
389
404
|
assert mock_response.headers["X-Frame-Options"] == "DENY"
|
390
405
|
assert mock_response.headers["X-XSS-Protection"] == "1; mode=block"
|
391
|
-
|
406
|
+
|
392
407
|
def test_create_error_response(self):
|
393
408
|
"""Test creating error response."""
|
394
409
|
result = self.middleware._create_error_response(400, "Bad request")
|
395
|
-
|
410
|
+
|
396
411
|
assert isinstance(result, JSONResponse)
|
397
412
|
assert result.status_code == 400
|
398
413
|
assert "Security violation" in result.body.decode()
|
399
414
|
assert "Bad request" in result.body.decode()
|
400
|
-
|
415
|
+
|
401
416
|
def test_rate_limit_response(self):
|
402
417
|
"""Test creating rate limit response."""
|
403
418
|
result = self.middleware._rate_limit_response()
|
404
|
-
|
419
|
+
|
405
420
|
assert isinstance(result, JSONResponse)
|
406
421
|
assert result.status_code == status.HTTP_429_TOO_MANY_REQUESTS
|
407
422
|
assert "Rate limit exceeded" in result.body.decode()
|
408
423
|
assert "Retry-After" in result.headers
|
409
|
-
|
424
|
+
|
410
425
|
def test_auth_error_response(self):
|
411
426
|
"""Test creating authentication error response."""
|
412
427
|
auth_result = AuthResult(
|
@@ -416,108 +431,108 @@ class TestFastAPISecurityMiddleware:
|
|
416
431
|
roles=[],
|
417
432
|
auth_method=AuthMethod.API_KEY,
|
418
433
|
error_code=-32005,
|
419
|
-
error_message="Invalid API key"
|
434
|
+
error_message="Invalid API key",
|
420
435
|
)
|
421
|
-
|
436
|
+
|
422
437
|
result = self.middleware._auth_error_response(auth_result)
|
423
|
-
|
438
|
+
|
424
439
|
assert isinstance(result, JSONResponse)
|
425
440
|
assert result.status_code == status.HTTP_401_UNAUTHORIZED
|
426
441
|
assert "Authentication failed" in result.body.decode()
|
427
442
|
assert "Invalid API key" in result.body.decode()
|
428
443
|
assert "WWW-Authenticate" in result.headers
|
429
|
-
|
444
|
+
|
430
445
|
def test_permission_error_response(self):
|
431
446
|
"""Test creating permission error response."""
|
432
447
|
result = self.middleware._permission_error_response()
|
433
|
-
|
448
|
+
|
434
449
|
assert isinstance(result, JSONResponse)
|
435
450
|
assert result.status_code == status.HTTP_403_FORBIDDEN
|
436
451
|
assert "Permission denied" in result.body.decode()
|
437
452
|
assert "Insufficient permissions" in result.body.decode()
|
438
|
-
|
453
|
+
|
439
454
|
def test_get_client_ip_from_forwarded_for(self):
|
440
455
|
"""Test getting client IP from X-Forwarded-For header."""
|
441
456
|
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
442
457
|
mock_request = self.create_mock_request(headers=headers)
|
443
|
-
|
458
|
+
|
444
459
|
result = self.middleware._get_client_ip(mock_request)
|
445
460
|
assert result == "192.168.1.1"
|
446
|
-
|
461
|
+
|
447
462
|
def test_get_client_ip_from_real_ip(self):
|
448
463
|
"""Test getting client IP from X-Real-IP header."""
|
449
464
|
headers = {"X-Real-IP": "192.168.1.100"}
|
450
465
|
mock_request = self.create_mock_request(headers=headers)
|
451
|
-
|
466
|
+
|
452
467
|
result = self.middleware._get_client_ip(mock_request)
|
453
468
|
assert result == "192.168.1.100"
|
454
|
-
|
469
|
+
|
455
470
|
def test_get_client_ip_from_client_host(self):
|
456
471
|
"""Test getting client IP from client host."""
|
457
472
|
mock_request = self.create_mock_request()
|
458
473
|
mock_request.client.host = "192.168.1.50"
|
459
|
-
|
474
|
+
|
460
475
|
result = self.middleware._get_client_ip(mock_request)
|
461
476
|
assert result == "192.168.1.50"
|
462
|
-
|
477
|
+
|
463
478
|
def test_get_client_ip_fallback(self):
|
464
479
|
"""Test getting client IP with fallback."""
|
465
480
|
mock_request = self.create_mock_request()
|
466
481
|
mock_request.client = None
|
467
|
-
|
482
|
+
|
468
483
|
result = self.middleware._get_client_ip(mock_request)
|
469
484
|
assert result == "127.0.0.1"
|
470
|
-
|
485
|
+
|
471
486
|
@pytest.mark.asyncio
|
472
487
|
async def test_call_with_http_exception(self):
|
473
488
|
"""Test middleware call that raises HTTPException."""
|
474
489
|
mock_request = self.create_mock_request()
|
475
490
|
mock_call_next = AsyncMock()
|
476
|
-
mock_call_next.side_effect = HTTPException(
|
477
|
-
|
491
|
+
mock_call_next.side_effect = HTTPException(
|
492
|
+
status_code=500, detail="Internal error"
|
493
|
+
)
|
494
|
+
|
478
495
|
# Mock successful authentication to reach call_next
|
479
496
|
auth_result = AuthResult(
|
480
497
|
is_valid=True,
|
481
498
|
status=AuthStatus.SUCCESS,
|
482
499
|
username="test_user",
|
483
500
|
roles=["user"],
|
484
|
-
auth_method=AuthMethod.API_KEY
|
501
|
+
auth_method=AuthMethod.API_KEY,
|
485
502
|
)
|
486
503
|
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
487
504
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
488
|
-
is_valid=True,
|
489
|
-
status=ValidationStatus.VALID
|
505
|
+
is_valid=True, status=ValidationStatus.VALID
|
490
506
|
)
|
491
|
-
|
507
|
+
|
492
508
|
with pytest.raises(HTTPException) as exc_info:
|
493
509
|
await self.middleware(mock_request, mock_call_next)
|
494
|
-
|
510
|
+
|
495
511
|
assert exc_info.value.status_code == 500
|
496
512
|
assert exc_info.value.detail == "Internal error"
|
497
|
-
|
513
|
+
|
498
514
|
@pytest.mark.asyncio
|
499
515
|
async def test_call_with_general_exception(self):
|
500
516
|
"""Test middleware call with general exception."""
|
501
517
|
mock_request = self.create_mock_request()
|
502
518
|
mock_call_next = AsyncMock()
|
503
519
|
mock_call_next.side_effect = Exception("General error")
|
504
|
-
|
520
|
+
|
505
521
|
# Mock successful authentication to reach call_next
|
506
522
|
auth_result = AuthResult(
|
507
523
|
is_valid=True,
|
508
524
|
status=AuthStatus.SUCCESS,
|
509
525
|
username="test_user",
|
510
526
|
roles=["user"],
|
511
|
-
auth_method=AuthMethod.API_KEY
|
527
|
+
auth_method=AuthMethod.API_KEY,
|
512
528
|
)
|
513
529
|
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
514
530
|
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
515
|
-
is_valid=True,
|
516
|
-
status=ValidationStatus.VALID
|
531
|
+
is_valid=True, status=ValidationStatus.VALID
|
517
532
|
)
|
518
|
-
|
533
|
+
|
519
534
|
with pytest.raises(FastAPIMiddlewareError) as exc_info:
|
520
535
|
await self.middleware(mock_request, mock_call_next)
|
521
|
-
|
536
|
+
|
522
537
|
assert "Middleware processing failed" in str(exc_info.value)
|
523
538
|
assert exc_info.value.error_code == -32003
|