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
@@ -17,34 +17,39 @@ Version: 1.0.0
|
|
17
17
|
License: MIT
|
18
18
|
"""
|
19
19
|
|
20
|
-
import os
|
21
20
|
import json
|
22
21
|
import logging
|
23
|
-
|
22
|
+
import os
|
24
23
|
from datetime import datetime, timedelta, timezone
|
24
|
+
from typing import Any, Dict, List, Optional
|
25
25
|
|
26
|
-
from flask import Flask, request, jsonify, g, session, current_app
|
27
|
-
from flask_cors import CORS
|
28
26
|
import werkzeug
|
27
|
+
from flask import Flask, current_app, g, jsonify, request, session
|
28
|
+
from flask_cors import CORS
|
29
29
|
|
30
|
-
from mcp_security_framework.
|
30
|
+
from mcp_security_framework.constants import (
|
31
|
+
AUTH_METHODS,
|
32
|
+
DEFAULT_CLIENT_IP,
|
33
|
+
DEFAULT_SECURITY_HEADERS,
|
34
|
+
HTTP_FORBIDDEN,
|
35
|
+
HTTP_TOO_MANY_REQUESTS,
|
36
|
+
HTTP_UNAUTHORIZED,
|
37
|
+
ErrorCodes,
|
38
|
+
)
|
31
39
|
from mcp_security_framework.core.auth_manager import AuthManager
|
32
|
-
from mcp_security_framework.core.ssl_manager import SSLManager
|
33
40
|
from mcp_security_framework.core.permission_manager import PermissionManager
|
34
41
|
from mcp_security_framework.core.rate_limiter import RateLimiter
|
35
|
-
from mcp_security_framework.
|
36
|
-
from mcp_security_framework.
|
42
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
43
|
+
from mcp_security_framework.core.ssl_manager import SSLManager
|
37
44
|
from mcp_security_framework.middleware.flask_middleware import FlaskSecurityMiddleware
|
38
|
-
from mcp_security_framework.
|
39
|
-
|
40
|
-
ErrorCodes, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_TOO_MANY_REQUESTS
|
41
|
-
)
|
45
|
+
from mcp_security_framework.schemas.config import AuthConfig, SecurityConfig, SSLConfig
|
46
|
+
from mcp_security_framework.schemas.models import AuthMethod, AuthResult, AuthStatus
|
42
47
|
|
43
48
|
|
44
49
|
class FlaskExample:
|
45
50
|
"""
|
46
51
|
Complete Flask Example with Security Framework Implementation
|
47
|
-
|
52
|
+
|
48
53
|
This class demonstrates a production-ready Flask application
|
49
54
|
with comprehensive security features including:
|
50
55
|
- Multi-method authentication (API Key, JWT, Certificate)
|
@@ -54,11 +59,11 @@ class FlaskExample:
|
|
54
59
|
- Security headers and CORS
|
55
60
|
- Comprehensive logging and monitoring
|
56
61
|
"""
|
57
|
-
|
62
|
+
|
58
63
|
def __init__(self, config_path: Optional[str] = None):
|
59
64
|
"""
|
60
65
|
Initialize Flask example with security configuration.
|
61
|
-
|
66
|
+
|
62
67
|
Args:
|
63
68
|
config_path: Path to security configuration file
|
64
69
|
"""
|
@@ -68,37 +73,41 @@ class FlaskExample:
|
|
68
73
|
self._setup_middleware()
|
69
74
|
self._setup_routes()
|
70
75
|
self._setup_error_handlers()
|
71
|
-
|
76
|
+
|
72
77
|
def _load_config(self, config_path: Optional[str]) -> SecurityConfig:
|
73
78
|
"""
|
74
79
|
Load security configuration from file or create default.
|
75
|
-
|
80
|
+
|
76
81
|
Args:
|
77
82
|
config_path: Path to configuration file
|
78
|
-
|
83
|
+
|
79
84
|
Returns:
|
80
85
|
SecurityConfig: Loaded configuration
|
81
86
|
"""
|
82
87
|
if config_path and os.path.exists(config_path):
|
83
|
-
with open(config_path,
|
88
|
+
with open(config_path, "r") as f:
|
84
89
|
config_data = json.load(f)
|
85
90
|
return SecurityConfig(**config_data)
|
86
|
-
|
91
|
+
|
87
92
|
# Create production-ready default configuration
|
88
93
|
return SecurityConfig(
|
89
94
|
auth=AuthConfig(
|
90
95
|
enabled=True,
|
91
|
-
methods=[
|
96
|
+
methods=[
|
97
|
+
AUTH_METHODS["API_KEY"],
|
98
|
+
AUTH_METHODS["JWT"],
|
99
|
+
AUTH_METHODS["CERTIFICATE"],
|
100
|
+
],
|
92
101
|
api_keys={
|
93
102
|
"admin_key_123": {"username": "admin", "roles": ["admin", "user"]},
|
94
103
|
"user_key_456": {"username": "user", "roles": ["user"]},
|
95
|
-
"readonly_key_789": {"username": "readonly", "roles": ["readonly"]}
|
104
|
+
"readonly_key_789": {"username": "readonly", "roles": ["readonly"]},
|
96
105
|
},
|
97
106
|
jwt_secret="your-super-secret-jwt-key-change-in-production",
|
98
107
|
jwt_algorithm="HS256",
|
99
108
|
jwt_expiry_hours=24,
|
100
109
|
public_paths=["/health", "/docs", "/metrics"],
|
101
|
-
security_headers=DEFAULT_SECURITY_HEADERS
|
110
|
+
security_headers=DEFAULT_SECURITY_HEADERS,
|
102
111
|
),
|
103
112
|
ssl=SSLConfig(
|
104
113
|
enabled=False, # Disable SSL for testing
|
@@ -106,7 +115,7 @@ class FlaskExample:
|
|
106
115
|
key_file=None,
|
107
116
|
ca_cert_file=None,
|
108
117
|
verify_mode="CERT_REQUIRED",
|
109
|
-
min_version="TLSv1.2"
|
118
|
+
min_version="TLSv1.2",
|
110
119
|
),
|
111
120
|
rate_limit={
|
112
121
|
"enabled": True,
|
@@ -119,16 +128,16 @@ class FlaskExample:
|
|
119
128
|
"host": "localhost",
|
120
129
|
"port": 6379,
|
121
130
|
"db": 0,
|
122
|
-
"password": None
|
131
|
+
"password": None,
|
123
132
|
},
|
124
133
|
"exempt_paths": ["/health", "/metrics"],
|
125
|
-
"exempt_roles": ["admin"]
|
134
|
+
"exempt_roles": ["admin"],
|
126
135
|
},
|
127
136
|
permissions={
|
128
137
|
"enabled": True,
|
129
138
|
"roles_file": "config/roles.json",
|
130
139
|
"default_role": "user",
|
131
|
-
"hierarchy_enabled": True
|
140
|
+
"hierarchy_enabled": True,
|
132
141
|
},
|
133
142
|
logging={
|
134
143
|
"enabled": True,
|
@@ -138,100 +147,108 @@ class FlaskExample:
|
|
138
147
|
"max_file_size": 10,
|
139
148
|
"backup_count": 5,
|
140
149
|
"console_output": True,
|
141
|
-
"json_format": False
|
142
|
-
}
|
150
|
+
"json_format": False,
|
151
|
+
},
|
143
152
|
)
|
144
|
-
|
153
|
+
|
145
154
|
def _create_flask_app(self) -> Flask:
|
146
155
|
"""
|
147
156
|
Create Flask application with security features.
|
148
|
-
|
157
|
+
|
149
158
|
Returns:
|
150
159
|
Flask: Configured Flask application
|
151
160
|
"""
|
152
161
|
app = Flask(__name__)
|
153
|
-
app.config[
|
154
|
-
app.config[
|
155
|
-
|
162
|
+
app.config["SECRET_KEY"] = "your-secret-key-change-in-production"
|
163
|
+
app.config["JSON_SORT_KEYS"] = False
|
164
|
+
|
156
165
|
# Add CORS support
|
157
166
|
CORS(app, origins=["https://trusted-domain.com"])
|
158
|
-
|
167
|
+
|
159
168
|
return app
|
160
|
-
|
169
|
+
|
161
170
|
def _setup_middleware(self):
|
162
171
|
"""Setup security middleware."""
|
163
172
|
# For now, skip middleware setup to avoid WSGI issues
|
164
173
|
# and use fallback authentication for testing
|
165
174
|
print("Middleware setup skipped - using fallback authentication")
|
166
175
|
self._setup_test_authentication()
|
167
|
-
|
176
|
+
|
168
177
|
def _setup_test_authentication(self):
|
169
178
|
"""Setup authentication for testing environment."""
|
170
179
|
security_manager = self.security_manager
|
171
|
-
|
180
|
+
|
172
181
|
def get_current_user():
|
173
182
|
"""Get current user from request headers."""
|
174
183
|
api_key = request.headers.get("X-API-Key")
|
175
184
|
if api_key:
|
176
185
|
try:
|
177
|
-
auth_result = security_manager.auth_manager.authenticate_api_key(
|
186
|
+
auth_result = security_manager.auth_manager.authenticate_api_key(
|
187
|
+
api_key
|
188
|
+
)
|
178
189
|
if auth_result.is_valid:
|
179
190
|
return auth_result
|
180
191
|
except Exception as e:
|
181
192
|
print(f"Authentication error: {e}")
|
182
193
|
pass
|
183
|
-
|
194
|
+
|
184
195
|
# Check for JWT token
|
185
196
|
auth_header = request.headers.get("Authorization")
|
186
197
|
if auth_header and auth_header.startswith("Bearer "):
|
187
198
|
token = auth_header.split(" ")[1]
|
188
199
|
try:
|
189
|
-
auth_result = security_manager.auth_manager.authenticate_jwt_token(
|
200
|
+
auth_result = security_manager.auth_manager.authenticate_jwt_token(
|
201
|
+
token
|
202
|
+
)
|
190
203
|
if auth_result.is_valid:
|
191
204
|
return auth_result
|
192
205
|
except Exception as e:
|
193
206
|
print(f"JWT authentication error: {e}")
|
194
207
|
pass
|
195
|
-
|
208
|
+
|
196
209
|
return None
|
197
|
-
|
210
|
+
|
198
211
|
# Store function for use in routes
|
199
212
|
self.get_current_user = get_current_user
|
200
|
-
|
213
|
+
|
201
214
|
# Make function available in app context
|
202
215
|
self.app.get_current_user = get_current_user
|
203
|
-
|
216
|
+
|
204
217
|
def _setup_routes(self):
|
205
218
|
"""Setup application routes with security."""
|
206
|
-
|
219
|
+
|
207
220
|
# Get reference to authentication function
|
208
221
|
get_current_user_func = self.get_current_user
|
209
|
-
|
210
|
-
@self.app.route(
|
222
|
+
|
223
|
+
@self.app.route("/health", methods=["GET"])
|
211
224
|
def health_check():
|
212
225
|
"""Health check endpoint (public)."""
|
213
|
-
return jsonify(
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
226
|
+
return jsonify(
|
227
|
+
{
|
228
|
+
"status": "healthy",
|
229
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
230
|
+
"version": "1.0.0",
|
231
|
+
}
|
232
|
+
)
|
233
|
+
|
234
|
+
@self.app.route("/metrics", methods=["GET"])
|
220
235
|
def metrics():
|
221
236
|
"""Metrics endpoint (public)."""
|
222
|
-
return jsonify(
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
237
|
+
return jsonify(
|
238
|
+
{
|
239
|
+
"requests_total": 1000,
|
240
|
+
"requests_per_minute": 60,
|
241
|
+
"active_connections": 25,
|
242
|
+
"uptime_seconds": 3600,
|
243
|
+
}
|
244
|
+
)
|
245
|
+
|
246
|
+
@self.app.route("/api/v1/users/me", methods=["GET"])
|
230
247
|
def get_current_user_route():
|
231
248
|
"""Get current user information (authenticated)."""
|
232
249
|
# Try to get user info from middleware
|
233
|
-
user_info = getattr(g,
|
234
|
-
|
250
|
+
user_info = getattr(g, "user_info", None)
|
251
|
+
|
235
252
|
# If middleware didn't set user_info, try authentication
|
236
253
|
if not user_info:
|
237
254
|
auth_result = get_current_user_func()
|
@@ -239,24 +256,26 @@ class FlaskExample:
|
|
239
256
|
user_info = {
|
240
257
|
"username": auth_result.username,
|
241
258
|
"roles": auth_result.roles,
|
242
|
-
"permissions": auth_result.permissions
|
259
|
+
"permissions": auth_result.permissions,
|
243
260
|
}
|
244
261
|
else:
|
245
262
|
return jsonify({"error": "Authentication required"}), 401
|
246
|
-
|
247
|
-
return jsonify(
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
263
|
+
|
264
|
+
return jsonify(
|
265
|
+
{
|
266
|
+
"username": user_info.get("username"),
|
267
|
+
"roles": user_info.get("roles", []),
|
268
|
+
"permissions": list(user_info.get("permissions", [])),
|
269
|
+
"last_login": datetime.now(timezone.utc).isoformat(),
|
270
|
+
}
|
271
|
+
)
|
272
|
+
|
273
|
+
@self.app.route("/api/v1/admin/users", methods=["GET"])
|
255
274
|
def get_all_users():
|
256
275
|
"""Get all users (admin only)."""
|
257
276
|
# Try to get user info from middleware
|
258
|
-
user_info = getattr(g,
|
259
|
-
|
277
|
+
user_info = getattr(g, "user_info", None)
|
278
|
+
|
260
279
|
# If middleware didn't set user_info, try authentication
|
261
280
|
if not user_info:
|
262
281
|
auth_result = get_current_user_func()
|
@@ -264,29 +283,35 @@ class FlaskExample:
|
|
264
283
|
user_info = {
|
265
284
|
"username": auth_result.username,
|
266
285
|
"roles": auth_result.roles,
|
267
|
-
"permissions": auth_result.permissions
|
286
|
+
"permissions": auth_result.permissions,
|
268
287
|
}
|
269
288
|
else:
|
270
289
|
return jsonify({"error": "Authentication required"}), 401
|
271
|
-
|
290
|
+
|
272
291
|
# Check admin permission
|
273
292
|
if "admin" not in user_info.get("roles", []):
|
274
293
|
return jsonify({"error": "Admin access required"}), 403
|
275
|
-
|
276
|
-
return jsonify(
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
294
|
+
|
295
|
+
return jsonify(
|
296
|
+
{
|
297
|
+
"users": [
|
298
|
+
{"username": "admin", "roles": ["admin"], "status": "active"},
|
299
|
+
{"username": "user", "roles": ["user"], "status": "active"},
|
300
|
+
{
|
301
|
+
"username": "readonly",
|
302
|
+
"roles": ["readonly"],
|
303
|
+
"status": "active",
|
304
|
+
},
|
305
|
+
]
|
306
|
+
}
|
307
|
+
)
|
308
|
+
|
309
|
+
@self.app.route("/api/v1/data", methods=["POST"])
|
285
310
|
def create_data():
|
286
311
|
"""Create data (authenticated users)."""
|
287
312
|
# Try to get user info from middleware
|
288
|
-
user_info = getattr(g,
|
289
|
-
|
313
|
+
user_info = getattr(g, "user_info", None)
|
314
|
+
|
290
315
|
# If middleware didn't set user_info, try authentication
|
291
316
|
if not user_info:
|
292
317
|
auth_result = get_current_user_func()
|
@@ -294,30 +319,32 @@ class FlaskExample:
|
|
294
319
|
user_info = {
|
295
320
|
"username": auth_result.username,
|
296
321
|
"roles": auth_result.roles,
|
297
|
-
"permissions": auth_result.permissions
|
322
|
+
"permissions": auth_result.permissions,
|
298
323
|
}
|
299
324
|
else:
|
300
325
|
return jsonify({"error": "Authentication required"}), 401
|
301
|
-
|
326
|
+
|
302
327
|
# Check write permission
|
303
328
|
if "readonly" in user_info.get("roles", []):
|
304
329
|
return jsonify({"error": "Write permission required"}), 403
|
305
|
-
|
330
|
+
|
306
331
|
# Process request data
|
307
332
|
data = request.get_json()
|
308
|
-
return jsonify(
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
333
|
+
return jsonify(
|
334
|
+
{
|
335
|
+
"id": "data_123",
|
336
|
+
"created_by": user_info.get("username"),
|
337
|
+
"data": data,
|
338
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
339
|
+
}
|
340
|
+
)
|
341
|
+
|
342
|
+
@self.app.route("/api/v1/data/<data_id>", methods=["GET"])
|
316
343
|
def get_data(data_id):
|
317
344
|
"""Get data by ID (authenticated users)."""
|
318
345
|
# Try to get user info from middleware
|
319
|
-
user_info = getattr(g,
|
320
|
-
|
346
|
+
user_info = getattr(g, "user_info", None)
|
347
|
+
|
321
348
|
# If middleware didn't set user_info, try authentication
|
322
349
|
if not user_info:
|
323
350
|
auth_result = get_current_user_func()
|
@@ -325,91 +352,129 @@ class FlaskExample:
|
|
325
352
|
user_info = {
|
326
353
|
"username": auth_result.username,
|
327
354
|
"roles": auth_result.roles,
|
328
|
-
"permissions": auth_result.permissions
|
355
|
+
"permissions": auth_result.permissions,
|
329
356
|
}
|
330
357
|
else:
|
331
358
|
return jsonify({"error": "Authentication required"}), 401
|
332
|
-
|
333
|
-
return jsonify(
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
359
|
+
|
360
|
+
return jsonify(
|
361
|
+
{
|
362
|
+
"id": data_id,
|
363
|
+
"data": {"example": "data"},
|
364
|
+
"created_by": user_info.get("username"),
|
365
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
366
|
+
}
|
367
|
+
)
|
368
|
+
|
340
369
|
def _setup_error_handlers(self):
|
341
370
|
"""Setup custom error handlers."""
|
342
|
-
|
371
|
+
|
343
372
|
@self.app.errorhandler(401)
|
344
373
|
def unauthorized(error):
|
345
374
|
"""Handle unauthorized errors."""
|
346
|
-
return
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
375
|
+
return (
|
376
|
+
jsonify(
|
377
|
+
{
|
378
|
+
"error": "Unauthorized",
|
379
|
+
"message": "Authentication required",
|
380
|
+
"status_code": 401,
|
381
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
382
|
+
"path": request.path,
|
383
|
+
}
|
384
|
+
),
|
385
|
+
401,
|
386
|
+
)
|
387
|
+
|
354
388
|
@self.app.errorhandler(403)
|
355
389
|
def forbidden(error):
|
356
390
|
"""Handle forbidden errors."""
|
357
|
-
return
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
391
|
+
return (
|
392
|
+
jsonify(
|
393
|
+
{
|
394
|
+
"error": "Forbidden",
|
395
|
+
"message": "Insufficient permissions",
|
396
|
+
"status_code": 403,
|
397
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
398
|
+
"path": request.path,
|
399
|
+
}
|
400
|
+
),
|
401
|
+
403,
|
402
|
+
)
|
403
|
+
|
365
404
|
@self.app.errorhandler(404)
|
366
405
|
def not_found(error):
|
367
406
|
"""Handle not found errors."""
|
368
|
-
return
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
407
|
+
return (
|
408
|
+
jsonify(
|
409
|
+
{
|
410
|
+
"error": "Not Found",
|
411
|
+
"message": "Resource not found",
|
412
|
+
"status_code": 404,
|
413
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
414
|
+
"path": request.path,
|
415
|
+
}
|
416
|
+
),
|
417
|
+
404,
|
418
|
+
)
|
419
|
+
|
376
420
|
@self.app.errorhandler(429)
|
377
421
|
def too_many_requests(error):
|
378
422
|
"""Handle rate limit errors."""
|
379
|
-
return
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
423
|
+
return (
|
424
|
+
jsonify(
|
425
|
+
{
|
426
|
+
"error": "Too Many Requests",
|
427
|
+
"message": "Rate limit exceeded",
|
428
|
+
"status_code": 429,
|
429
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
430
|
+
"path": request.path,
|
431
|
+
}
|
432
|
+
),
|
433
|
+
429,
|
434
|
+
)
|
435
|
+
|
387
436
|
@self.app.errorhandler(500)
|
388
437
|
def internal_error(error):
|
389
438
|
"""Handle internal server errors."""
|
390
|
-
return
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
439
|
+
return (
|
440
|
+
jsonify(
|
441
|
+
{
|
442
|
+
"error": "Internal Server Error",
|
443
|
+
"message": "An unexpected error occurred",
|
444
|
+
"status_code": 500,
|
445
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
446
|
+
"path": request.path,
|
447
|
+
}
|
448
|
+
),
|
449
|
+
500,
|
450
|
+
)
|
451
|
+
|
398
452
|
@self.app.errorhandler(Exception)
|
399
453
|
def handle_exception(error):
|
400
454
|
"""Handle general exceptions."""
|
401
|
-
return
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
455
|
+
return (
|
456
|
+
jsonify(
|
457
|
+
{
|
458
|
+
"error": "Internal Server Error",
|
459
|
+
"message": "An unexpected error occurred",
|
460
|
+
"status_code": 500,
|
461
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
462
|
+
"path": request.path,
|
463
|
+
}
|
464
|
+
),
|
465
|
+
500,
|
466
|
+
)
|
467
|
+
|
468
|
+
def run(
|
469
|
+
self,
|
470
|
+
host: str = "0.0.0.0",
|
471
|
+
port: int = 5000,
|
472
|
+
ssl_context: Optional[tuple] = None,
|
473
|
+
debug: bool = False,
|
474
|
+
):
|
410
475
|
"""
|
411
476
|
Run the Flask application with security features.
|
412
|
-
|
477
|
+
|
413
478
|
Args:
|
414
479
|
host: Host to bind to
|
415
480
|
port: Port to bind to
|
@@ -420,81 +485,84 @@ class FlaskExample:
|
|
420
485
|
print(f"SSL Enabled: {self.config.ssl.enabled}")
|
421
486
|
print(f"Authentication Methods: {self.config.auth.methods}")
|
422
487
|
print(f"Rate Limiting: {self.config.rate_limit.enabled}")
|
423
|
-
|
488
|
+
|
424
489
|
if self.config.ssl.enabled and not ssl_context:
|
425
490
|
ssl_context = (self.config.ssl.cert_file, self.config.ssl.key_file)
|
426
|
-
|
427
|
-
self.app.run(
|
428
|
-
host=host,
|
429
|
-
port=port,
|
430
|
-
ssl_context=ssl_context,
|
431
|
-
debug=debug
|
432
|
-
)
|
491
|
+
|
492
|
+
self.app.run(host=host, port=port, ssl_context=ssl_context, debug=debug)
|
433
493
|
|
434
494
|
|
435
495
|
# Example usage and testing
|
436
496
|
class FlaskExampleTest:
|
437
497
|
"""Test class for Flask example functionality."""
|
438
|
-
|
498
|
+
|
439
499
|
@staticmethod
|
440
500
|
def test_authentication():
|
441
501
|
"""Test authentication functionality."""
|
442
502
|
example = FlaskExample()
|
443
|
-
|
503
|
+
|
444
504
|
# Test API key authentication
|
445
|
-
auth_result = example.security_manager.auth_manager.authenticate_api_key(
|
505
|
+
auth_result = example.security_manager.auth_manager.authenticate_api_key(
|
506
|
+
"admin_key_123"
|
507
|
+
)
|
446
508
|
assert auth_result.is_valid
|
447
509
|
assert auth_result.username == "admin"
|
448
510
|
assert "admin" in auth_result.roles
|
449
|
-
|
511
|
+
|
450
512
|
print("✅ API Key authentication test passed")
|
451
|
-
|
513
|
+
|
452
514
|
@staticmethod
|
453
515
|
def test_rate_limiting():
|
454
516
|
"""Test rate limiting functionality."""
|
455
517
|
example = FlaskExample()
|
456
|
-
|
518
|
+
|
457
519
|
# Test rate limiting
|
458
520
|
identifier = "test_user"
|
459
521
|
for i in range(5):
|
460
|
-
is_allowed = example.security_manager.rate_limiter.check_rate_limit(
|
522
|
+
is_allowed = example.security_manager.rate_limiter.check_rate_limit(
|
523
|
+
identifier
|
524
|
+
)
|
461
525
|
print(f"Request {i+1}: {'Allowed' if is_allowed else 'Blocked'}")
|
462
|
-
|
526
|
+
|
463
527
|
print("✅ Rate limiting test completed")
|
464
|
-
|
528
|
+
|
465
529
|
@staticmethod
|
466
530
|
def test_permissions():
|
467
531
|
"""Test permission checking."""
|
468
532
|
example = FlaskExample()
|
469
|
-
|
533
|
+
|
470
534
|
# Test admin permissions
|
471
535
|
admin_roles = ["admin"]
|
472
536
|
user_roles = ["user"]
|
473
537
|
readonly_roles = ["readonly"]
|
474
|
-
|
538
|
+
|
475
539
|
# Admin should have all permissions
|
476
540
|
admin_result = example.security_manager.permission_manager.validate_access(
|
477
541
|
admin_roles, ["read", "write", "delete"]
|
478
542
|
)
|
479
543
|
assert admin_result.is_valid
|
480
|
-
|
544
|
+
|
481
545
|
# User should have read and write permissions
|
482
546
|
user_result = example.security_manager.permission_manager.validate_access(
|
483
547
|
user_roles, ["read", "write"]
|
484
548
|
)
|
485
549
|
assert user_result.is_valid
|
486
|
-
|
550
|
+
|
487
551
|
# Readonly should only have read permission
|
488
|
-
readonly_read_result =
|
489
|
-
|
552
|
+
readonly_read_result = (
|
553
|
+
example.security_manager.permission_manager.validate_access(
|
554
|
+
readonly_roles, ["read"]
|
555
|
+
)
|
490
556
|
)
|
491
557
|
assert readonly_read_result.is_valid
|
492
|
-
|
493
|
-
readonly_write_result =
|
494
|
-
|
558
|
+
|
559
|
+
readonly_write_result = (
|
560
|
+
example.security_manager.permission_manager.validate_access(
|
561
|
+
readonly_roles, ["write"]
|
562
|
+
)
|
495
563
|
)
|
496
564
|
assert not readonly_write_result.is_valid
|
497
|
-
|
565
|
+
|
498
566
|
print("✅ Permission checking test passed")
|
499
567
|
|
500
568
|
|
@@ -504,43 +572,46 @@ if __name__ == "__main__":
|
|
504
572
|
FlaskExampleTest.test_authentication()
|
505
573
|
FlaskExampleTest.test_rate_limiting()
|
506
574
|
FlaskExampleTest.test_permissions()
|
507
|
-
|
575
|
+
|
508
576
|
# Start server in background thread for testing
|
509
577
|
print("\nStarting Flask Example Server in background...")
|
510
578
|
example = FlaskExample()
|
511
|
-
|
579
|
+
|
512
580
|
import threading
|
513
581
|
import time
|
582
|
+
|
514
583
|
import requests
|
515
|
-
|
584
|
+
|
516
585
|
# Start server in background thread
|
517
586
|
server_thread = threading.Thread(target=example.run, daemon=True)
|
518
587
|
server_thread.start()
|
519
|
-
|
588
|
+
|
520
589
|
# Wait for server to start
|
521
590
|
time.sleep(3)
|
522
|
-
|
591
|
+
|
523
592
|
try:
|
524
593
|
# Test server endpoints
|
525
594
|
print("Testing server endpoints...")
|
526
|
-
|
595
|
+
|
527
596
|
# Test health endpoint
|
528
597
|
response = requests.get("http://localhost:5000/health", timeout=5)
|
529
598
|
print(f"Health endpoint: {response.status_code}")
|
530
|
-
|
599
|
+
|
531
600
|
# Test metrics endpoint
|
532
601
|
response = requests.get("http://localhost:5000/metrics", timeout=5)
|
533
602
|
print(f"Metrics endpoint: {response.status_code}")
|
534
|
-
|
603
|
+
|
535
604
|
# Test protected endpoint with API key
|
536
605
|
headers = {"X-API-Key": "admin_key_123"}
|
537
|
-
response = requests.get(
|
606
|
+
response = requests.get(
|
607
|
+
"http://localhost:5000/api/v1/users/me", headers=headers, timeout=5
|
608
|
+
)
|
538
609
|
print(f"Protected endpoint: {response.status_code}")
|
539
|
-
|
610
|
+
|
540
611
|
print("✅ Server testing completed successfully")
|
541
|
-
|
612
|
+
|
542
613
|
except requests.exceptions.RequestException as e:
|
543
614
|
print(f"⚠️ Server testing failed: {e}")
|
544
|
-
|
615
|
+
|
545
616
|
# Server will automatically stop when main thread exits
|
546
617
|
print("Flask example completed")
|