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
@@ -25,15 +25,15 @@ import json
|
|
25
25
|
import logging
|
26
26
|
from typing import Any, Dict, List, Optional, Union
|
27
27
|
|
28
|
-
from flask import Request, Response,
|
28
|
+
from flask import Request, Response, current_app, jsonify, make_response, request
|
29
29
|
|
30
|
+
from ..schemas.models import AuthMethod, AuthResult, AuthStatus
|
30
31
|
from .security_middleware import SecurityMiddleware, SecurityMiddlewareError
|
31
|
-
from ..schemas.models import AuthResult, AuthStatus, AuthMethod
|
32
32
|
|
33
33
|
|
34
34
|
class FlaskMiddlewareError(SecurityMiddlewareError):
|
35
35
|
"""Raised when Flask middleware encounters an error."""
|
36
|
-
|
36
|
+
|
37
37
|
def __init__(self, message: str, error_code: int = -32020):
|
38
38
|
self.message = message
|
39
39
|
self.error_code = error_code
|
@@ -43,11 +43,11 @@ class FlaskMiddlewareError(SecurityMiddlewareError):
|
|
43
43
|
class FlaskSecurityMiddleware(SecurityMiddleware):
|
44
44
|
"""
|
45
45
|
Flask Security Middleware Class
|
46
|
-
|
46
|
+
|
47
47
|
This class provides Flask-specific implementation of the security
|
48
48
|
middleware. It integrates with Flask's WSGI system and handles
|
49
49
|
Flask Request/Response objects.
|
50
|
-
|
50
|
+
|
51
51
|
The FlaskSecurityMiddleware implements:
|
52
52
|
- Flask-specific request processing
|
53
53
|
- Flask authentication method handling
|
@@ -55,7 +55,7 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
55
55
|
- Flask-specific error handling
|
56
56
|
- Flask header management
|
57
57
|
- Flask rate limiting integration
|
58
|
-
|
58
|
+
|
59
59
|
Key Responsibilities:
|
60
60
|
- Process Flask requests through security pipeline
|
61
61
|
- Extract authentication credentials from Flask requests
|
@@ -63,206 +63,224 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
63
63
|
- Add security headers to Flask responses
|
64
64
|
- Handle Flask-specific request/response objects
|
65
65
|
- Integrate with Flask WSGI system
|
66
|
-
|
66
|
+
|
67
67
|
Attributes:
|
68
68
|
Inherits all attributes from SecurityMiddleware
|
69
69
|
_flask_app: Reference to Flask application (if available)
|
70
|
-
|
70
|
+
|
71
71
|
Example:
|
72
72
|
>>> from flask import Flask
|
73
73
|
>>> from mcp_security_framework.middleware import FlaskSecurityMiddleware
|
74
|
-
>>>
|
74
|
+
>>>
|
75
75
|
>>> app = Flask(__name__)
|
76
76
|
>>> security_manager = SecurityManager(config)
|
77
77
|
>>> middleware = FlaskSecurityMiddleware(security_manager)
|
78
78
|
>>> app.wsgi_app = middleware(app.wsgi_app)
|
79
|
-
|
79
|
+
|
80
80
|
Note:
|
81
81
|
This middleware should be integrated with Flask applications
|
82
82
|
by wrapping the WSGI application.
|
83
83
|
"""
|
84
|
-
|
84
|
+
|
85
85
|
def __init__(self, security_manager):
|
86
86
|
"""
|
87
87
|
Initialize Flask Security Middleware.
|
88
|
-
|
88
|
+
|
89
89
|
Args:
|
90
90
|
security_manager: Security manager instance containing
|
91
91
|
all security components and configuration.
|
92
|
-
|
92
|
+
|
93
93
|
Raises:
|
94
94
|
FlaskMiddlewareError: If initialization fails
|
95
95
|
"""
|
96
96
|
super().__init__(security_manager)
|
97
97
|
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
98
|
-
|
98
|
+
|
99
99
|
self.logger.info("Flask Security Middleware initialized")
|
100
|
-
|
100
|
+
|
101
101
|
def __call__(self, environ: Dict[str, Any], start_response) -> List[bytes]:
|
102
102
|
"""
|
103
103
|
Process Flask request through security middleware.
|
104
|
-
|
104
|
+
|
105
105
|
This method implements the security processing pipeline for
|
106
106
|
Flask requests, including rate limiting, authentication,
|
107
107
|
authorization, and security header management.
|
108
|
-
|
108
|
+
|
109
109
|
Args:
|
110
110
|
environ (Dict[str, Any]): WSGI environment dictionary
|
111
111
|
start_response: WSGI start_response callable
|
112
|
-
|
112
|
+
|
113
113
|
Returns:
|
114
114
|
List[bytes]: WSGI response body
|
115
|
-
|
115
|
+
|
116
116
|
Raises:
|
117
117
|
FlaskMiddlewareError: For middleware processing errors
|
118
118
|
"""
|
119
119
|
try:
|
120
120
|
# Create Flask request object from WSGI environ
|
121
121
|
flask_request = Request(environ)
|
122
|
-
|
122
|
+
|
123
123
|
# Check rate limit
|
124
124
|
if not self._check_rate_limit(flask_request):
|
125
125
|
return self._rate_limit_response(start_response)
|
126
|
-
|
126
|
+
|
127
127
|
# Check if public path
|
128
128
|
if self._is_public_path(flask_request):
|
129
129
|
# Process request normally
|
130
130
|
return self._process_request(environ, start_response, flask_request)
|
131
|
-
|
131
|
+
|
132
132
|
# Authenticate request
|
133
133
|
auth_result = self._authenticate_request(flask_request)
|
134
134
|
if not auth_result.is_valid:
|
135
135
|
return self._auth_error_response(auth_result, start_response)
|
136
|
-
|
136
|
+
|
137
137
|
# Validate permissions
|
138
138
|
if not self._validate_permissions(flask_request, auth_result):
|
139
139
|
return self._permission_error_response(start_response)
|
140
|
-
|
140
|
+
|
141
141
|
# Process request
|
142
|
-
return self._process_request(
|
143
|
-
|
142
|
+
return self._process_request(
|
143
|
+
environ, start_response, flask_request, auth_result
|
144
|
+
)
|
145
|
+
|
144
146
|
except Exception as e:
|
145
147
|
self.logger.error(
|
146
148
|
"Flask middleware processing failed",
|
147
149
|
extra={"error": str(e)},
|
148
|
-
exc_info=True
|
150
|
+
exc_info=True,
|
149
151
|
)
|
150
152
|
raise FlaskMiddlewareError(
|
151
|
-
f"Middleware processing failed: {str(e)}",
|
152
|
-
error_code=-32021
|
153
|
+
f"Middleware processing failed: {str(e)}", error_code=-32021
|
153
154
|
)
|
154
|
-
|
155
|
-
def _process_request(
|
156
|
-
|
155
|
+
|
156
|
+
def _process_request(
|
157
|
+
self,
|
158
|
+
environ: Dict[str, Any],
|
159
|
+
start_response,
|
160
|
+
flask_request: Request,
|
161
|
+
auth_result: AuthResult = None,
|
162
|
+
) -> List[bytes]:
|
157
163
|
"""
|
158
164
|
Process the actual request through the WSGI application.
|
159
|
-
|
165
|
+
|
160
166
|
Args:
|
161
167
|
environ (Dict[str, Any]): WSGI environment
|
162
168
|
start_response: WSGI start_response callable
|
163
169
|
flask_request (Request): Flask request object
|
164
170
|
auth_result (AuthResult): Authentication result (optional)
|
165
|
-
|
171
|
+
|
166
172
|
Returns:
|
167
173
|
List[bytes]: WSGI response body
|
168
174
|
"""
|
169
175
|
# Store auth result in request context for later use
|
170
176
|
if auth_result:
|
171
177
|
flask_request.auth_result = auth_result
|
172
|
-
|
178
|
+
|
173
179
|
# Call the original WSGI application
|
174
180
|
def custom_start_response(status, headers, exc_info=None):
|
175
181
|
# Add security headers
|
176
182
|
security_headers = self._get_security_headers()
|
177
183
|
headers.extend(security_headers)
|
178
|
-
|
184
|
+
|
179
185
|
# Log successful request
|
180
186
|
if auth_result:
|
181
|
-
self._log_security_event(
|
182
|
-
"
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
187
|
+
self._log_security_event(
|
188
|
+
"request_processed",
|
189
|
+
{
|
190
|
+
"ip_address": self._get_client_ip(flask_request),
|
191
|
+
"username": auth_result.username,
|
192
|
+
"path": flask_request.path,
|
193
|
+
"method": flask_request.method,
|
194
|
+
"status_code": int(status.split()[0]),
|
195
|
+
},
|
196
|
+
)
|
197
|
+
|
189
198
|
return start_response(status, headers, exc_info)
|
190
|
-
|
199
|
+
|
191
200
|
# Get the original WSGI app from the middleware chain
|
192
201
|
app = current_app._get_current_object()
|
193
202
|
return app(environ, custom_start_response)
|
194
|
-
|
203
|
+
|
195
204
|
def _get_rate_limit_identifier(self, request: Request) -> str:
|
196
205
|
"""
|
197
206
|
Get rate limit identifier from Flask request.
|
198
|
-
|
207
|
+
|
199
208
|
This method extracts the rate limit identifier from the Flask
|
200
209
|
request, typically using the client IP address.
|
201
|
-
|
210
|
+
|
202
211
|
Args:
|
203
212
|
request (Request): Flask request object
|
204
|
-
|
213
|
+
|
205
214
|
Returns:
|
206
215
|
str: Rate limit identifier (IP address)
|
207
216
|
"""
|
208
217
|
return self._get_client_ip(request)
|
209
|
-
|
218
|
+
|
210
219
|
def _get_request_path(self, request: Request) -> str:
|
211
220
|
"""
|
212
221
|
Get request path from Flask request.
|
213
|
-
|
222
|
+
|
214
223
|
Args:
|
215
224
|
request (Request): Flask request object
|
216
|
-
|
225
|
+
|
217
226
|
Returns:
|
218
227
|
str: Request path
|
219
228
|
"""
|
220
229
|
return request.path
|
221
|
-
|
230
|
+
|
222
231
|
def _get_required_permissions(self, request: Request) -> List[str]:
|
223
232
|
"""
|
224
233
|
Get required permissions for Flask request.
|
225
|
-
|
234
|
+
|
226
235
|
This method extracts required permissions from the Flask request,
|
227
236
|
typically from route decorators or request context.
|
228
|
-
|
237
|
+
|
229
238
|
Args:
|
230
239
|
request (Request): Flask request object
|
231
|
-
|
240
|
+
|
232
241
|
Returns:
|
233
242
|
List[str]: List of required permissions
|
234
243
|
"""
|
235
244
|
# Try to get permissions from request context
|
236
|
-
if hasattr(request,
|
245
|
+
if hasattr(request, "required_permissions"):
|
237
246
|
return request.required_permissions
|
238
|
-
|
247
|
+
|
239
248
|
# Try to get permissions from route decorators
|
240
|
-
if hasattr(request,
|
249
|
+
if hasattr(request, "endpoint"):
|
241
250
|
# Check if endpoint has permission decorators
|
242
251
|
endpoint = request.endpoint
|
243
|
-
if
|
252
|
+
if (
|
253
|
+
hasattr(endpoint, "required_permissions")
|
254
|
+
and endpoint.required_permissions is not None
|
255
|
+
):
|
244
256
|
return endpoint.required_permissions
|
245
257
|
# Check for permission decorators
|
246
|
-
if
|
258
|
+
if (
|
259
|
+
hasattr(endpoint, "__permissions__")
|
260
|
+
and endpoint.__permissions__ is not None
|
261
|
+
):
|
247
262
|
return endpoint.__permissions__
|
248
263
|
# Check for role-based decorators
|
249
|
-
if
|
264
|
+
if (
|
265
|
+
hasattr(endpoint, "required_roles")
|
266
|
+
and endpoint.required_roles is not None
|
267
|
+
):
|
250
268
|
return endpoint.required_roles
|
251
|
-
|
269
|
+
|
252
270
|
# Default: no specific permissions required
|
253
271
|
return []
|
254
|
-
|
272
|
+
|
255
273
|
def _try_auth_method(self, request: Request, method: str) -> AuthResult:
|
256
274
|
"""
|
257
275
|
Try authentication using specific method with Flask request.
|
258
|
-
|
276
|
+
|
259
277
|
This method attempts to authenticate the Flask request using
|
260
278
|
the specified authentication method.
|
261
|
-
|
279
|
+
|
262
280
|
Args:
|
263
281
|
request (Request): Flask request object
|
264
282
|
method (str): Authentication method to try
|
265
|
-
|
283
|
+
|
266
284
|
Returns:
|
267
285
|
AuthResult: Authentication result
|
268
286
|
"""
|
@@ -283,13 +301,13 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
283
301
|
roles=[],
|
284
302
|
auth_method=None,
|
285
303
|
error_code=-32022,
|
286
|
-
error_message=f"Unsupported authentication method: {method}"
|
304
|
+
error_message=f"Unsupported authentication method: {method}",
|
287
305
|
)
|
288
306
|
except Exception as e:
|
289
307
|
self.logger.error(
|
290
308
|
f"Authentication method {method} failed",
|
291
309
|
extra={"error": str(e)},
|
292
|
-
exc_info=True
|
310
|
+
exc_info=True,
|
293
311
|
)
|
294
312
|
return AuthResult(
|
295
313
|
is_valid=False,
|
@@ -298,16 +316,16 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
298
316
|
roles=[],
|
299
317
|
auth_method=None,
|
300
318
|
error_code=-32023,
|
301
|
-
error_message=f"Authentication method {method} failed: {str(e)}"
|
319
|
+
error_message=f"Authentication method {method} failed: {str(e)}",
|
302
320
|
)
|
303
|
-
|
321
|
+
|
304
322
|
def _try_api_key_auth(self, request: Request) -> AuthResult:
|
305
323
|
"""
|
306
324
|
Try API key authentication with Flask request.
|
307
|
-
|
325
|
+
|
308
326
|
Args:
|
309
327
|
request (Request): Flask request object
|
310
|
-
|
328
|
+
|
311
329
|
Returns:
|
312
330
|
AuthResult: Authentication result
|
313
331
|
"""
|
@@ -318,7 +336,7 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
318
336
|
auth_header = request.headers.get("Authorization")
|
319
337
|
if auth_header and auth_header.startswith("Bearer "):
|
320
338
|
api_key = auth_header[7:] # Remove "Bearer " prefix
|
321
|
-
|
339
|
+
|
322
340
|
if not api_key:
|
323
341
|
return AuthResult(
|
324
342
|
is_valid=False,
|
@@ -327,19 +345,19 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
327
345
|
roles=[],
|
328
346
|
auth_method=AuthMethod.API_KEY,
|
329
347
|
error_code=-32024,
|
330
|
-
error_message="API key not found in request"
|
348
|
+
error_message="API key not found in request",
|
331
349
|
)
|
332
|
-
|
350
|
+
|
333
351
|
# Authenticate using security manager
|
334
352
|
return self.security_manager.auth_manager.authenticate_api_key(api_key)
|
335
|
-
|
353
|
+
|
336
354
|
def _try_jwt_auth(self, request: Request) -> AuthResult:
|
337
355
|
"""
|
338
356
|
Try JWT authentication with Flask request.
|
339
|
-
|
357
|
+
|
340
358
|
Args:
|
341
359
|
request (Request): Flask request object
|
342
|
-
|
360
|
+
|
343
361
|
Returns:
|
344
362
|
AuthResult: Authentication result
|
345
363
|
"""
|
@@ -353,28 +371,28 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
353
371
|
roles=[],
|
354
372
|
auth_method=AuthMethod.JWT,
|
355
373
|
error_code=-32025,
|
356
|
-
error_message="JWT token not found in Authorization header"
|
374
|
+
error_message="JWT token not found in Authorization header",
|
357
375
|
)
|
358
|
-
|
376
|
+
|
359
377
|
token = auth_header[7:] # Remove "Bearer " prefix
|
360
|
-
|
378
|
+
|
361
379
|
# Authenticate using security manager
|
362
380
|
return self.security_manager.auth_manager.authenticate_jwt_token(token)
|
363
|
-
|
381
|
+
|
364
382
|
def _try_certificate_auth(self, request: Request) -> AuthResult:
|
365
383
|
"""
|
366
384
|
Try certificate authentication with Flask request.
|
367
|
-
|
385
|
+
|
368
386
|
Args:
|
369
387
|
request (Request): Flask request object
|
370
|
-
|
388
|
+
|
371
389
|
Returns:
|
372
390
|
AuthResult: Authentication result
|
373
391
|
"""
|
374
392
|
# For certificate authentication, we would typically need
|
375
393
|
# to access the client certificate from the SSL context
|
376
394
|
# This is more complex and depends on the SSL configuration
|
377
|
-
|
395
|
+
|
378
396
|
# For now, return not implemented
|
379
397
|
return AuthResult(
|
380
398
|
is_valid=False,
|
@@ -383,16 +401,16 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
383
401
|
roles=[],
|
384
402
|
auth_method=AuthMethod.CERTIFICATE,
|
385
403
|
error_code=-32026,
|
386
|
-
error_message="Certificate authentication not implemented"
|
404
|
+
error_message="Certificate authentication not implemented",
|
387
405
|
)
|
388
|
-
|
406
|
+
|
389
407
|
def _try_basic_auth(self, request: Request) -> AuthResult:
|
390
408
|
"""
|
391
409
|
Try basic authentication with Flask request.
|
392
|
-
|
410
|
+
|
393
411
|
Args:
|
394
412
|
request (Request): Flask request object
|
395
|
-
|
413
|
+
|
396
414
|
Returns:
|
397
415
|
AuthResult: Authentication result
|
398
416
|
"""
|
@@ -406,9 +424,9 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
406
424
|
roles=[],
|
407
425
|
auth_method=AuthMethod.BASIC,
|
408
426
|
error_code=-32027,
|
409
|
-
error_message="Basic authentication credentials not found"
|
427
|
+
error_message="Basic authentication credentials not found",
|
410
428
|
)
|
411
|
-
|
429
|
+
|
412
430
|
# Basic auth implementation would go here
|
413
431
|
# For now, return not implemented
|
414
432
|
return AuthResult(
|
@@ -418,74 +436,80 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
418
436
|
roles=[],
|
419
437
|
auth_method=AuthMethod.BASIC,
|
420
438
|
error_code=-32028,
|
421
|
-
error_message="Basic authentication not implemented"
|
439
|
+
error_message="Basic authentication not implemented",
|
422
440
|
)
|
423
|
-
|
424
|
-
def _apply_security_headers(
|
441
|
+
|
442
|
+
def _apply_security_headers(
|
443
|
+
self, response: Response, headers: Dict[str, str]
|
444
|
+
) -> None:
|
425
445
|
"""
|
426
446
|
Apply security headers to Flask response.
|
427
|
-
|
447
|
+
|
428
448
|
Args:
|
429
449
|
response (Response): Flask response object
|
430
450
|
headers (Dict[str, str]): Headers to apply
|
431
451
|
"""
|
432
452
|
for header_name, header_value in headers.items():
|
433
453
|
response.headers[header_name] = header_value
|
434
|
-
|
454
|
+
|
435
455
|
def _create_error_response(self, status_code: int, message: str) -> Response:
|
436
456
|
"""
|
437
457
|
Create error response for security violations.
|
438
|
-
|
458
|
+
|
439
459
|
Args:
|
440
460
|
status_code (int): HTTP status code
|
441
461
|
message (str): Error message
|
442
|
-
|
462
|
+
|
443
463
|
Returns:
|
444
464
|
Response: Flask error response
|
445
465
|
"""
|
446
466
|
return make_response(
|
447
|
-
jsonify(
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
467
|
+
jsonify(
|
468
|
+
{
|
469
|
+
"error": "Security violation",
|
470
|
+
"message": message,
|
471
|
+
"error_code": -32029,
|
472
|
+
}
|
473
|
+
),
|
474
|
+
status_code,
|
453
475
|
)
|
454
|
-
|
476
|
+
|
455
477
|
def _rate_limit_response(self, start_response) -> List[bytes]:
|
456
478
|
"""
|
457
479
|
Create rate limit exceeded response.
|
458
|
-
|
480
|
+
|
459
481
|
Args:
|
460
482
|
start_response: WSGI start_response callable
|
461
|
-
|
483
|
+
|
462
484
|
Returns:
|
463
485
|
List[bytes]: WSGI response body
|
464
486
|
"""
|
465
487
|
response_data = {
|
466
488
|
"error": "Rate limit exceeded",
|
467
489
|
"message": "Too many requests, please try again later",
|
468
|
-
"error_code": -32030
|
490
|
+
"error_code": -32030,
|
469
491
|
}
|
470
|
-
|
471
|
-
response_body = json.dumps(response_data).encode(
|
492
|
+
|
493
|
+
response_body = json.dumps(response_data).encode("utf-8")
|
472
494
|
headers = [
|
473
|
-
(
|
474
|
-
(
|
475
|
-
(
|
495
|
+
("Content-Type", "application/json"),
|
496
|
+
("Content-Length", str(len(response_body))),
|
497
|
+
("Retry-After", str(self.config.rate_limit.window_size_seconds)),
|
476
498
|
]
|
477
|
-
|
478
|
-
start_response(
|
499
|
+
|
500
|
+
start_response("429 Too Many Requests", headers)
|
479
501
|
return [response_body]
|
480
|
-
|
481
|
-
def _auth_error_response(
|
502
|
+
|
503
|
+
def _auth_error_response(
|
504
|
+
self, auth_result: AuthResult, start_response
|
505
|
+
) -> List[bytes]:
|
482
506
|
"""
|
483
507
|
Create authentication error response.
|
484
|
-
|
508
|
+
|
485
509
|
Args:
|
486
510
|
auth_result (AuthResult): Authentication result
|
487
511
|
start_response: WSGI start_response callable
|
488
|
-
|
512
|
+
|
489
513
|
Returns:
|
490
514
|
List[bytes]: WSGI response body
|
491
515
|
"""
|
@@ -493,51 +517,51 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
493
517
|
"error": "Authentication failed",
|
494
518
|
"message": auth_result.error_message or "Invalid credentials",
|
495
519
|
"error_code": auth_result.error_code,
|
496
|
-
"auth_method": auth_result.auth_method
|
520
|
+
"auth_method": auth_result.auth_method,
|
497
521
|
}
|
498
|
-
|
499
|
-
response_body = json.dumps(response_data).encode(
|
522
|
+
|
523
|
+
response_body = json.dumps(response_data).encode("utf-8")
|
500
524
|
headers = [
|
501
|
-
(
|
502
|
-
(
|
503
|
-
(
|
525
|
+
("Content-Type", "application/json"),
|
526
|
+
("Content-Length", str(len(response_body))),
|
527
|
+
("WWW-Authenticate", "Bearer, ApiKey"),
|
504
528
|
]
|
505
|
-
|
506
|
-
start_response(
|
529
|
+
|
530
|
+
start_response("401 Unauthorized", headers)
|
507
531
|
return [response_body]
|
508
|
-
|
532
|
+
|
509
533
|
def _permission_error_response(self, start_response) -> List[bytes]:
|
510
534
|
"""
|
511
535
|
Create permission denied response.
|
512
|
-
|
536
|
+
|
513
537
|
Args:
|
514
538
|
start_response: WSGI start_response callable
|
515
|
-
|
539
|
+
|
516
540
|
Returns:
|
517
541
|
List[bytes]: WSGI response body
|
518
542
|
"""
|
519
543
|
response_data = {
|
520
544
|
"error": "Permission denied",
|
521
545
|
"message": "Insufficient permissions to access this resource",
|
522
|
-
"error_code": -32031
|
546
|
+
"error_code": -32031,
|
523
547
|
}
|
524
|
-
|
525
|
-
response_body = json.dumps(response_data).encode(
|
548
|
+
|
549
|
+
response_body = json.dumps(response_data).encode("utf-8")
|
526
550
|
headers = [
|
527
|
-
(
|
528
|
-
(
|
551
|
+
("Content-Type", "application/json"),
|
552
|
+
("Content-Length", str(len(response_body))),
|
529
553
|
]
|
530
|
-
|
531
|
-
start_response(
|
554
|
+
|
555
|
+
start_response("403 Forbidden", headers)
|
532
556
|
return [response_body]
|
533
|
-
|
557
|
+
|
534
558
|
def _get_client_ip(self, request: Request) -> str:
|
535
559
|
"""
|
536
560
|
Get client IP address from Flask request.
|
537
|
-
|
561
|
+
|
538
562
|
Args:
|
539
563
|
request (Request): Flask request object
|
540
|
-
|
564
|
+
|
541
565
|
Returns:
|
542
566
|
str: Client IP address
|
543
567
|
"""
|
@@ -546,46 +570,48 @@ class FlaskSecurityMiddleware(SecurityMiddleware):
|
|
546
570
|
if forwarded_for:
|
547
571
|
# Take the first IP in the chain
|
548
572
|
return forwarded_for.split(",")[0].strip()
|
549
|
-
|
573
|
+
|
550
574
|
# Try to get IP from X-Real-IP header
|
551
575
|
real_ip = request.headers.get("X-Real-IP")
|
552
576
|
if real_ip:
|
553
577
|
return real_ip
|
554
|
-
|
578
|
+
|
555
579
|
# Fall back to remote address
|
556
580
|
if request.remote_addr:
|
557
581
|
return request.remote_addr
|
558
|
-
|
582
|
+
|
559
583
|
# Default fallback
|
560
584
|
# Fallback to default IP from config or environment
|
561
|
-
default_ip = getattr(self.config,
|
585
|
+
default_ip = getattr(self.config, "default_client_ip", None)
|
562
586
|
if default_ip:
|
563
587
|
return default_ip
|
564
|
-
|
588
|
+
|
565
589
|
# Use environment variable or default
|
566
590
|
import os
|
591
|
+
|
567
592
|
from ..constants import DEFAULT_CLIENT_IP
|
568
|
-
|
569
|
-
|
593
|
+
|
594
|
+
return os.environ.get("DEFAULT_CLIENT_IP", DEFAULT_CLIENT_IP)
|
595
|
+
|
570
596
|
def _get_security_headers(self) -> List[tuple]:
|
571
597
|
"""
|
572
598
|
Get security headers to add to responses.
|
573
|
-
|
599
|
+
|
574
600
|
Returns:
|
575
601
|
List[tuple]: List of (header_name, header_value) tuples
|
576
602
|
"""
|
577
603
|
headers = [
|
578
|
-
(
|
579
|
-
(
|
580
|
-
(
|
581
|
-
(
|
582
|
-
(
|
583
|
-
(
|
604
|
+
("X-Content-Type-Options", "nosniff"),
|
605
|
+
("X-Frame-Options", "DENY"),
|
606
|
+
("X-XSS-Protection", "1; mode=block"),
|
607
|
+
("Strict-Transport-Security", "max-age=31536000; includeSubDomains"),
|
608
|
+
("Content-Security-Policy", "default-src 'self'"),
|
609
|
+
("Referrer-Policy", "strict-origin-when-cross-origin"),
|
584
610
|
]
|
585
|
-
|
611
|
+
|
586
612
|
# Add custom security headers from config
|
587
613
|
if self.config.auth and self.config.auth.security_headers:
|
588
614
|
for header_name, header_value in self.config.auth.security_headers.items():
|
589
615
|
headers.append((header_name, header_value))
|
590
|
-
|
616
|
+
|
591
617
|
return headers
|