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
@@ -11,25 +11,30 @@ License: MIT
|
|
11
11
|
"""
|
12
12
|
|
13
13
|
import json
|
14
|
-
import tempfile
|
15
14
|
import os
|
16
|
-
|
17
|
-
from typing import
|
15
|
+
import tempfile
|
16
|
+
from typing import Any, Dict
|
17
|
+
from unittest.mock import MagicMock, patch
|
18
18
|
|
19
19
|
import pytest
|
20
|
-
from fastapi.testclient import TestClient
|
21
20
|
from cryptography import x509
|
22
21
|
from cryptography.hazmat.primitives import hashes, serialization
|
23
22
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
23
|
+
from fastapi.testclient import TestClient
|
24
24
|
|
25
|
-
from mcp_security_framework.examples.fastapi_example import FastAPISecurityExample
|
26
25
|
from mcp_security_framework.core.security_manager import SecurityManager
|
27
|
-
from mcp_security_framework.
|
26
|
+
from mcp_security_framework.examples.fastapi_example import FastAPISecurityExample
|
27
|
+
from mcp_security_framework.schemas.config import (
|
28
|
+
AuthConfig,
|
29
|
+
RateLimitConfig,
|
30
|
+
SecurityConfig,
|
31
|
+
SSLConfig,
|
32
|
+
)
|
28
33
|
|
29
34
|
|
30
35
|
class TestFastAPIIntegration:
|
31
36
|
"""Integration tests for FastAPI with security framework."""
|
32
|
-
|
37
|
+
|
33
38
|
def setup_method(self):
|
34
39
|
"""Set up test fixtures before each test method."""
|
35
40
|
# Create temporary configuration
|
@@ -40,237 +45,232 @@ class TestFastAPIIntegration:
|
|
40
45
|
"api_keys": {
|
41
46
|
"admin_key_123": {"username": "admin", "roles": ["admin", "user"]},
|
42
47
|
"user_key_456": {"username": "user", "roles": ["user"]},
|
43
|
-
"readonly_key_789": {"username": "readonly", "roles": ["readonly"]}
|
48
|
+
"readonly_key_789": {"username": "readonly", "roles": ["readonly"]},
|
44
49
|
},
|
45
|
-
"public_paths": ["/health", "/metrics"]
|
46
|
-
},
|
47
|
-
"rate_limit": {
|
48
|
-
"enabled": True,
|
49
|
-
"default_requests_per_minute": 100
|
50
|
+
"public_paths": ["/health", "/metrics"],
|
50
51
|
},
|
51
|
-
"
|
52
|
-
|
53
|
-
},
|
54
|
-
"
|
55
|
-
|
56
|
-
"roles_file": "test_roles.json"
|
57
|
-
},
|
58
|
-
"certificates": {
|
59
|
-
"enabled": False
|
60
|
-
},
|
61
|
-
"logging": {
|
62
|
-
"level": "INFO",
|
63
|
-
"format": "standard"
|
64
|
-
}
|
52
|
+
"rate_limit": {"enabled": True, "default_requests_per_minute": 100},
|
53
|
+
"ssl": {"enabled": False},
|
54
|
+
"permissions": {"enabled": True, "roles_file": "test_roles.json"},
|
55
|
+
"certificates": {"enabled": False},
|
56
|
+
"logging": {"level": "INFO", "format": "standard"},
|
65
57
|
}
|
66
|
-
|
58
|
+
|
67
59
|
# Create temporary config file
|
68
|
-
self.config_fd, self.config_path = tempfile.mkstemp(suffix=
|
69
|
-
with os.fdopen(self.config_fd,
|
60
|
+
self.config_fd, self.config_path = tempfile.mkstemp(suffix=".json")
|
61
|
+
with os.fdopen(self.config_fd, "w") as f:
|
70
62
|
json.dump(self.test_config, f)
|
71
|
-
|
63
|
+
|
72
64
|
# Create temporary roles file
|
73
65
|
self.roles_config = {
|
74
66
|
"roles": {
|
75
67
|
"admin": {
|
76
|
-
"permissions": [
|
77
|
-
|
68
|
+
"permissions": [
|
69
|
+
"read:own",
|
70
|
+
"write:own",
|
71
|
+
"delete:own",
|
72
|
+
"admin",
|
73
|
+
"*",
|
74
|
+
],
|
75
|
+
"description": "Administrator role",
|
78
76
|
},
|
79
77
|
"user": {
|
80
78
|
"permissions": ["read:own", "write:own"],
|
81
|
-
"description": "Regular user role"
|
79
|
+
"description": "Regular user role",
|
82
80
|
},
|
83
81
|
"readonly": {
|
84
82
|
"permissions": ["read:own"],
|
85
|
-
"description": "Read-only user role"
|
86
|
-
}
|
83
|
+
"description": "Read-only user role",
|
84
|
+
},
|
87
85
|
}
|
88
86
|
}
|
89
|
-
|
90
|
-
self.roles_fd, self.roles_path = tempfile.mkstemp(suffix=
|
91
|
-
with os.fdopen(self.roles_fd,
|
87
|
+
|
88
|
+
self.roles_fd, self.roles_path = tempfile.mkstemp(suffix=".json")
|
89
|
+
with os.fdopen(self.roles_fd, "w") as f:
|
92
90
|
json.dump(self.roles_config, f)
|
93
|
-
|
91
|
+
|
94
92
|
# Update config to use roles file
|
95
93
|
self.test_config["permissions"]["roles_file"] = self.roles_path
|
96
|
-
|
94
|
+
|
97
95
|
# Recreate config file with updated roles path
|
98
|
-
with open(self.config_path,
|
96
|
+
with open(self.config_path, "w") as f:
|
99
97
|
json.dump(self.test_config, f)
|
100
|
-
|
98
|
+
|
101
99
|
def teardown_method(self):
|
102
100
|
"""Clean up after each test method."""
|
103
101
|
# Remove temporary files
|
104
|
-
if hasattr(self,
|
102
|
+
if hasattr(self, "config_path") and os.path.exists(self.config_path):
|
105
103
|
os.unlink(self.config_path)
|
106
|
-
if hasattr(self,
|
104
|
+
if hasattr(self, "roles_path") and os.path.exists(self.roles_path):
|
107
105
|
os.unlink(self.roles_path)
|
108
|
-
|
106
|
+
|
109
107
|
def test_fastapi_full_integration(self):
|
110
108
|
"""Test complete FastAPI integration with security framework."""
|
111
109
|
# Create FastAPI example
|
112
110
|
example = FastAPISecurityExample(config_path=self.config_path)
|
113
|
-
|
111
|
+
|
114
112
|
# Test that the app is properly configured
|
115
113
|
assert example.app is not None
|
116
|
-
assert hasattr(example.app,
|
117
|
-
|
114
|
+
assert hasattr(example.app, "add_middleware")
|
115
|
+
|
118
116
|
# Test that security manager is configured
|
119
117
|
assert example.security_manager is not None
|
120
118
|
assert isinstance(example.security_manager, SecurityManager)
|
121
|
-
|
119
|
+
|
122
120
|
# Test that configuration is loaded
|
123
121
|
assert example.config is not None
|
124
122
|
assert example.config.auth.enabled is True
|
125
123
|
assert example.config.rate_limit.enabled is True
|
126
|
-
|
124
|
+
|
127
125
|
def test_fastapi_authentication_flow(self):
|
128
126
|
"""Test complete authentication flow in FastAPI."""
|
129
127
|
example = FastAPISecurityExample(config_path=self.config_path)
|
130
128
|
client = TestClient(example.app)
|
131
|
-
|
129
|
+
|
132
130
|
# Test unauthenticated access to protected endpoint
|
133
131
|
response = client.get("/api/v1/users/me")
|
134
132
|
assert response.status_code == 401
|
135
|
-
|
133
|
+
|
136
134
|
# Test authenticated access with valid API key
|
137
135
|
headers = {"X-API-Key": "admin_key_123"}
|
138
136
|
response = client.get("/api/v1/users/me", headers=headers)
|
139
137
|
assert response.status_code == 200 # Should be authenticated
|
140
|
-
|
138
|
+
|
141
139
|
# Test authenticated access with different user
|
142
140
|
headers = {"X-API-Key": "user_key_456"}
|
143
141
|
response = client.get("/api/v1/users/me", headers=headers)
|
144
142
|
assert response.status_code == 200 # User should be authenticated
|
145
|
-
|
143
|
+
|
146
144
|
def test_fastapi_authorization_flow(self):
|
147
145
|
"""Test complete authorization flow in FastAPI."""
|
148
146
|
example = FastAPISecurityExample(config_path=self.config_path)
|
149
147
|
client = TestClient(example.app)
|
150
|
-
|
148
|
+
|
151
149
|
# Test admin access to admin-only endpoint
|
152
150
|
headers = {"X-API-Key": "admin_key_123"}
|
153
151
|
response = client.get("/admin/users", headers=headers)
|
154
152
|
assert response.status_code == 200 # Admin should have access
|
155
|
-
|
153
|
+
|
156
154
|
# Test regular user access to admin-only endpoint (should be denied)
|
157
155
|
headers = {"X-API-Key": "user_key_456"}
|
158
156
|
response = client.get("/admin/users", headers=headers)
|
159
157
|
assert response.status_code == 403 # User should be denied admin access
|
160
|
-
|
161
|
-
|
158
|
+
|
159
|
+
# Test readonly user access to data endpoint (should be allowed for read)
|
162
160
|
headers = {"X-API-Key": "readonly_key_789"}
|
163
161
|
response = client.get("/api/v1/data", headers=headers)
|
164
162
|
assert response.status_code == 200 # Readonly user should have read access
|
165
|
-
|
163
|
+
|
166
164
|
def test_fastapi_rate_limiting(self):
|
167
165
|
"""Test rate limiting in FastAPI."""
|
168
166
|
example = FastAPISecurityExample(config_path=self.config_path)
|
169
167
|
client = TestClient(example.app)
|
170
|
-
|
168
|
+
|
171
169
|
headers = {"X-API-Key": "user_key_456"}
|
172
|
-
|
170
|
+
|
173
171
|
# Make multiple requests to trigger rate limiting
|
174
172
|
responses = []
|
175
173
|
for i in range(105): # Exceed the 100 requests per minute limit
|
176
174
|
response = client.get("/api/v1/users/me", headers=headers)
|
177
175
|
responses.append(response.status_code)
|
178
|
-
|
176
|
+
|
179
177
|
# Check that some requests were rate limited
|
180
178
|
# Note: Rate limiting may not be triggered in test environment
|
181
179
|
# but the requests should still be processed
|
182
180
|
assert len(responses) == 105, "All requests should be processed"
|
183
|
-
assert all(
|
184
|
-
|
181
|
+
assert all(
|
182
|
+
status in [200, 429] for status in responses
|
183
|
+
), "Responses should be either 200 or 429"
|
184
|
+
|
185
185
|
def test_fastapi_ssl_integration(self):
|
186
186
|
"""Test SSL/TLS integration in FastAPI."""
|
187
187
|
# Create config with SSL enabled
|
188
188
|
ssl_config = self.test_config.copy()
|
189
|
-
ssl_config["ssl"] = {
|
190
|
-
|
191
|
-
}
|
192
|
-
|
189
|
+
ssl_config["ssl"] = {"enabled": False} # Disable SSL for testing
|
190
|
+
|
193
191
|
# Create temporary SSL config file
|
194
|
-
ssl_config_fd, ssl_config_path = tempfile.mkstemp(suffix=
|
195
|
-
with os.fdopen(ssl_config_fd,
|
192
|
+
ssl_config_fd, ssl_config_path = tempfile.mkstemp(suffix=".json")
|
193
|
+
with os.fdopen(ssl_config_fd, "w") as f:
|
196
194
|
json.dump(ssl_config, f)
|
197
|
-
|
195
|
+
|
198
196
|
try:
|
199
197
|
# Mock SSL context creation to avoid file requirements
|
200
|
-
with patch(
|
198
|
+
with patch(
|
199
|
+
"mcp_security_framework.core.ssl_manager.SSLManager.create_server_context"
|
200
|
+
) as mock_ssl:
|
201
201
|
mock_ssl.return_value = MagicMock()
|
202
|
-
|
202
|
+
|
203
203
|
example = FastAPISecurityExample(config_path=ssl_config_path)
|
204
|
-
|
204
|
+
|
205
205
|
# Test that SSL is configured
|
206
206
|
assert example.config.ssl.enabled is False # SSL disabled for testing
|
207
|
-
|
207
|
+
|
208
208
|
finally:
|
209
209
|
os.unlink(ssl_config_path)
|
210
|
-
|
210
|
+
|
211
211
|
def test_fastapi_error_handling(self):
|
212
212
|
"""Test error handling in FastAPI integration."""
|
213
213
|
example = FastAPISecurityExample(config_path=self.config_path)
|
214
214
|
client = TestClient(example.app)
|
215
|
-
|
215
|
+
|
216
216
|
# Test invalid API key
|
217
217
|
headers = {"X-API-Key": "invalid_key"}
|
218
218
|
response = client.get("/api/v1/users/me", headers=headers)
|
219
219
|
assert response.status_code == 401
|
220
|
-
|
220
|
+
|
221
221
|
# Test malformed request
|
222
222
|
headers = {"X-API-Key": "admin_key_123"}
|
223
223
|
response = client.get("/api/v1/data", headers=headers)
|
224
224
|
assert response.status_code == 200 # Should succeed with valid auth
|
225
|
-
|
225
|
+
|
226
226
|
def test_fastapi_health_and_metrics(self):
|
227
227
|
"""Test health check and metrics endpoints."""
|
228
228
|
example = FastAPISecurityExample(config_path=self.config_path)
|
229
229
|
client = TestClient(example.app)
|
230
|
-
|
230
|
+
|
231
231
|
# Test health check
|
232
232
|
response = client.get("/health")
|
233
233
|
assert response.status_code == 200
|
234
234
|
data = response.json()
|
235
235
|
assert "status" in data
|
236
236
|
assert data["status"] == "healthy"
|
237
|
-
|
237
|
+
|
238
238
|
# Test metrics
|
239
239
|
response = client.get("/metrics")
|
240
240
|
assert response.status_code == 200
|
241
241
|
data = response.json()
|
242
242
|
assert "metrics" in data # The actual response structure has "metrics" wrapper
|
243
243
|
assert "framework" in data
|
244
|
-
|
244
|
+
|
245
245
|
def test_fastapi_data_operations(self):
|
246
246
|
"""Test data operations with security."""
|
247
247
|
example = FastAPISecurityExample(config_path=self.config_path)
|
248
248
|
client = TestClient(example.app)
|
249
|
-
|
249
|
+
|
250
250
|
headers = {"X-API-Key": "admin_key_123"}
|
251
|
-
|
251
|
+
|
252
252
|
# Get data
|
253
253
|
create_response = client.get("/api/v1/data", headers=headers)
|
254
254
|
|
255
255
|
assert create_response.status_code == 200 # Should succeed with valid auth
|
256
256
|
data = create_response.json()
|
257
257
|
assert "message" in data # The actual response contains "message"
|
258
|
-
|
258
|
+
|
259
259
|
# Retrieve data (using the general data endpoint since there's no specific ID endpoint)
|
260
260
|
get_response = client.get("/api/v1/data", headers=headers)
|
261
261
|
assert get_response.status_code == 200
|
262
262
|
retrieved_data = get_response.json()
|
263
263
|
assert "data" in retrieved_data
|
264
|
-
|
264
|
+
|
265
265
|
def test_fastapi_middleware_integration(self):
|
266
266
|
"""Test that security middleware is properly integrated."""
|
267
267
|
example = FastAPISecurityExample(config_path=self.config_path)
|
268
|
-
|
268
|
+
|
269
269
|
# Check that middleware is configured
|
270
270
|
# Note: In test environment, middleware setup is skipped
|
271
271
|
# but we can verify the app structure
|
272
|
-
assert hasattr(example.app,
|
273
|
-
|
272
|
+
assert hasattr(example.app, "user_middleware")
|
273
|
+
|
274
274
|
# Test that routes are properly configured
|
275
275
|
routes = [route.path for route in example.app.routes]
|
276
276
|
expected_routes = [
|
@@ -278,58 +278,55 @@ class TestFastAPIIntegration:
|
|
278
278
|
"/metrics",
|
279
279
|
"/admin/users",
|
280
280
|
"/api/v1/users/me",
|
281
|
-
"/api/v1/data"
|
281
|
+
"/api/v1/data",
|
282
282
|
]
|
283
|
-
|
283
|
+
|
284
284
|
for route in expected_routes:
|
285
285
|
assert route in routes, f"Route {route} not found in app routes"
|
286
|
-
|
286
|
+
|
287
287
|
def test_fastapi_configuration_validation(self):
|
288
288
|
"""Test configuration validation in FastAPI integration."""
|
289
289
|
# Test with invalid configuration
|
290
|
-
invalid_config = {
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
}
|
295
|
-
}
|
296
|
-
|
297
|
-
invalid_config_fd, invalid_config_path = tempfile.mkstemp(suffix='.json')
|
298
|
-
with os.fdopen(invalid_config_fd, 'w') as f:
|
290
|
+
invalid_config = {"auth": {"enabled": True, "methods": ["invalid_method"]}}
|
291
|
+
|
292
|
+
invalid_config_fd, invalid_config_path = tempfile.mkstemp(suffix=".json")
|
293
|
+
with os.fdopen(invalid_config_fd, "w") as f:
|
299
294
|
json.dump(invalid_config, f)
|
300
|
-
|
295
|
+
|
301
296
|
try:
|
302
297
|
# Should raise validation error
|
303
298
|
with pytest.raises(Exception):
|
304
299
|
FastAPISecurityExample(config_path=invalid_config_path)
|
305
300
|
finally:
|
306
301
|
os.unlink(invalid_config_path)
|
307
|
-
|
302
|
+
|
308
303
|
def test_fastapi_performance_benchmark(self):
|
309
304
|
"""Test performance of FastAPI integration."""
|
310
305
|
example = FastAPISecurityExample(config_path=self.config_path)
|
311
306
|
client = TestClient(example.app)
|
312
|
-
|
307
|
+
|
313
308
|
headers = {"X-API-Key": "user_key_456"}
|
314
|
-
|
309
|
+
|
315
310
|
import time
|
316
|
-
|
311
|
+
|
317
312
|
# Benchmark health check endpoint
|
318
313
|
start_time = time.time()
|
319
314
|
for _ in range(100):
|
320
315
|
response = client.get("/health")
|
321
316
|
assert response.status_code == 200
|
322
317
|
end_time = time.time()
|
323
|
-
|
318
|
+
|
324
319
|
avg_time = (end_time - start_time) / 100
|
325
320
|
assert avg_time < 0.01, f"Health check too slow: {avg_time:.4f}s per request"
|
326
|
-
|
321
|
+
|
327
322
|
# Benchmark authenticated endpoint
|
328
323
|
start_time = time.time()
|
329
324
|
for _ in range(50):
|
330
325
|
response = client.get("/api/v1/users/me", headers=headers)
|
331
326
|
assert response.status_code == 200 # Should be authenticated
|
332
327
|
end_time = time.time()
|
333
|
-
|
328
|
+
|
334
329
|
avg_time = (end_time - start_time) / 50
|
335
|
-
assert
|
330
|
+
assert (
|
331
|
+
avg_time < 0.02
|
332
|
+
), f"Authenticated endpoint too slow: {avg_time:.4f}s per request"
|