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.
Files changed (58) hide show
  1. mcp_security_framework/__init__.py +26 -15
  2. mcp_security_framework/cli/__init__.py +1 -1
  3. mcp_security_framework/cli/cert_cli.py +233 -197
  4. mcp_security_framework/cli/security_cli.py +324 -234
  5. mcp_security_framework/constants.py +21 -27
  6. mcp_security_framework/core/auth_manager.py +41 -22
  7. mcp_security_framework/core/cert_manager.py +210 -147
  8. mcp_security_framework/core/permission_manager.py +9 -9
  9. mcp_security_framework/core/rate_limiter.py +2 -2
  10. mcp_security_framework/core/security_manager.py +284 -229
  11. mcp_security_framework/examples/__init__.py +6 -0
  12. mcp_security_framework/examples/comprehensive_example.py +349 -279
  13. mcp_security_framework/examples/django_example.py +247 -206
  14. mcp_security_framework/examples/fastapi_example.py +315 -283
  15. mcp_security_framework/examples/flask_example.py +274 -203
  16. mcp_security_framework/examples/gateway_example.py +304 -237
  17. mcp_security_framework/examples/microservice_example.py +258 -189
  18. mcp_security_framework/examples/standalone_example.py +255 -230
  19. mcp_security_framework/examples/test_all_examples.py +151 -135
  20. mcp_security_framework/middleware/__init__.py +46 -55
  21. mcp_security_framework/middleware/auth_middleware.py +62 -63
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +119 -118
  23. mcp_security_framework/middleware/fastapi_middleware.py +156 -148
  24. mcp_security_framework/middleware/flask_auth_middleware.py +160 -147
  25. mcp_security_framework/middleware/flask_middleware.py +183 -157
  26. mcp_security_framework/middleware/mtls_middleware.py +106 -117
  27. mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
  28. mcp_security_framework/middleware/security_middleware.py +109 -124
  29. mcp_security_framework/schemas/config.py +2 -1
  30. mcp_security_framework/schemas/models.py +18 -6
  31. mcp_security_framework/utils/cert_utils.py +14 -8
  32. mcp_security_framework/utils/datetime_compat.py +116 -0
  33. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/METADATA +4 -3
  34. mcp_security_framework-1.1.2.dist-info/RECORD +84 -0
  35. tests/conftest.py +63 -66
  36. tests/test_cli/test_cert_cli.py +184 -146
  37. tests/test_cli/test_security_cli.py +274 -247
  38. tests/test_core/test_cert_manager.py +24 -10
  39. tests/test_core/test_security_manager.py +2 -2
  40. tests/test_examples/test_comprehensive_example.py +190 -137
  41. tests/test_examples/test_fastapi_example.py +124 -101
  42. tests/test_examples/test_flask_example.py +124 -101
  43. tests/test_examples/test_standalone_example.py +73 -80
  44. tests/test_integration/test_auth_flow.py +213 -197
  45. tests/test_integration/test_certificate_flow.py +180 -149
  46. tests/test_integration/test_fastapi_integration.py +108 -111
  47. tests/test_integration/test_flask_integration.py +141 -140
  48. tests/test_integration/test_standalone_integration.py +290 -259
  49. tests/test_middleware/test_fastapi_auth_middleware.py +195 -174
  50. tests/test_middleware/test_fastapi_middleware.py +147 -132
  51. tests/test_middleware/test_flask_auth_middleware.py +260 -202
  52. tests/test_middleware/test_flask_middleware.py +201 -179
  53. tests/test_middleware/test_security_middleware.py +145 -130
  54. tests/test_utils/test_datetime_compat.py +147 -0
  55. mcp_security_framework-1.1.0.dist-info/RECORD +0 -82
  56. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/WHEEL +0 -0
  57. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/entry_points.txt +0 -0
  58. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.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
