refactor: add API versioning and security headers utilities
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
- Create APIVersion enum and api_version decorator - Implement APIVersionRouter for version routing - Add deprecation warnings and sunset dates for deprecated APIs - Create SecurityHeaders dataclass for security headers - Implement SecurityHeadersMiddleware for applying headers - Create CORSMiddleware for CORS policy management - Add production and development security header presets - Implement strict and permissive CORS configurations
This commit is contained in:
136
aitbc/api_versioning.py
Normal file
136
aitbc/api_versioning.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
API versioning utilities for AITBC
|
||||||
|
Provides API versioning for backward compatibility
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Dict, Any, Callable
|
||||||
|
from functools import wraps
|
||||||
|
from enum import Enum
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .aitbc_logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersion(Enum):
|
||||||
|
"""API version enumeration"""
|
||||||
|
V1 = "v1"
|
||||||
|
V2 = "v2"
|
||||||
|
LATEST = "latest"
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedAPIError(Exception):
|
||||||
|
"""Exception raised when deprecated API is called"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def api_version(
|
||||||
|
version: APIVersion = APIVersion.V1,
|
||||||
|
deprecated: bool = False,
|
||||||
|
deprecation_date: Optional[datetime] = None,
|
||||||
|
sunset_date: Optional[datetime] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Decorator to mark API endpoint with version information
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: API version
|
||||||
|
deprecated: Whether the endpoint is deprecated
|
||||||
|
deprecation_date: Date when endpoint was deprecated
|
||||||
|
sunset_date: Date when endpoint will be removed
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if deprecated:
|
||||||
|
warning_msg = f"API endpoint {func.__name__} is deprecated"
|
||||||
|
if sunset_date:
|
||||||
|
warning_msg += f" and will be removed on {sunset_date.isoformat()}"
|
||||||
|
logger.warning(warning_msg)
|
||||||
|
|
||||||
|
# Add version info to response if applicable
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
|
||||||
|
if isinstance(result, dict):
|
||||||
|
result["_meta"] = result.get("_meta", {})
|
||||||
|
result["_meta"]["api_version"] = version.value
|
||||||
|
if deprecated:
|
||||||
|
result["_meta"]["deprecated"] = True
|
||||||
|
if deprecation_date:
|
||||||
|
result["_meta"]["deprecated_since"] = deprecation_date.isoformat()
|
||||||
|
if sunset_date:
|
||||||
|
result["_meta"]["sunset_date"] = sunset_date.isoformat()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
wrapper._api_version = version.value
|
||||||
|
wrapper._deprecated = deprecated
|
||||||
|
wrapper._deprecation_date = deprecation_date
|
||||||
|
wrapper._sunset_date = sunset_date
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersionRouter:
|
||||||
|
"""
|
||||||
|
API version router for handling multiple API versions.
|
||||||
|
Routes requests to appropriate version handlers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize API version router"""
|
||||||
|
self._version_handlers: Dict[str, Callable] = {}
|
||||||
|
self._default_version = APIVersion.V1.value
|
||||||
|
|
||||||
|
def register_handler(self, version: str, handler: Callable) -> None:
|
||||||
|
"""
|
||||||
|
Register a handler for a specific API version
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: API version string
|
||||||
|
handler: Handler function
|
||||||
|
"""
|
||||||
|
self._version_handlers[version] = handler
|
||||||
|
logger.info(f"Registered handler for API version {version}")
|
||||||
|
|
||||||
|
def set_default_version(self, version: str) -> None:
|
||||||
|
"""
|
||||||
|
Set default API version
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Default version string
|
||||||
|
"""
|
||||||
|
self._default_version = version
|
||||||
|
logger.info(f"Set default API version to {version}")
|
||||||
|
|
||||||
|
def route(self, version: Optional[str] = None) -> Callable:
|
||||||
|
"""
|
||||||
|
Route request to appropriate version handler
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Requested version (uses default if None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Handler function
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If version is not supported
|
||||||
|
"""
|
||||||
|
target_version = version or self._default_version
|
||||||
|
|
||||||
|
if target_version not in self._version_handlers:
|
||||||
|
raise ValueError(f"Unsupported API version: {target_version}")
|
||||||
|
|
||||||
|
return self._version_handlers[target_version]
|
||||||
|
|
||||||
|
def get_supported_versions(self) -> list:
|
||||||
|
"""
|
||||||
|
Get list of supported API versions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of supported version strings
|
||||||
|
"""
|
||||||
|
return list(self._version_handlers.keys())
|
||||||
234
aitbc/security_headers.py
Normal file
234
aitbc/security_headers.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
"""
|
||||||
|
Security headers and CORS utilities for AITBC web services
|
||||||
|
Provides security headers and CORS policies configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Set
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .aitbc_logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SecurityHeaders:
|
||||||
|
"""Security headers configuration"""
|
||||||
|
X_Content_Type_Options: str = "nosniff"
|
||||||
|
X_Frame_Options: str = "DENY"
|
||||||
|
X_XSS_Protection: str = "1; mode=block"
|
||||||
|
Strict_Transport_Security: str = "max-age=31536000; includeSubDomains"
|
||||||
|
Content_Security_Policy: str = "default-src 'self'"
|
||||||
|
Referrer_Policy: str = "strict-origin-when-cross-origin"
|
||||||
|
Permissions_Policy: str = ""
|
||||||
|
Cache_Control: str = "no-cache, no-store, must-revalidate"
|
||||||
|
Pragma: str = "no-cache"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CORSConfig:
|
||||||
|
"""CORS configuration"""
|
||||||
|
allow_origins: List[str]
|
||||||
|
allow_methods: List[str]
|
||||||
|
allow_headers: List[str]
|
||||||
|
allow_credentials: bool = False
|
||||||
|
expose_headers: List[str] = None
|
||||||
|
max_age: int = 3600
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityHeadersMiddleware:
|
||||||
|
"""
|
||||||
|
Security headers middleware for web services.
|
||||||
|
Adds security headers to HTTP responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, headers: Optional[SecurityHeaders] = None):
|
||||||
|
"""
|
||||||
|
Initialize security headers middleware
|
||||||
|
|
||||||
|
Args:
|
||||||
|
headers: Security headers configuration
|
||||||
|
"""
|
||||||
|
self.headers = headers or SecurityHeaders()
|
||||||
|
|
||||||
|
def get_headers(self) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get security headers dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of security headers
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"X-Content-Type-Options": self.headers.X_Content_Type_Options,
|
||||||
|
"X-Frame-Options": self.headers.X_Frame_Options,
|
||||||
|
"X-XSS-Protection": self.headers.X_XSS_Protection,
|
||||||
|
"Strict-Transport-Security": self.headers.Strict_Transport_Security,
|
||||||
|
"Content-Security-Policy": self.headers.Content_Security_Policy,
|
||||||
|
"Referrer-Policy": self.headers.Referrer_Policy,
|
||||||
|
"Permissions-Policy": self.headers.Permissions_Policy,
|
||||||
|
"Cache-Control": self.headers.Cache_Control,
|
||||||
|
"Pragma": self.headers.Pragma
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply_to_response(self, response_headers: Dict[str, str]) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Apply security headers to response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_headers: Existing response headers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response headers with security headers added
|
||||||
|
"""
|
||||||
|
security_headers = self.get_headers()
|
||||||
|
response_headers.update(security_headers)
|
||||||
|
return response_headers
|
||||||
|
|
||||||
|
|
||||||
|
class CORSMiddleware:
|
||||||
|
"""
|
||||||
|
CORS middleware for web services.
|
||||||
|
Handles Cross-Origin Resource Sharing policies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: CORSConfig):
|
||||||
|
"""
|
||||||
|
Initialize CORS middleware
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: CORS configuration
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def get_cors_headers(self, origin: str) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get CORS headers for a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
origin: Request origin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of CORS headers
|
||||||
|
"""
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
# Check if origin is allowed
|
||||||
|
if self._is_origin_allowed(origin):
|
||||||
|
headers["Access-Control-Allow-Origin"] = origin
|
||||||
|
|
||||||
|
if self.config.allow_credentials:
|
||||||
|
headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
|
||||||
|
headers["Access-Control-Allow-Methods"] = ", ".join(self.config.allow_methods)
|
||||||
|
headers["Access-Control-Allow-Headers"] = ", ".join(self.config.allow_headers)
|
||||||
|
|
||||||
|
if self.config.expose_headers:
|
||||||
|
headers["Access-Control-Expose-Headers"] = ", ".join(self.config.expose_headers)
|
||||||
|
|
||||||
|
headers["Access-Control-Max-Age"] = str(self.config.max_age)
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _is_origin_allowed(self, origin: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if origin is allowed based on CORS policy
|
||||||
|
|
||||||
|
Args:
|
||||||
|
origin: Request origin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if origin is allowed, False otherwise
|
||||||
|
"""
|
||||||
|
if "*" in self.config.allow_origins:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return origin in self.config.allow_origins
|
||||||
|
|
||||||
|
def is_preflight_request(self, method: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if request is a CORS preflight request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if preflight request, False otherwise
|
||||||
|
"""
|
||||||
|
return method.upper() == "OPTIONS"
|
||||||
|
|
||||||
|
|
||||||
|
def create_production_security_headers() -> SecurityHeaders:
|
||||||
|
"""
|
||||||
|
Create security headers configuration for production
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SecurityHeaders configured for production
|
||||||
|
"""
|
||||||
|
return SecurityHeaders(
|
||||||
|
X_Content_Type_Options="nosniff",
|
||||||
|
X_Frame_Options="DENY",
|
||||||
|
X_XSS_Protection="1; mode=block",
|
||||||
|
Strict_Transport_Security="max-age=31536000; includeSubDomains; preload",
|
||||||
|
Content_Security_Policy="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'",
|
||||||
|
Referrer_Policy="strict-origin-when-cross-origin",
|
||||||
|
Permissions_Policy="geolocation=(), microphone=(), camera=()",
|
||||||
|
Cache_Control="no-cache, no-store, must-revalidate",
|
||||||
|
Pragma="no-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_development_security_headers() -> SecurityHeaders:
|
||||||
|
"""
|
||||||
|
Create security headers configuration for development
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SecurityHeaders configured for development
|
||||||
|
"""
|
||||||
|
return SecurityHeaders(
|
||||||
|
X_Content_Type_Options="nosniff",
|
||||||
|
X_Frame_Options="SAMEORIGIN",
|
||||||
|
X_XSS_Protection="1; mode=block",
|
||||||
|
Strict_Transport_Security="max-age=3600",
|
||||||
|
Content_Security_Policy="default-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
||||||
|
Referrer_Policy="strict-origin-when-cross-origin",
|
||||||
|
Permissions_Policy="",
|
||||||
|
Cache_Control="no-cache",
|
||||||
|
Pragma="no-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_strict_cors_config(allowed_origins: List[str]) -> CORSConfig:
|
||||||
|
"""
|
||||||
|
Create strict CORS configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
allowed_origins: List of allowed origins
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CORSConfig with strict settings
|
||||||
|
"""
|
||||||
|
return CORSConfig(
|
||||||
|
allow_origins=allowed_origins,
|
||||||
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||||
|
allow_headers=["Content-Type", "Authorization", "X-API-Key"],
|
||||||
|
allow_credentials=True,
|
||||||
|
expose_headers=["X-Request-ID"],
|
||||||
|
max_age=3600
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_permissive_cors_config() -> CORSConfig:
|
||||||
|
"""
|
||||||
|
Create permissive CORS configuration (for development)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CORSConfig with permissive settings
|
||||||
|
"""
|
||||||
|
return CORSConfig(
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
allow_credentials=False,
|
||||||
|
expose_headers=["*"],
|
||||||
|
max_age=86400
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user