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
@@ -17,25 +17,38 @@ Version: 1.0.0
|
|
17
17
|
License: MIT
|
18
18
|
"""
|
19
19
|
|
20
|
-
import os
|
21
20
|
import json
|
22
|
-
import
|
21
|
+
import os
|
23
22
|
import tempfile
|
24
|
-
from unittest.mock import Mock, patch
|
23
|
+
from unittest.mock import MagicMock, Mock, patch
|
24
|
+
|
25
|
+
import pytest
|
25
26
|
from flask.testing import FlaskClient
|
26
27
|
|
27
28
|
from mcp_security_framework.examples.flask_example import FlaskExample
|
28
|
-
from mcp_security_framework.schemas.
|
29
|
-
|
29
|
+
from mcp_security_framework.schemas.config import (
|
30
|
+
AuthConfig,
|
31
|
+
PermissionConfig,
|
32
|
+
RateLimitConfig,
|
33
|
+
SecurityConfig,
|
34
|
+
SSLConfig,
|
35
|
+
)
|
36
|
+
from mcp_security_framework.schemas.models import (
|
37
|
+
AuthMethod,
|
38
|
+
AuthResult,
|
39
|
+
AuthStatus,
|
40
|
+
ValidationResult,
|
41
|
+
ValidationStatus,
|
42
|
+
)
|
30
43
|
|
31
44
|
|
32
45
|
class TestFlaskExample:
|
33
46
|
"""Test suite for Flask example implementation."""
|
34
|
-
|
47
|
+
|
35
48
|
def setup_method(self):
|
36
49
|
"""Set up test fixtures before each test method."""
|
37
50
|
self.temp_dir = tempfile.mkdtemp()
|
38
|
-
|
51
|
+
|
39
52
|
# Create test configuration
|
40
53
|
self.test_config = {
|
41
54
|
"environment": "test",
|
@@ -46,16 +59,16 @@ class TestFlaskExample:
|
|
46
59
|
"methods": ["api_key", "jwt", "certificate"],
|
47
60
|
"api_keys": {
|
48
61
|
"admin_key_123": {"username": "admin", "roles": ["admin"]},
|
49
|
-
"user_key_456": {"username": "user", "roles": ["user"]}
|
62
|
+
"user_key_456": {"username": "user", "roles": ["user"]},
|
50
63
|
},
|
51
64
|
"jwt_secret": "test-super-secret-jwt-key-for-testing-purposes-only",
|
52
65
|
"jwt_algorithm": "HS256",
|
53
|
-
"jwt_expiry_hours": 24
|
66
|
+
"jwt_expiry_hours": 24,
|
54
67
|
},
|
55
68
|
"permissions": {
|
56
69
|
"enabled": True,
|
57
70
|
"roles_file": "test_roles.json",
|
58
|
-
"default_role": "user"
|
71
|
+
"default_role": "user",
|
59
72
|
},
|
60
73
|
"ssl": {
|
61
74
|
"enabled": False,
|
@@ -63,61 +76,68 @@ class TestFlaskExample:
|
|
63
76
|
"key_file": None,
|
64
77
|
"ca_cert_file": None,
|
65
78
|
"verify_mode": "CERT_NONE",
|
66
|
-
"min_version": "TLSv1.2"
|
79
|
+
"min_version": "TLSv1.2",
|
67
80
|
},
|
68
81
|
"certificates": {
|
69
82
|
"enabled": False,
|
70
83
|
"ca_cert_path": None,
|
71
84
|
"ca_key_path": None,
|
72
|
-
"cert_output_dir": None
|
85
|
+
"cert_output_dir": None,
|
73
86
|
},
|
74
87
|
"rate_limit": {
|
75
88
|
"enabled": True,
|
76
89
|
"requests_per_minute": 100,
|
77
90
|
"burst_limit": 10,
|
78
|
-
"window_seconds": 60
|
79
|
-
}
|
91
|
+
"window_seconds": 60,
|
92
|
+
},
|
80
93
|
}
|
81
|
-
|
94
|
+
|
82
95
|
def teardown_method(self):
|
83
96
|
"""Clean up after each test method."""
|
84
97
|
import shutil
|
98
|
+
|
85
99
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
86
|
-
|
100
|
+
|
87
101
|
def _create_config_file(self) -> str:
|
88
102
|
"""Create temporary configuration file for testing."""
|
89
103
|
config_file = os.path.join(self.temp_dir, "test_config.json")
|
90
|
-
with open(config_file,
|
104
|
+
with open(config_file, "w") as f:
|
91
105
|
json.dump(self.test_config, f)
|
92
106
|
return config_file
|
93
|
-
|
107
|
+
|
94
108
|
def test_flask_example_initialization(self):
|
95
109
|
"""Test Flask example initialization."""
|
96
110
|
config_file = self._create_config_file()
|
97
111
|
example = FlaskExample(config_path=config_file)
|
98
|
-
|
112
|
+
|
99
113
|
# Assertions
|
100
114
|
assert example is not None
|
101
115
|
assert example.app is not None
|
102
116
|
assert example.config is not None
|
103
117
|
assert example.security_manager is not None
|
104
|
-
|
118
|
+
|
105
119
|
def test_flask_example_health_endpoint(self):
|
106
120
|
"""Test health check endpoint."""
|
107
121
|
config_file = self._create_config_file()
|
108
122
|
example = FlaskExample(config_path=config_file)
|
109
123
|
client = example.app.test_client()
|
110
|
-
|
124
|
+
|
111
125
|
# Test health endpoint
|
112
126
|
response = client.get("/health")
|
113
|
-
|
127
|
+
|
114
128
|
# Assertions
|
115
129
|
assert response.status_code == 200
|
116
130
|
assert response.json["status"] == "healthy"
|
117
|
-
|
118
|
-
@patch(
|
119
|
-
|
120
|
-
|
131
|
+
|
132
|
+
@patch(
|
133
|
+
"mcp_security_framework.core.security_manager.SecurityManager.authenticate_user"
|
134
|
+
)
|
135
|
+
@patch(
|
136
|
+
"mcp_security_framework.core.security_manager.SecurityManager.check_permissions"
|
137
|
+
)
|
138
|
+
def test_flask_example_protected_endpoint_with_api_key(
|
139
|
+
self, mock_check_permissions, mock_authenticate
|
140
|
+
):
|
121
141
|
"""Test protected endpoint with API key authentication."""
|
122
142
|
# Mock authentication
|
123
143
|
mock_authenticate.return_value = AuthResult(
|
@@ -125,47 +145,51 @@ class TestFlaskExample:
|
|
125
145
|
status=AuthStatus.SUCCESS,
|
126
146
|
username="admin",
|
127
147
|
roles=["admin"],
|
128
|
-
auth_method=AuthMethod.API_KEY
|
148
|
+
auth_method=AuthMethod.API_KEY,
|
129
149
|
)
|
130
|
-
|
150
|
+
|
131
151
|
# Mock permission check
|
132
152
|
mock_check_permissions.return_value = ValidationResult(
|
133
|
-
is_valid=True,
|
134
|
-
status=ValidationStatus.VALID
|
153
|
+
is_valid=True, status=ValidationStatus.VALID
|
135
154
|
)
|
136
|
-
|
155
|
+
|
137
156
|
# Create example
|
138
157
|
config_file = self._create_config_file()
|
139
158
|
example = FlaskExample(config_path=config_file)
|
140
159
|
client = example.app.test_client()
|
141
|
-
|
160
|
+
|
142
161
|
# Test protected endpoint
|
143
162
|
response = client.get(
|
144
|
-
"/api/v1/users/me",
|
145
|
-
headers={"X-API-Key": "admin_key_123"}
|
163
|
+
"/api/v1/users/me", headers={"X-API-Key": "admin_key_123"}
|
146
164
|
)
|
147
|
-
|
165
|
+
|
148
166
|
# Assertions
|
149
167
|
assert response.status_code == 200
|
150
168
|
assert "username" in response.json
|
151
|
-
|
169
|
+
|
152
170
|
def test_flask_example_protected_endpoint_unauthorized(self):
|
153
171
|
"""Test protected endpoint without authentication."""
|
154
172
|
# Create example
|
155
173
|
config_file = self._create_config_file()
|
156
174
|
example = FlaskExample(config_path=config_file)
|
157
175
|
client = example.app.test_client()
|
158
|
-
|
176
|
+
|
159
177
|
# Test protected endpoint without auth
|
160
178
|
response = client.get("/api/v1/users/me")
|
161
|
-
|
179
|
+
|
162
180
|
# Assertions
|
163
181
|
assert response.status_code == 401
|
164
|
-
|
165
|
-
@patch(
|
166
|
-
|
167
|
-
|
168
|
-
|
182
|
+
|
183
|
+
@patch(
|
184
|
+
"mcp_security_framework.core.security_manager.SecurityManager.authenticate_user"
|
185
|
+
)
|
186
|
+
@patch(
|
187
|
+
"mcp_security_framework.core.security_manager.SecurityManager.check_permissions"
|
188
|
+
)
|
189
|
+
@patch("mcp_security_framework.core.rate_limiter.RateLimiter.check_rate_limit")
|
190
|
+
def test_flask_example_rate_limiting(
|
191
|
+
self, mock_check_rate_limit, mock_check_permissions, mock_authenticate
|
192
|
+
):
|
169
193
|
"""Test rate limiting functionality."""
|
170
194
|
# Mock authentication
|
171
195
|
mock_authenticate.return_value = AuthResult(
|
@@ -173,57 +197,54 @@ class TestFlaskExample:
|
|
173
197
|
status=AuthStatus.SUCCESS,
|
174
198
|
username="user",
|
175
199
|
roles=["user"],
|
176
|
-
auth_method=AuthMethod.API_KEY
|
200
|
+
auth_method=AuthMethod.API_KEY,
|
177
201
|
)
|
178
|
-
|
202
|
+
|
179
203
|
# Mock permission check
|
180
204
|
mock_check_permissions.return_value = ValidationResult(
|
181
|
-
is_valid=True,
|
182
|
-
status=ValidationStatus.VALID
|
205
|
+
is_valid=True, status=ValidationStatus.VALID
|
183
206
|
)
|
184
|
-
|
207
|
+
|
185
208
|
# Mock rate limiting - first 100 requests allowed, then blocked
|
186
209
|
request_count = 0
|
210
|
+
|
187
211
|
def mock_rate_limit(identifier):
|
188
212
|
nonlocal request_count
|
189
213
|
request_count += 1
|
190
214
|
return request_count <= 100
|
191
|
-
|
215
|
+
|
192
216
|
mock_check_rate_limit.side_effect = mock_rate_limit
|
193
|
-
|
217
|
+
|
194
218
|
# Create example
|
195
219
|
config_file = self._create_config_file()
|
196
220
|
example = FlaskExample(config_path=config_file)
|
197
221
|
client = example.app.test_client()
|
198
|
-
|
222
|
+
|
199
223
|
# Test rate limiting
|
200
|
-
response = client.get(
|
201
|
-
|
202
|
-
headers={"X-API-Key": "user_key_456"}
|
203
|
-
)
|
204
|
-
|
224
|
+
response = client.get("/api/v1/users/me", headers={"X-API-Key": "user_key_456"})
|
225
|
+
|
205
226
|
# Assertions
|
206
227
|
assert response.status_code == 200
|
207
|
-
|
228
|
+
|
208
229
|
def test_flask_example_ssl_configuration(self):
|
209
230
|
"""Test SSL configuration."""
|
210
231
|
# SSL configuration
|
211
232
|
ssl_config = self.test_config.copy()
|
212
|
-
ssl_config["ssl"] = {
|
213
|
-
|
214
|
-
}
|
215
|
-
|
233
|
+
ssl_config["ssl"] = {"enabled": False}
|
234
|
+
|
216
235
|
# Create example
|
217
236
|
config_file = os.path.join(self.temp_dir, "ssl_config.json")
|
218
|
-
with open(config_file,
|
237
|
+
with open(config_file, "w") as f:
|
219
238
|
json.dump(ssl_config, f)
|
220
|
-
|
239
|
+
|
221
240
|
example = FlaskExample(config_path=config_file)
|
222
|
-
|
241
|
+
|
223
242
|
# Assertions
|
224
243
|
assert example.app is not None
|
225
|
-
|
226
|
-
@patch(
|
244
|
+
|
245
|
+
@patch(
|
246
|
+
"mcp_security_framework.core.security_manager.SecurityManager.authenticate_user"
|
247
|
+
)
|
227
248
|
def test_flask_example_error_handling(self, mock_authenticate):
|
228
249
|
"""Test error handling."""
|
229
250
|
# Mock authentication failure
|
@@ -234,75 +255,78 @@ class TestFlaskExample:
|
|
234
255
|
roles=[],
|
235
256
|
auth_method=None,
|
236
257
|
error_code=-32002,
|
237
|
-
error_message="Authentication failed"
|
258
|
+
error_message="Authentication failed",
|
238
259
|
)
|
239
|
-
|
260
|
+
|
240
261
|
# Create example
|
241
262
|
config_file = self._create_config_file()
|
242
263
|
example = FlaskExample(config_path=config_file)
|
243
264
|
client = example.app.test_client()
|
244
|
-
|
265
|
+
|
245
266
|
# Test error handling
|
246
|
-
response = client.get(
|
247
|
-
|
248
|
-
headers={"X-API-Key": "invalid_key"}
|
249
|
-
)
|
250
|
-
|
267
|
+
response = client.get("/api/v1/users/me", headers={"X-API-Key": "invalid_key"})
|
268
|
+
|
251
269
|
# Assertions
|
252
270
|
assert response.status_code == 401
|
253
|
-
|
271
|
+
|
254
272
|
def test_flask_example_metrics_endpoint(self):
|
255
273
|
"""Test metrics endpoint."""
|
256
274
|
# Create example
|
257
275
|
config_file = self._create_config_file()
|
258
276
|
example = FlaskExample(config_path=config_file)
|
259
277
|
client = example.app.test_client()
|
260
|
-
|
278
|
+
|
261
279
|
# Test metrics endpoint
|
262
280
|
response = client.get("/metrics")
|
263
|
-
|
281
|
+
|
264
282
|
# Assertions
|
265
283
|
assert response.status_code == 200
|
266
284
|
assert "requests_total" in response.text
|
267
|
-
|
285
|
+
|
268
286
|
def test_flask_example_run_method(self):
|
269
287
|
"""Test Flask example run method."""
|
270
288
|
# Create example
|
271
289
|
config_file = self._create_config_file()
|
272
290
|
example = FlaskExample(config_path=config_file)
|
273
|
-
|
291
|
+
|
274
292
|
# Test run method (should not raise exception)
|
275
293
|
try:
|
276
294
|
# This would normally start a server, but we're just testing the method exists
|
277
|
-
assert hasattr(example,
|
295
|
+
assert hasattr(example, "run")
|
278
296
|
except Exception as e:
|
279
297
|
# Expected behavior - server can't start in test environment
|
280
298
|
pass
|
281
|
-
|
299
|
+
|
282
300
|
def test_flask_example_default_config(self):
|
283
301
|
"""Test Flask example with default configuration."""
|
284
302
|
# Use configuration with SSL disabled to avoid certificate file issues
|
285
303
|
config_file = self._create_config_file()
|
286
304
|
example = FlaskExample(config_path=config_file)
|
287
|
-
|
305
|
+
|
288
306
|
# Assertions
|
289
307
|
assert example is not None
|
290
308
|
assert example.app is not None
|
291
309
|
assert example.config is not None
|
292
|
-
|
310
|
+
|
293
311
|
def test_flask_example_config_loading(self):
|
294
312
|
"""Test configuration loading from file."""
|
295
313
|
config_file = self._create_config_file()
|
296
314
|
example = FlaskExample(config_path=config_file)
|
297
|
-
|
315
|
+
|
298
316
|
# Assertions
|
299
317
|
assert example.config.environment == "test"
|
300
318
|
assert example.config.auth.enabled is True
|
301
319
|
assert example.config.ssl.enabled is False
|
302
|
-
|
303
|
-
@patch(
|
304
|
-
|
305
|
-
|
320
|
+
|
321
|
+
@patch(
|
322
|
+
"mcp_security_framework.core.security_manager.SecurityManager.authenticate_user"
|
323
|
+
)
|
324
|
+
@patch(
|
325
|
+
"mcp_security_framework.core.security_manager.SecurityManager.check_permissions"
|
326
|
+
)
|
327
|
+
def test_flask_example_jwt_authentication(
|
328
|
+
self, mock_check_permissions, mock_authenticate_jwt
|
329
|
+
):
|
306
330
|
"""Test JWT token authentication."""
|
307
331
|
# Mock JWT authentication
|
308
332
|
mock_authenticate_jwt.return_value = AuthResult(
|
@@ -310,47 +334,46 @@ class TestFlaskExample:
|
|
310
334
|
status=AuthStatus.SUCCESS,
|
311
335
|
username="user",
|
312
336
|
roles=["user"],
|
313
|
-
auth_method=AuthMethod.JWT
|
337
|
+
auth_method=AuthMethod.JWT,
|
314
338
|
)
|
315
|
-
|
339
|
+
|
316
340
|
# Mock permission check
|
317
341
|
mock_check_permissions.return_value = ValidationResult(
|
318
|
-
is_valid=True,
|
319
|
-
status=ValidationStatus.VALID
|
342
|
+
is_valid=True, status=ValidationStatus.VALID
|
320
343
|
)
|
321
|
-
|
344
|
+
|
322
345
|
# Create example
|
323
346
|
config_file = self._create_config_file()
|
324
347
|
example = FlaskExample(config_path=config_file)
|
325
348
|
client = example.app.test_client()
|
326
|
-
|
349
|
+
|
327
350
|
# Test JWT authentication - use a public endpoint that doesn't require auth
|
328
351
|
response = client.get("/health")
|
329
|
-
|
352
|
+
|
330
353
|
# Assertions
|
331
354
|
assert response.status_code == 200
|
332
|
-
|
355
|
+
|
333
356
|
def test_flask_example_cors_configuration(self):
|
334
357
|
"""Test CORS configuration."""
|
335
358
|
config_file = self._create_config_file()
|
336
359
|
example = FlaskExample(config_path=config_file)
|
337
360
|
client = example.app.test_client()
|
338
|
-
|
361
|
+
|
339
362
|
# Test CORS headers - use GET request instead of OPTIONS
|
340
363
|
response = client.get("/health")
|
341
|
-
|
364
|
+
|
342
365
|
# Assertions
|
343
366
|
assert response.status_code == 200
|
344
|
-
|
367
|
+
|
345
368
|
def test_flask_example_security_headers(self):
|
346
369
|
"""Test security headers configuration."""
|
347
370
|
config_file = self._create_config_file()
|
348
371
|
example = FlaskExample(config_path=config_file)
|
349
372
|
client = example.app.test_client()
|
350
|
-
|
373
|
+
|
351
374
|
# Test security headers
|
352
375
|
response = client.get("/health")
|
353
|
-
|
376
|
+
|
354
377
|
# Assertions
|
355
378
|
assert response.status_code == 200
|
356
379
|
# Check that response has headers (basic check)
|