- from unittest.mock import patch, MagicMock
17
- from typing import Dict, Any
15
+ import tempfile
16
+ from typing import Any, Dict
17
+ from unittest.mock import MagicMock, patch
18
18
 
19
19
  import pytest
20
- from flask.testing import FlaskClient
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 flask.testing import FlaskClient
24
24
 
25
- from mcp_security_framework.examples.flask_example import FlaskExample
26
25
  from mcp_security_framework.core.security_manager import SecurityManager
27
- from mcp_security_framework.schemas.config import SecurityConfig, AuthConfig, RateLimitConfig, SSLConfig
26
+ from mcp_security_framework.examples.flask_example import FlaskExample
27
+ from mcp_security_framework.schemas.config import (
28
+ AuthConfig,
29
+ RateLimitConfig,
30
+ SecurityConfig,
31
+ SSLConfig,
32
+ )
28
33
 
29
34
 
30
35
  class TestFlaskIntegration:
31
36
  """Integration tests for Flask 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,227 +45,226 @@ class TestFlaskIntegration:
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"]}
44
- }
45
- },
46
- "rate_limit": {
47
- "enabled": True,
48
- "default_requests_per_minute": 100
49
- },
50
- "ssl": {
51
- "enabled": False
52
- },
53
- "permissions": {
54
- "enabled": True,
55
- "roles_file": "test_roles.json"
56
- },
57
- "certificates": {
58
- "enabled": False
48
+ "readonly_key_789": {"username": "readonly", "roles": ["readonly"]},
49
+ },
59
50
  },
60
- "logging": {
61
- "level": "INFO",
62
- "format": "standard"
63
- }
51
+ "rate_limit": {"enabled": True, "default_requests_per_minute": 100},
52
+ "ssl": {"enabled": False},
53
+ "permissions": {"enabled": True, "roles_file": "test_roles.json"},
54
+ "certificates": {"enabled": False},
55
+ "logging": {"level": "INFO", "format": "standard"},
64
56
  }
65
-
57
+
66
58
  # Create temporary config file
67
- self.config_fd, self.config_path = tempfile.mkstemp(suffix='.json')
68
- with os.fdopen(self.config_fd, 'w') as f:
59
+ self.config_fd, self.config_path = tempfile.mkstemp(suffix=".json")
60
+ with os.fdopen(self.config_fd, "w") as f:
69
61
  json.dump(self.test_config, f)
70
-
62
+
71
63
  # Create temporary roles file
72
64
  self.roles_config = {
73
65
  "roles": {
74
66
  "admin": {
75
67
  "permissions": ["read", "write", "delete", "admin"],
76
- "description": "Administrator role"
68
+ "description": "Administrator role",
77
69
  },
78
70
  "user": {
79
71
  "permissions": ["read", "write"],
80
- "description": "Regular user role"
72
+ "description": "Regular user role",
81
73
  },
82
74
  "readonly": {
83
75
  "permissions": ["read"],
84
- "description": "Read-only user role"
85
- }
76
+ "description": "Read-only user role",
77
+ },
86
78
  }
87
79
  }
88
-
89
- self.roles_fd, self.roles_path = tempfile.mkstemp(suffix='.json')
90
- with os.fdopen(self.roles_fd, 'w') as f:
80
+
81
+ self.roles_fd, self.roles_path = tempfile.mkstemp(suffix=".json")
82
+ with os.fdopen(self.roles_fd, "w") as f:
91
83
  json.dump(self.roles_config, f)
92
-
84
+
93
85
  # Update config to use roles file
94
86
  self.test_config["permissions"]["roles_file"] = self.roles_path
95
-
87
+
96
88
  # Recreate config file with updated roles path
97
- with open(self.config_path, 'w') as f:
89
+ with open(self.config_path, "w") as f:
98
90
  json.dump(self.test_config, f)
99
-
91
+
100
92
  def teardown_method(self):
101
93
  """Clean up after each test method."""
102
94
  # Remove temporary files
103
- if hasattr(self, 'config_path') and os.path.exists(self.config_path):
95
+ if hasattr(self, "config_path") and os.path.exists(self.config_path):
104
96
  os.unlink(self.config_path)
105
- if hasattr(self, 'roles_path') and os.path.exists(self.roles_path):
97
+ if hasattr(self, "roles_path") and os.path.exists(self.roles_path):
106
98
  os.unlink(self.roles_path)
107
-
99
+
108
100
  def test_flask_full_integration(self):
109
101
  """Test complete Flask integration with security framework."""
110
102
  # Create Flask example
111
103
  example = FlaskExample(config_path=self.config_path)
112
-
104
+
113
105
  # Test that the app is properly configured
114
106
  assert example.app is not None
115
- assert hasattr(example.app, 'wsgi_app')
116
-
107
+ assert hasattr(example.app, "wsgi_app")
108
+
117
109
  # Test that security manager is configured
118
110
  assert example.security_manager is not None
119
111
  assert isinstance(example.security_manager, SecurityManager)
120
-
112
+
121
113
  # Test that configuration is loaded
122
114
  assert example.config is not None
123
115
  assert example.config.auth.enabled is True
124
116
  assert example.config.rate_limit.enabled is True
125
-
117
+
126
118
  def test_flask_authentication_flow(self):
127
119
  """Test complete authentication flow in Flask."""
128
120
  example = FlaskExample(config_path=self.config_path)
129
121
  client = example.app.test_client()
130
-
122
+
131
123
  # Test unauthenticated access to protected endpoint
132
124
  response = client.get("/api/v1/users/me")
133
125
  assert response.status_code == 401
134
-
126
+
135
127
  # Test authenticated access with valid API key
136
128
  headers = {"X-API-Key": "admin_key_123"}
137
129
  response = client.get("/api/v1/users/me", headers=headers)
138
130
  assert response.status_code == 200 # Should be authenticated
139
-
131
+
140
132
  # Test authenticated access with different user
141
133
  headers = {"X-API-Key": "user_key_456"}
142
134
  response = client.get("/api/v1/users/me", headers=headers)
143
135
  assert response.status_code == 200 # User should be authenticated
144
-
136
+
145
137
  def test_flask_authorization_flow(self):
146
138
  """Test complete authorization flow in Flask."""
147
139
  example = FlaskExample(config_path=self.config_path)
148
140
  client = example.app.test_client()
149
-
141
+
150
142
  # Test admin access to admin-only endpoint
151
143
  headers = {"X-API-Key": "admin_key_123"}
152
144
  response = client.get("/api/v1/admin/users", headers=headers)
153
145
  assert response.status_code == 200 # Admin should have access
154
-
146
+
155
147
  # Test regular user access to admin-only endpoint (should be denied)
156
148
  headers = {"X-API-Key": "user_key_456"}
157
149
  response = client.get("/api/v1/admin/users", headers=headers)
158
150
  assert response.status_code == 403 # User should be denied admin access
159
-
151
+
160
152
  # Test readonly user access to write endpoint (should be denied)
161
153
  headers = {"X-API-Key": "readonly_key_789"}
162
- response = client.post("/api/v1/data",
163
- headers=headers,
164
- json={"name": "test", "value": "test_value"})
165
- assert response.status_code == 403 # Readonly user should be denied write access
166
-
154
+ response = client.post(
155
+ "/api/v1/data",
156
+ headers=headers,
157
+ json={"name": "test", "value": "test_value"},
158
+ )
159
+ assert (
160
+ response.status_code == 403
161
+ ) # Readonly user should be denied write access
162
+
167
163
  def test_flask_rate_limiting(self):
168
164
  """Test rate limiting in Flask."""
169
165
  example = FlaskExample(config_path=self.config_path)
170
166
  client = example.app.test_client()
171
-
167
+
172
168
  headers = {"X-API-Key": "user_key_456"}
173
-
169
+
174
170
  # Make multiple requests to trigger rate limiting
175
171
  responses = []
176
172
  for i in range(105): # Exceed the 100 requests per minute limit
177
173
  response = client.get("/api/v1/users/me", headers=headers)
178
174
  responses.append(response.status_code)
179
-
175
+
180
176
  # Check that some requests were rate limited
181
177
  # Note: Rate limiting may not be triggered in test environment
182
178
  # but the requests should still be processed
183
179
  assert len(responses) == 105, "All requests should be processed"
184
- assert all(status in [200, 429] for status in responses), "Responses should be either 200 or 429"
185
-
180
+ assert all(
181
+ status in [200, 429] for status in responses
182
+ ), "Responses should be either 200 or 429"
183
+
186
184
  def test_flask_ssl_integration(self):
187
185
  """Test SSL/TLS integration in Flask."""
188
186
  # Create config with SSL enabled
189
187
  ssl_config = self.test_config.copy()
190
- ssl_config["ssl"] = {
191
- "enabled": False # Disable SSL for testing
192
- }
193
-
188
+ ssl_config["ssl"] = {"enabled": False} # Disable SSL for testing
189
+
194
190
  # Create temporary SSL config file
195
- ssl_config_fd, ssl_config_path = tempfile.mkstemp(suffix='.json')
196
- with os.fdopen(ssl_config_fd, 'w') as f:
191
+ ssl_config_fd, ssl_config_path = tempfile.mkstemp(suffix=".json")
192
+ with os.fdopen(ssl_config_fd, "w") as f:
197
193
  json.dump(ssl_config, f)
198
-
194
+
199
195
  try:
200
196
  # Mock SSL context creation to avoid file requirements
201
- with patch('mcp_security_framework.core.ssl_manager.SSLManager.create_server_context') as mock_ssl:
197
+ with patch(
198
+ "mcp_security_framework.core.ssl_manager.SSLManager.create_server_context"
199
+ ) as mock_ssl:
202
200
  mock_ssl.return_value = MagicMock()
203
-
201
+
204
202
  example = FlaskExample(config_path=ssl_config_path)
205
-
203
+
206
204
  # Test that SSL is configured
207
205
  assert example.config.ssl.enabled is False # SSL disabled for testing
208
-
206
+
209
207
  finally:
210
208
  os.unlink(ssl_config_path)
211
-
209
+
212
210
  def test_flask_error_handling(self):
213
211
  """Test error handling in Flask integration."""
214
212
  example = FlaskExample(config_path=self.config_path)
215
213
  client = example.app.test_client()
216
-
214
+
217
215
  # Test invalid API key
218
216
  headers = {"X-API-Key": "invalid_key"}
219
217
  response = client.get("/api/v1/users/me", headers=headers)
220
218
  assert response.status_code == 401
221
-
219
+
222
220
  # Test malformed request
223
221
  headers = {"X-API-Key": "admin_key_123"}
224
- response = client.post("/api/v1/data",
225
- headers=headers,
226
- json={"invalid": "data"})
227
- assert response.status_code == 200 # Should succeed with valid auth (Flask returns 200 for POST)
228
-
222
+ response = client.post(
223
+ "/api/v1/data", headers=headers, json={"invalid": "data"}
224
+ )
225
+ assert (
226
+ response.status_code == 200
227
+ ) # Should succeed with valid auth (Flask returns 200 for POST)
228
+
229
229
  def test_flask_health_and_metrics(self):
230
230
  """Test health check and metrics endpoints."""
231
231
  example = FlaskExample(config_path=self.config_path)
232
232
  client = example.app.test_client()
233
-
233
+
234
234
  # Test health check
235
235
  response = client.get("/health")
236
236
  assert response.status_code == 200
237
237
  data = response.get_json()
238
238
  assert "status" in data
239
239
  assert data["status"] == "healthy"
240
-
240
+
241
241
  # Test metrics
242
242
  response = client.get("/metrics")
243
243
  assert response.status_code == 200
244
244
  data = response.get_json()
245
245
  assert "uptime_seconds" in data
246
246
  assert "requests_total" in data
247
-
247
+
248
248
  def test_flask_data_operations(self):
249
249
  """Test data operations with security."""
250
250
  example = FlaskExample(config_path=self.config_path)
251
251
  client = example.app.test_client()
252
-
252
+
253
253
  headers = {"X-API-Key": "admin_key_123"}
254
-
254
+
255
255
  # Create data
256
- create_response = client.post("/api/v1/data",
257
- headers=headers,
258
- json={"name": "test_item", "value": "test_value"})
256
+ create_response = client.post(
257
+ "/api/v1/data",
258
+ headers=headers,
259
+ json={"name": "test_item", "value": "test_value"},
260
+ )
259
261
 
260
- assert create_response.status_code == 200 # Should succeed with valid auth (Flask returns 200 for POST)
262
+ assert (
263
+ create_response.status_code == 200
264
+ ) # Should succeed with valid auth (Flask returns 200 for POST)
261
265
  data = create_response.get_json()
262
266
  assert "id" in data
263
-
267
+
264
268
  # Retrieve data
265
269
  data_id = data["id"]
266
270
  get_response = client.get(f"/api/v1/data/{data_id}", headers=headers)
@@ -268,133 +272,130 @@ class TestFlaskIntegration:
268
272
  retrieved_data = get_response.get_json()
269
273
  assert retrieved_data["id"] == data_id
270
274
  assert "data" in retrieved_data
271
-
275
+
272
276
  def test_flask_middleware_integration(self):
273
277
  """Test that security middleware is properly integrated."""
274
278
  example = FlaskExample(config_path=self.config_path)
275
-
279
+
276
280
  # Check that middleware is configured
277
281
  # Note: In test environment, middleware setup is skipped
278
282
  # but we can verify the app structure
279
- assert hasattr(example.app, 'wsgi_app')
280
-
283
+ assert hasattr(example.app, "wsgi_app")
284
+
281
285
  # Test that routes are properly configured
282
286
  routes = []
283
287
  for rule in example.app.url_map.iter_rules():
284
288
  routes.append(rule.rule)
285
-
289
+
286
290
  expected_routes = [
287
291
  "/health",
288
- "/metrics",
292
+ "/metrics",
289
293
  "/api/v1/users/me",
290
294
  "/api/v1/admin/users",
291
295
  "/api/v1/data",
292
- "/api/v1/data/<data_id>"
296
+ "/api/v1/data/<data_id>",
293
297
  ]
294
-
298
+
295
299
  for route in expected_routes:
296
300
  assert route in routes, f"Route {route} not found in app routes"
297
-
301
+
298
302
  def test_flask_configuration_validation(self):
299
303
  """Test configuration validation in Flask integration."""
300
304
  # Test with invalid configuration
301
- invalid_config = {
302
- "auth": {
303
- "enabled": True,
304
- "methods": ["invalid_method"]
305
- }
306
- }
307
-
308
- invalid_config_fd, invalid_config_path = tempfile.mkstemp(suffix='.json')
309
- with os.fdopen(invalid_config_fd, 'w') as f:
305
+ invalid_config = {"auth": {"enabled": True, "methods": ["invalid_method"]}}
306
+
307
+ invalid_config_fd, invalid_config_path = tempfile.mkstemp(suffix=".json")
308
+ with os.fdopen(invalid_config_fd, "w") as f:
310
309
  json.dump(invalid_config, f)
311
-
310
+
312
311
  try:
313
312
  # Should raise validation error
314
313
  with pytest.raises(Exception):
315
314
  FlaskExample(config_path=invalid_config_path)
316
315
  finally:
317
316
  os.unlink(invalid_config_path)
318
-
317
+
319
318
  def test_flask_performance_benchmark(self):
320
319
  """Test performance of Flask integration."""
321
320
  example = FlaskExample(config_path=self.config_path)
322
321
  client = example.app.test_client()
323
-
322
+
324
323
  headers = {"X-API-Key": "user_key_456"}
325
-
324
+
326
325
  import time
327
-
326
+
328
327
  # Benchmark health check endpoint
329
328
  start_time = time.time()
330
329
  for _ in range(100):
331
330
  response = client.get("/health")
332
331
  assert response.status_code == 200
333
332
  end_time = time.time()
334
-
333
+
335
334
  avg_time = (end_time - start_time) / 100
336
335
  assert avg_time < 0.01, f"Health check too slow: {avg_time:.4f}s per request"
337
-
336
+
338
337
  # Benchmark authenticated endpoint
339
338
  start_time = time.time()
340
339
  for _ in range(50):
341
340
  response = client.get("/api/v1/users/me", headers=headers)
342
341
  assert response.status_code == 200 # Should be authenticated
343
342
  end_time = time.time()
344
-
343
+
345
344
  avg_time = (end_time - start_time) / 50
346
- assert avg_time < 0.02, f"Authenticated endpoint too slow: {avg_time:.4f}s per request"
347
-
345
+ assert (
346
+ avg_time < 0.02
347
+ ), f"Authenticated endpoint too slow: {avg_time:.4f}s per request"
348
+
348
349
  def test_flask_session_management(self):
349
350
  """Test session management in Flask."""
350
351
  example = FlaskExample(config_path=self.config_path)
351
352
  client = example.app.test_client()
352
-
353
+
353
354
  # Test that sessions are properly configured
354
- assert hasattr(example.app, 'config')
355
- assert 'SECRET_KEY' in example.app.config
356
-
355
+ assert hasattr(example.app, "config")
356
+ assert "SECRET_KEY" in example.app.config
357
+
357
358
  # Test session persistence across requests
358
359
  headers = {"X-API-Key": "admin_key_123"}
359
-
360
+
360
361
  # First request
361
362
  response1 = client.get("/api/v1/users/me", headers=headers)
362
363
  assert response1.status_code == 200 # Should be authenticated
363
-
364
+
364
365
  # Second request with same session
365
366
  response2 = client.get("/api/v1/users/me", headers=headers)
366
367
  assert response2.status_code == 200 # Should be authenticated
367
-
368
+
368
369
  # Verify consistent user data
369
370
  data1 = response1.get_json()
370
371
  data2 = response2.get_json()
371
372
  assert data1["username"] == data2["username"]
372
-
373
+
373
374
  def test_flask_cors_integration(self):
374
375
  """Test CORS integration in Flask."""
375
376
  example = FlaskExample(config_path=self.config_path)
376
377
  client = example.app.test_client()
377
-
378
+
378
379
  # Test CORS headers are present
379
380
  response = client.get("/health")
380
381
  assert response.status_code == 200
381
-
382
+
382
383
  # Check for CORS headers (if CORS is configured)
383
384
  # Note: This depends on CORS configuration in the Flask app
384
- cors_headers = ['Access-Control-Allow-Origin', 'Access-Control-Allow-Methods']
385
+ cors_headers = ["Access-Control-Allow-Origin", "Access-Control-Allow-Methods"]
385
386
  # We don't assert specific CORS headers as they may not be configured in test mode
386
-
387
+
387
388
  def test_flask_logging_integration(self):
388
389
  """Test logging integration in Flask."""
389
390
  example = FlaskExample(config_path=self.config_path)
390
391
  client = example.app.test_client()
391
-
392
+
392
393
  # Test that logging is configured
393
- assert hasattr(example.app, 'logger')
394
-
394
+ assert hasattr(example.app, "logger")
395
+
395
396
  # Test that requests are logged
396
397
  response = client.get("/health")
397
398
  assert response.status_code == 200
398
-
399
+
399
400
  # Verify that the app has proper logging configuration
400
401
  assert example.app.logger is not None