mcp-security-framework 0.1.0__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 +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,803 @@
|
|
1
|
+
"""
|
2
|
+
API Gateway Example Implementation
|
3
|
+
|
4
|
+
This module provides a complete example of how to implement the MCP Security Framework
|
5
|
+
in an API Gateway, including all abstract method implementations for real server usage.
|
6
|
+
|
7
|
+
The example demonstrates:
|
8
|
+
- API Gateway with security framework
|
9
|
+
- Request routing and load balancing
|
10
|
+
- Rate limiting at gateway level
|
11
|
+
- Certificate-based authentication
|
12
|
+
- Production-ready security features
|
13
|
+
- Comprehensive error handling
|
14
|
+
|
15
|
+
Author: MCP Security Team
|
16
|
+
Version: 1.0.0
|
17
|
+
License: MIT
|
18
|
+
"""
|
19
|
+
|
20
|
+
import os
|
21
|
+
import json
|
22
|
+
import logging
|
23
|
+
import asyncio
|
24
|
+
import aiohttp
|
25
|
+
from typing import Dict, List, Any, Optional
|
26
|
+
from datetime import datetime, timedelta
|
27
|
+
|
28
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
29
|
+
from mcp_security_framework.core.auth_manager import AuthManager
|
30
|
+
from mcp_security_framework.core.ssl_manager import SSLManager
|
31
|
+
from mcp_security_framework.core.permission_manager import PermissionManager
|
32
|
+
from mcp_security_framework.core.rate_limiter import RateLimiter
|
33
|
+
from mcp_security_framework.schemas.config import SecurityConfig, AuthConfig, SSLConfig
|
34
|
+
from mcp_security_framework.schemas.models import AuthResult, AuthStatus, AuthMethod
|
35
|
+
from mcp_security_framework.constants import (
|
36
|
+
DEFAULT_CLIENT_IP, DEFAULT_SECURITY_HEADERS, AUTH_METHODS,
|
37
|
+
ErrorCodes, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_TOO_MANY_REQUESTS
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
class APIGatewayExample:
|
42
|
+
"""
|
43
|
+
Complete API Gateway Example with Security Framework Implementation
|
44
|
+
|
45
|
+
This class demonstrates a production-ready API Gateway
|
46
|
+
with comprehensive security features including:
|
47
|
+
- Request routing and load balancing
|
48
|
+
- Gateway-level authentication and authorization
|
49
|
+
- Rate limiting at gateway level
|
50
|
+
- Certificate-based authentication
|
51
|
+
- Request/response transformation
|
52
|
+
- Comprehensive logging and monitoring
|
53
|
+
"""
|
54
|
+
|
55
|
+
def __init__(self, config_path: Optional[str] = None):
|
56
|
+
"""
|
57
|
+
Initialize API Gateway example with security configuration.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
config_path: Path to security configuration file
|
61
|
+
"""
|
62
|
+
self.config = self._load_config(config_path)
|
63
|
+
self.security_manager = SecurityManager(self.config)
|
64
|
+
self.logger = logging.getLogger(__name__)
|
65
|
+
self._setup_logging()
|
66
|
+
self._setup_routing_rules()
|
67
|
+
self._setup_load_balancer()
|
68
|
+
|
69
|
+
def _load_config(self, config_path: Optional[str]) -> SecurityConfig:
|
70
|
+
"""
|
71
|
+
Load security configuration from file or create default.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
config_path: Path to configuration file
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
SecurityConfig: Loaded configuration
|
78
|
+
"""
|
79
|
+
if config_path and os.path.exists(config_path):
|
80
|
+
with open(config_path, 'r') as f:
|
81
|
+
config_data = json.load(f)
|
82
|
+
return SecurityConfig(**config_data)
|
83
|
+
|
84
|
+
# Create production-ready API Gateway configuration
|
85
|
+
return SecurityConfig(
|
86
|
+
auth=AuthConfig(
|
87
|
+
enabled=True,
|
88
|
+
methods=[AUTH_METHODS["API_KEY"], AUTH_METHODS["JWT"], AUTH_METHODS["CERTIFICATE"]],
|
89
|
+
api_keys={
|
90
|
+
"gateway_key_123": {"username": "gateway-admin", "roles": ["gateway", "admin"]},
|
91
|
+
"client_key_456": {"username": "client-app", "roles": ["client", "user"]},
|
92
|
+
"service_key_789": {"username": "internal-service", "roles": ["service", "internal"]}
|
93
|
+
},
|
94
|
+
jwt_secret="your-super-secret-jwt-key-change-in-production",
|
95
|
+
jwt_algorithm="HS256",
|
96
|
+
jwt_expiry_hours=24,
|
97
|
+
public_paths=["/health", "/metrics", "/status"],
|
98
|
+
security_headers=DEFAULT_SECURITY_HEADERS
|
99
|
+
),
|
100
|
+
ssl=SSLConfig(
|
101
|
+
enabled=True,
|
102
|
+
cert_file="certs/gateway.crt",
|
103
|
+
key_file="certs/gateway.key",
|
104
|
+
ca_cert_file="certs/ca.crt",
|
105
|
+
verify_mode="CERT_REQUIRED",
|
106
|
+
min_version="TLSv1.2"
|
107
|
+
),
|
108
|
+
rate_limit={
|
109
|
+
"enabled": True,
|
110
|
+
"default_requests_per_minute": 500, # Gateway-level limits
|
111
|
+
"default_requests_per_hour": 5000,
|
112
|
+
"burst_limit": 10,
|
113
|
+
"window_size_seconds": 60,
|
114
|
+
"storage_backend": "redis",
|
115
|
+
"redis_config": {
|
116
|
+
"host": "redis-cluster",
|
117
|
+
"port": 6379,
|
118
|
+
"db": 0,
|
119
|
+
"password": None
|
120
|
+
},
|
121
|
+
"exempt_paths": ["/health", "/metrics", "/status"],
|
122
|
+
"exempt_roles": ["gateway", "admin"]
|
123
|
+
},
|
124
|
+
permissions={
|
125
|
+
"enabled": True,
|
126
|
+
"roles_file": "config/roles.json",
|
127
|
+
"default_role": "user",
|
128
|
+
"hierarchy_enabled": True
|
129
|
+
},
|
130
|
+
logging={
|
131
|
+
"enabled": True,
|
132
|
+
"level": "INFO",
|
133
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
134
|
+
"file_path": "logs/gateway.log",
|
135
|
+
"max_file_size": 10,
|
136
|
+
"backup_count": 5,
|
137
|
+
"console_output": True,
|
138
|
+
"json_format": True
|
139
|
+
}
|
140
|
+
)
|
141
|
+
|
142
|
+
def _setup_logging(self):
|
143
|
+
"""Setup logging configuration."""
|
144
|
+
if self.config.logging.enabled:
|
145
|
+
logging.basicConfig(
|
146
|
+
level=getattr(logging, self.config.logging.level),
|
147
|
+
format=self.config.logging.format,
|
148
|
+
handlers=[
|
149
|
+
logging.FileHandler(self.config.logging.file_path) if self.config.logging.file_path else logging.NullHandler(),
|
150
|
+
logging.StreamHandler() if self.config.logging.console_output else logging.NullHandler()
|
151
|
+
]
|
152
|
+
)
|
153
|
+
|
154
|
+
def _setup_routing_rules(self):
|
155
|
+
"""Setup routing rules for different services."""
|
156
|
+
self.routing_rules = {
|
157
|
+
"/api/v1/users": {
|
158
|
+
"service": "user-service",
|
159
|
+
"endpoints": ["https://user-service-1:8080", "https://user-service-2:8080"],
|
160
|
+
"timeout": 30,
|
161
|
+
"retries": 3,
|
162
|
+
"required_permissions": ["read", "user"]
|
163
|
+
},
|
164
|
+
"/api/v1/orders": {
|
165
|
+
"service": "order-service",
|
166
|
+
"endpoints": ["https://order-service-1:8081", "https://order-service-2:8081"],
|
167
|
+
"timeout": 60,
|
168
|
+
"retries": 3,
|
169
|
+
"required_permissions": ["read", "write", "order"]
|
170
|
+
},
|
171
|
+
"/api/v1/payments": {
|
172
|
+
"service": "payment-service",
|
173
|
+
"endpoints": ["https://payment-service-1:8082", "https://payment-service-2:8082"],
|
174
|
+
"timeout": 45,
|
175
|
+
"retries": 2,
|
176
|
+
"required_permissions": ["write", "payment"]
|
177
|
+
},
|
178
|
+
"/api/v1/admin": {
|
179
|
+
"service": "admin-service",
|
180
|
+
"endpoints": ["https://admin-service:8083"],
|
181
|
+
"timeout": 30,
|
182
|
+
"retries": 1,
|
183
|
+
"required_permissions": ["admin"]
|
184
|
+
}
|
185
|
+
}
|
186
|
+
|
187
|
+
def _setup_load_balancer(self):
|
188
|
+
"""Setup load balancer for service endpoints."""
|
189
|
+
self.load_balancer = {
|
190
|
+
"algorithm": "round_robin", # round_robin, least_connections, weighted
|
191
|
+
"health_check_interval": 30,
|
192
|
+
"health_check_timeout": 5,
|
193
|
+
"max_failures": 3
|
194
|
+
}
|
195
|
+
self.current_endpoint_index = {} # Track current endpoint for round-robin
|
196
|
+
|
197
|
+
async def route_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
198
|
+
"""
|
199
|
+
Route request through API Gateway with security validation.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
request_data: Request data including path, method, headers, body
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Dict[str, Any]: Response data
|
206
|
+
"""
|
207
|
+
try:
|
208
|
+
# Extract request components
|
209
|
+
path = request_data.get("path", "")
|
210
|
+
method = request_data.get("method", "GET")
|
211
|
+
headers = request_data.get("headers", {})
|
212
|
+
body = request_data.get("body", {})
|
213
|
+
client_ip = request_data.get("client_ip", DEFAULT_CLIENT_IP)
|
214
|
+
request_id = request_data.get("request_id", self._generate_request_id())
|
215
|
+
|
216
|
+
# Step 1: Rate limiting check
|
217
|
+
if not self.check_rate_limit(client_ip):
|
218
|
+
return self._create_error_response(
|
219
|
+
"Rate limit exceeded",
|
220
|
+
ErrorCodes.RATE_LIMIT_EXCEEDED_ERROR,
|
221
|
+
HTTP_TOO_MANY_REQUESTS,
|
222
|
+
request_id
|
223
|
+
)
|
224
|
+
|
225
|
+
# Step 2: Authentication
|
226
|
+
auth_result = self.authenticate_request(headers)
|
227
|
+
if not auth_result.is_valid:
|
228
|
+
return self._create_error_response(
|
229
|
+
"Authentication failed",
|
230
|
+
auth_result.error_code,
|
231
|
+
HTTP_UNAUTHORIZED,
|
232
|
+
request_id,
|
233
|
+
auth_result.error_message
|
234
|
+
)
|
235
|
+
|
236
|
+
# Step 3: Find routing rule
|
237
|
+
routing_rule = self._find_routing_rule(path)
|
238
|
+
if not routing_rule:
|
239
|
+
return self._create_error_response(
|
240
|
+
"Service not found",
|
241
|
+
ErrorCodes.GENERAL_ERROR,
|
242
|
+
404,
|
243
|
+
request_id
|
244
|
+
)
|
245
|
+
|
246
|
+
# Step 4: Authorization check
|
247
|
+
if not self.check_permissions(auth_result.roles, routing_rule["required_permissions"]):
|
248
|
+
return self._create_error_response(
|
249
|
+
"Insufficient permissions",
|
250
|
+
ErrorCodes.PERMISSION_DENIED_ERROR,
|
251
|
+
HTTP_FORBIDDEN,
|
252
|
+
request_id
|
253
|
+
)
|
254
|
+
|
255
|
+
# Step 5: Route request to backend service
|
256
|
+
response = await self._forward_request(
|
257
|
+
routing_rule, method, path, headers, body, request_id
|
258
|
+
)
|
259
|
+
|
260
|
+
# Step 6: Log security event
|
261
|
+
self._log_security_event("request_routed", {
|
262
|
+
"username": auth_result.username,
|
263
|
+
"path": path,
|
264
|
+
"method": method,
|
265
|
+
"service": routing_rule["service"],
|
266
|
+
"success": True,
|
267
|
+
"request_id": request_id,
|
268
|
+
"timestamp": datetime.utcnow().isoformat()
|
269
|
+
})
|
270
|
+
|
271
|
+
return response
|
272
|
+
|
273
|
+
except Exception as e:
|
274
|
+
self.logger.error(f"Request routing failed: {str(e)}")
|
275
|
+
return self._create_error_response(
|
276
|
+
"Internal server error",
|
277
|
+
ErrorCodes.GENERAL_ERROR,
|
278
|
+
500,
|
279
|
+
request_id if 'request_id' in locals() else self._generate_request_id()
|
280
|
+
)
|
281
|
+
|
282
|
+
def authenticate_request(self, headers: Dict[str, str]) -> AuthResult:
|
283
|
+
"""
|
284
|
+
Authenticate request using headers.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
headers: Request headers
|
288
|
+
|
289
|
+
Returns:
|
290
|
+
AuthResult: Authentication result
|
291
|
+
"""
|
292
|
+
try:
|
293
|
+
# Try API key authentication
|
294
|
+
api_key = headers.get("X-API-Key") or headers.get("Authorization", "").replace("Bearer ", "")
|
295
|
+
if api_key:
|
296
|
+
return self.security_manager.auth_manager.authenticate_api_key(api_key)
|
297
|
+
|
298
|
+
# Try JWT authentication
|
299
|
+
auth_header = headers.get("Authorization", "")
|
300
|
+
if auth_header.startswith("Bearer "):
|
301
|
+
token = auth_header[7:]
|
302
|
+
return self.security_manager.auth_manager.authenticate_jwt_token(token)
|
303
|
+
|
304
|
+
# Try certificate authentication (would be handled at TLS level)
|
305
|
+
client_cert = headers.get("X-Client-Cert")
|
306
|
+
if client_cert:
|
307
|
+
return self.security_manager.auth_manager.authenticate_certificate(client_cert)
|
308
|
+
|
309
|
+
return AuthResult(
|
310
|
+
is_valid=False,
|
311
|
+
status=AuthStatus.FAILED,
|
312
|
+
username=None,
|
313
|
+
roles=[],
|
314
|
+
auth_method=None,
|
315
|
+
error_code=ErrorCodes.AUTHENTICATION_ERROR,
|
316
|
+
error_message="No valid authentication credentials found"
|
317
|
+
)
|
318
|
+
|
319
|
+
except Exception as e:
|
320
|
+
self.logger.error(f"Authentication failed: {str(e)}")
|
321
|
+
return AuthResult(
|
322
|
+
is_valid=False,
|
323
|
+
status=AuthStatus.FAILED,
|
324
|
+
username=None,
|
325
|
+
roles=[],
|
326
|
+
auth_method=None,
|
327
|
+
error_code=ErrorCodes.AUTHENTICATION_ERROR,
|
328
|
+
error_message=str(e)
|
329
|
+
)
|
330
|
+
|
331
|
+
def check_permissions(self, user_roles: List[str], required_permissions: List[str]) -> bool:
|
332
|
+
"""
|
333
|
+
Check if user has required permissions.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
user_roles: User roles
|
337
|
+
required_permissions: Required permissions
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
bool: True if user has required permissions
|
341
|
+
"""
|
342
|
+
try:
|
343
|
+
return self.security_manager.permission_manager.validate_access(
|
344
|
+
user_roles, required_permissions
|
345
|
+
)
|
346
|
+
except Exception as e:
|
347
|
+
self.logger.error(f"Permission check failed: {str(e)}")
|
348
|
+
return False
|
349
|
+
|
350
|
+
def check_rate_limit(self, identifier: str) -> bool:
|
351
|
+
"""
|
352
|
+
Check if request is within rate limits.
|
353
|
+
|
354
|
+
Args:
|
355
|
+
identifier: Request identifier (IP, user ID, etc.)
|
356
|
+
|
357
|
+
Returns:
|
358
|
+
bool: True if request is within rate limits
|
359
|
+
"""
|
360
|
+
try:
|
361
|
+
return self.security_manager.rate_limiter.check_rate_limit(identifier)
|
362
|
+
except Exception as e:
|
363
|
+
self.logger.error(f"Rate limit check failed: {str(e)}")
|
364
|
+
return True # Allow request if rate limiting fails
|
365
|
+
|
366
|
+
def _find_routing_rule(self, path: str) -> Optional[Dict[str, Any]]:
|
367
|
+
"""
|
368
|
+
Find routing rule for the given path.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
path: Request path
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
Optional[Dict[str, Any]]: Routing rule or None
|
375
|
+
"""
|
376
|
+
for route_path, rule in self.routing_rules.items():
|
377
|
+
if path.startswith(route_path):
|
378
|
+
return rule
|
379
|
+
return None
|
380
|
+
|
381
|
+
async def _forward_request(self, routing_rule: Dict[str, Any], method: str, path: str,
|
382
|
+
headers: Dict[str, str], body: Dict[str, Any], request_id: str) -> Dict[str, Any]:
|
383
|
+
"""
|
384
|
+
Forward request to backend service.
|
385
|
+
|
386
|
+
Args:
|
387
|
+
routing_rule: Routing rule for the service
|
388
|
+
method: HTTP method
|
389
|
+
path: Request path
|
390
|
+
headers: Request headers
|
391
|
+
body: Request body
|
392
|
+
request_id: Request ID
|
393
|
+
|
394
|
+
Returns:
|
395
|
+
Dict[str, Any]: Response from backend service
|
396
|
+
"""
|
397
|
+
try:
|
398
|
+
# Select endpoint using load balancer
|
399
|
+
endpoint = self._select_endpoint(routing_rule["service"], routing_rule["endpoints"])
|
400
|
+
|
401
|
+
# Prepare headers for backend service
|
402
|
+
backend_headers = {
|
403
|
+
**headers,
|
404
|
+
"X-Request-ID": request_id,
|
405
|
+
"X-Gateway": "true",
|
406
|
+
"X-Forwarded-For": headers.get("X-Forwarded-For", DEFAULT_CLIENT_IP),
|
407
|
+
"X-Original-Path": path
|
408
|
+
}
|
409
|
+
|
410
|
+
# Create SSL context if needed
|
411
|
+
ssl_context = None
|
412
|
+
if self.config.ssl.enabled:
|
413
|
+
ssl_context = self.security_manager.ssl_manager.create_client_context()
|
414
|
+
|
415
|
+
# Make request to backend service
|
416
|
+
timeout = aiohttp.ClientTimeout(total=routing_rule["timeout"])
|
417
|
+
|
418
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
419
|
+
url = f"{endpoint}{path}"
|
420
|
+
|
421
|
+
if method.upper() == "GET":
|
422
|
+
async with session.get(url, headers=backend_headers, ssl=ssl_context) as response:
|
423
|
+
return await self._process_response(response, request_id)
|
424
|
+
elif method.upper() == "POST":
|
425
|
+
async with session.post(url, headers=backend_headers, json=body, ssl=ssl_context) as response:
|
426
|
+
return await self._process_response(response, request_id)
|
427
|
+
elif method.upper() == "PUT":
|
428
|
+
async with session.put(url, headers=backend_headers, json=body, ssl=ssl_context) as response:
|
429
|
+
return await self._process_response(response, request_id)
|
430
|
+
elif method.upper() == "DELETE":
|
431
|
+
async with session.delete(url, headers=backend_headers, ssl=ssl_context) as response:
|
432
|
+
return await self._process_response(response, request_id)
|
433
|
+
else:
|
434
|
+
raise ValueError(f"Unsupported HTTP method: {method}")
|
435
|
+
|
436
|
+
except Exception as e:
|
437
|
+
self.logger.error(f"Request forwarding failed: {str(e)}")
|
438
|
+
return self._create_error_response(
|
439
|
+
"Backend service unavailable",
|
440
|
+
ErrorCodes.GENERAL_ERROR,
|
441
|
+
503,
|
442
|
+
request_id
|
443
|
+
)
|
444
|
+
|
445
|
+
def _select_endpoint(self, service: str, endpoints: List[str]) -> str:
|
446
|
+
"""
|
447
|
+
Select endpoint using load balancer algorithm.
|
448
|
+
|
449
|
+
Args:
|
450
|
+
service: Service name
|
451
|
+
endpoints: List of available endpoints
|
452
|
+
|
453
|
+
Returns:
|
454
|
+
str: Selected endpoint
|
455
|
+
"""
|
456
|
+
if not endpoints:
|
457
|
+
raise ValueError(f"No endpoints available for service: {service}")
|
458
|
+
|
459
|
+
if self.load_balancer["algorithm"] == "round_robin":
|
460
|
+
# Simple round-robin implementation
|
461
|
+
if service not in self.current_endpoint_index:
|
462
|
+
self.current_endpoint_index[service] = 0
|
463
|
+
|
464
|
+
endpoint = endpoints[self.current_endpoint_index[service]]
|
465
|
+
self.current_endpoint_index[service] = (self.current_endpoint_index[service] + 1) % len(endpoints)
|
466
|
+
return endpoint
|
467
|
+
else:
|
468
|
+
# Default to first endpoint
|
469
|
+
return endpoints[0]
|
470
|
+
|
471
|
+
async def _process_response(self, response: aiohttp.ClientResponse, request_id: str) -> Dict[str, Any]:
|
472
|
+
"""
|
473
|
+
Process response from backend service.
|
474
|
+
|
475
|
+
Args:
|
476
|
+
response: Response from backend service
|
477
|
+
request_id: Request ID
|
478
|
+
|
479
|
+
Returns:
|
480
|
+
Dict[str, Any]: Processed response
|
481
|
+
"""
|
482
|
+
try:
|
483
|
+
response_data = await response.json()
|
484
|
+
|
485
|
+
# Add gateway headers
|
486
|
+
response_headers = {
|
487
|
+
"X-Gateway": "true",
|
488
|
+
"X-Request-ID": request_id,
|
489
|
+
"X-Response-Time": str(response.headers.get("X-Response-Time", "")),
|
490
|
+
**DEFAULT_SECURITY_HEADERS
|
491
|
+
}
|
492
|
+
|
493
|
+
return {
|
494
|
+
"success": True,
|
495
|
+
"status_code": response.status,
|
496
|
+
"headers": response_headers,
|
497
|
+
"data": response_data,
|
498
|
+
"request_id": request_id,
|
499
|
+
"timestamp": datetime.utcnow().isoformat()
|
500
|
+
}
|
501
|
+
|
502
|
+
except Exception as e:
|
503
|
+
self.logger.error(f"Response processing failed: {str(e)}")
|
504
|
+
return self._create_error_response(
|
505
|
+
"Response processing failed",
|
506
|
+
ErrorCodes.GENERAL_ERROR,
|
507
|
+
500,
|
508
|
+
request_id
|
509
|
+
)
|
510
|
+
|
511
|
+
def _create_error_response(self, message: str, error_code: int, status_code: int,
|
512
|
+
request_id: str, details: Optional[str] = None) -> Dict[str, Any]:
|
513
|
+
"""
|
514
|
+
Create error response.
|
515
|
+
|
516
|
+
Args:
|
517
|
+
message: Error message
|
518
|
+
error_code: Error code
|
519
|
+
status_code: HTTP status code
|
520
|
+
request_id: Request ID
|
521
|
+
details: Additional error details
|
522
|
+
|
523
|
+
Returns:
|
524
|
+
Dict[str, Any]: Error response
|
525
|
+
"""
|
526
|
+
return {
|
527
|
+
"success": False,
|
528
|
+
"error": message,
|
529
|
+
"error_code": error_code,
|
530
|
+
"status_code": status_code,
|
531
|
+
"request_id": request_id,
|
532
|
+
"details": details,
|
533
|
+
"timestamp": datetime.utcnow().isoformat()
|
534
|
+
}
|
535
|
+
|
536
|
+
def _generate_request_id(self) -> str:
|
537
|
+
"""Generate unique request ID for tracing."""
|
538
|
+
import uuid
|
539
|
+
return str(uuid.uuid4())
|
540
|
+
|
541
|
+
def _log_security_event(self, event_type: str, details: Dict[str, Any]):
|
542
|
+
"""
|
543
|
+
Log security event.
|
544
|
+
|
545
|
+
Args:
|
546
|
+
event_type: Type of security event
|
547
|
+
details: Event details
|
548
|
+
"""
|
549
|
+
try:
|
550
|
+
self.logger.info(
|
551
|
+
f"Security event: {event_type}",
|
552
|
+
extra={
|
553
|
+
"event_type": event_type,
|
554
|
+
"gateway": "api-gateway",
|
555
|
+
"timestamp": details.get("timestamp"),
|
556
|
+
"username": details.get("username"),
|
557
|
+
"path": details.get("path"),
|
558
|
+
"method": details.get("method"),
|
559
|
+
"service": details.get("service"),
|
560
|
+
"success": details.get("success"),
|
561
|
+
"request_id": details.get("request_id"),
|
562
|
+
**details
|
563
|
+
}
|
564
|
+
)
|
565
|
+
except Exception as e:
|
566
|
+
self.logger.error(f"Failed to log security event: {str(e)}")
|
567
|
+
|
568
|
+
async def health_check(self) -> Dict[str, Any]:
|
569
|
+
"""
|
570
|
+
Perform health check for the API Gateway.
|
571
|
+
|
572
|
+
Returns:
|
573
|
+
Dict[str, Any]: Health check result
|
574
|
+
"""
|
575
|
+
try:
|
576
|
+
# Check security manager health
|
577
|
+
security_healthy = self.security_manager is not None
|
578
|
+
|
579
|
+
# Check rate limiter health
|
580
|
+
rate_limit_healthy = self.security_manager.rate_limiter is not None
|
581
|
+
|
582
|
+
# Check routing rules
|
583
|
+
routing_healthy = len(self.routing_rules) > 0
|
584
|
+
|
585
|
+
# Check SSL configuration
|
586
|
+
ssl_healthy = self.config.ssl.enabled and os.path.exists(self.config.ssl.cert_file)
|
587
|
+
|
588
|
+
overall_healthy = all([
|
589
|
+
security_healthy,
|
590
|
+
rate_limit_healthy,
|
591
|
+
routing_healthy,
|
592
|
+
ssl_healthy
|
593
|
+
])
|
594
|
+
|
595
|
+
return {
|
596
|
+
"status": "healthy" if overall_healthy else "unhealthy",
|
597
|
+
"gateway": "api-gateway",
|
598
|
+
"timestamp": datetime.utcnow().isoformat(),
|
599
|
+
"checks": {
|
600
|
+
"security_manager": security_healthy,
|
601
|
+
"rate_limiter": rate_limit_healthy,
|
602
|
+
"routing_rules": routing_healthy,
|
603
|
+
"ssl_configuration": ssl_healthy
|
604
|
+
}
|
605
|
+
}
|
606
|
+
except Exception as e:
|
607
|
+
self.logger.error(f"Health check failed: {str(e)}")
|
608
|
+
return {
|
609
|
+
"status": "unhealthy",
|
610
|
+
"gateway": "api-gateway",
|
611
|
+
"error": str(e),
|
612
|
+
"timestamp": datetime.utcnow().isoformat()
|
613
|
+
}
|
614
|
+
|
615
|
+
async def get_metrics(self) -> Dict[str, Any]:
|
616
|
+
"""
|
617
|
+
Get API Gateway metrics.
|
618
|
+
|
619
|
+
Returns:
|
620
|
+
Dict[str, Any]: Metrics data
|
621
|
+
"""
|
622
|
+
try:
|
623
|
+
# Get rate limiter metrics
|
624
|
+
rate_limit_stats = self.security_manager.rate_limiter.get_statistics()
|
625
|
+
|
626
|
+
# Get routing statistics
|
627
|
+
routing_stats = {
|
628
|
+
"total_routes": len(self.routing_rules),
|
629
|
+
"services": list(set(rule["service"] for rule in self.routing_rules.values())),
|
630
|
+
"load_balancer_algorithm": self.load_balancer["algorithm"]
|
631
|
+
}
|
632
|
+
|
633
|
+
return {
|
634
|
+
"gateway": "api-gateway",
|
635
|
+
"timestamp": datetime.utcnow().isoformat(),
|
636
|
+
"rate_limiting": rate_limit_stats,
|
637
|
+
"routing": routing_stats,
|
638
|
+
"security": self.get_security_status()
|
639
|
+
}
|
640
|
+
except Exception as e:
|
641
|
+
self.logger.error(f"Failed to get metrics: {str(e)}")
|
642
|
+
return {
|
643
|
+
"gateway": "api-gateway",
|
644
|
+
"error": str(e),
|
645
|
+
"timestamp": datetime.utcnow().isoformat()
|
646
|
+
}
|
647
|
+
|
648
|
+
def get_security_status(self) -> Dict[str, Any]:
|
649
|
+
"""
|
650
|
+
Get security framework status.
|
651
|
+
|
652
|
+
Returns:
|
653
|
+
Dict[str, Any]: Security status information
|
654
|
+
"""
|
655
|
+
return {
|
656
|
+
"gateway": "api-gateway",
|
657
|
+
"ssl_enabled": self.config.ssl.enabled,
|
658
|
+
"auth_enabled": self.config.auth.enabled,
|
659
|
+
"rate_limiting_enabled": self.config.rate_limit.enabled,
|
660
|
+
"permissions_enabled": self.config.permissions.enabled,
|
661
|
+
"logging_enabled": self.config.logging.enabled,
|
662
|
+
"auth_methods": self.config.auth.methods,
|
663
|
+
"timestamp": datetime.utcnow().isoformat()
|
664
|
+
}
|
665
|
+
|
666
|
+
|
667
|
+
# Example usage and testing
|
668
|
+
class APIGatewayExampleTest:
|
669
|
+
"""Test class for API Gateway example functionality."""
|
670
|
+
|
671
|
+
@staticmethod
|
672
|
+
async def test_authentication():
|
673
|
+
"""Test authentication functionality."""
|
674
|
+
gateway = APIGatewayExample()
|
675
|
+
|
676
|
+
# Test API key authentication
|
677
|
+
headers = {"X-API-Key": "gateway_key_123"}
|
678
|
+
auth_result = gateway.authenticate_request(headers)
|
679
|
+
assert auth_result.is_valid
|
680
|
+
assert auth_result.username == "gateway-admin"
|
681
|
+
assert "gateway" in auth_result.roles
|
682
|
+
|
683
|
+
print("✅ API Key authentication test passed")
|
684
|
+
|
685
|
+
@staticmethod
|
686
|
+
async def test_permissions():
|
687
|
+
"""Test permission checking."""
|
688
|
+
gateway = APIGatewayExample()
|
689
|
+
|
690
|
+
# Test gateway permissions
|
691
|
+
gateway_roles = ["gateway"]
|
692
|
+
client_roles = ["client"]
|
693
|
+
admin_roles = ["admin"]
|
694
|
+
|
695
|
+
# Gateway should have gateway permissions
|
696
|
+
assert gateway.check_permissions(gateway_roles, ["read", "write"])
|
697
|
+
|
698
|
+
# Client should have client permissions
|
699
|
+
assert gateway.check_permissions(client_roles, ["read"])
|
700
|
+
|
701
|
+
# Admin should have all permissions
|
702
|
+
assert gateway.check_permissions(admin_roles, ["read", "write", "delete"])
|
703
|
+
|
704
|
+
print("✅ Permission checking test passed")
|
705
|
+
|
706
|
+
@staticmethod
|
707
|
+
async def test_rate_limiting():
|
708
|
+
"""Test rate limiting functionality."""
|
709
|
+
gateway = APIGatewayExample()
|
710
|
+
|
711
|
+
# Test rate limiting
|
712
|
+
identifier = "test_client"
|
713
|
+
for i in range(5):
|
714
|
+
is_allowed = gateway.check_rate_limit(identifier)
|
715
|
+
print(f"Request {i+1}: {'Allowed' if is_allowed else 'Blocked'}")
|
716
|
+
|
717
|
+
print("✅ Rate limiting test completed")
|
718
|
+
|
719
|
+
@staticmethod
|
720
|
+
async def test_routing():
|
721
|
+
"""Test routing functionality."""
|
722
|
+
gateway = APIGatewayExample()
|
723
|
+
|
724
|
+
# Test routing rule finding
|
725
|
+
rule = gateway._find_routing_rule("/api/v1/users")
|
726
|
+
assert rule is not None
|
727
|
+
assert rule["service"] == "user-service"
|
728
|
+
|
729
|
+
rule = gateway._find_routing_rule("/api/v1/orders")
|
730
|
+
assert rule is not None
|
731
|
+
assert rule["service"] == "order-service"
|
732
|
+
|
733
|
+
rule = gateway._find_routing_rule("/unknown/path")
|
734
|
+
assert rule is None
|
735
|
+
|
736
|
+
print("✅ Routing test passed")
|
737
|
+
|
738
|
+
@staticmethod
|
739
|
+
async def test_health_check():
|
740
|
+
"""Test health check functionality."""
|
741
|
+
gateway = APIGatewayExample()
|
742
|
+
|
743
|
+
health = await gateway.health_check()
|
744
|
+
assert "status" in health
|
745
|
+
assert "gateway" in health
|
746
|
+
assert health["gateway"] == "api-gateway"
|
747
|
+
|
748
|
+
print("✅ Health check test passed")
|
749
|
+
|
750
|
+
@staticmethod
|
751
|
+
async def test_metrics():
|
752
|
+
"""Test metrics functionality."""
|
753
|
+
gateway = APIGatewayExample()
|
754
|
+
|
755
|
+
metrics = await gateway.get_metrics()
|
756
|
+
assert "gateway" in metrics
|
757
|
+
assert "timestamp" in metrics
|
758
|
+
assert metrics["gateway"] == "api-gateway"
|
759
|
+
|
760
|
+
print("✅ Metrics test passed")
|
761
|
+
|
762
|
+
|
763
|
+
async def main():
|
764
|
+
"""Main function for testing and example usage."""
|
765
|
+
# Run tests
|
766
|
+
print("Running API Gateway Example Tests...")
|
767
|
+
await APIGatewayExampleTest.test_authentication()
|
768
|
+
await APIGatewayExampleTest.test_permissions()
|
769
|
+
await APIGatewayExampleTest.test_rate_limiting()
|
770
|
+
await APIGatewayExampleTest.test_routing()
|
771
|
+
await APIGatewayExampleTest.test_health_check()
|
772
|
+
await APIGatewayExampleTest.test_metrics()
|
773
|
+
|
774
|
+
# Example usage
|
775
|
+
print("\nExample Usage:")
|
776
|
+
gateway = APIGatewayExample()
|
777
|
+
|
778
|
+
# Route a request
|
779
|
+
request_data = {
|
780
|
+
"path": "/api/v1/users/123",
|
781
|
+
"method": "GET",
|
782
|
+
"headers": {
|
783
|
+
"X-API-Key": "client_key_456",
|
784
|
+
"X-Forwarded-For": "192.168.1.100"
|
785
|
+
},
|
786
|
+
"body": {},
|
787
|
+
"client_ip": "192.168.1.100"
|
788
|
+
}
|
789
|
+
|
790
|
+
result = await gateway.route_request(request_data)
|
791
|
+
print(f"Request result: {result}")
|
792
|
+
|
793
|
+
# Get health status
|
794
|
+
health = await gateway.health_check()
|
795
|
+
print(f"Health status: {health}")
|
796
|
+
|
797
|
+
# Get metrics
|
798
|
+
metrics = await gateway.get_metrics()
|
799
|
+
print(f"Metrics: {metrics}")
|
800
|
+
|
801
|
+
|
802
|
+
if __name__ == "__main__":
|
803
|
+
asyncio.run(main())
|