ai-content-moderator / security.py
Abhay Kushwaha
feat: content moderation system complete setup
ddd8598
Raw
History Blame Contribute Delete
4.92 kB
"""
Security utilities for API authentication and authorization
"""
from fastapi import HTTPException, status, Header
from typing import Optional
import logging
from config import settings
logger = logging.getLogger(__name__)
def verify_api_key(x_api_key: Optional[str] = Header(None)) -> str:
"""
Verify API key from request header
Args:
x_api_key: API key from X-API-Key header
Returns:
API key if valid
Raises:
HTTPException: If API key is invalid or missing
"""
if not x_api_key:
# API key is optional in development
if settings.DEBUG:
return "development"
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="API key required in X-API-Key header"
)
if x_api_key != settings.API_KEY:
logger.warning(f"Invalid API key attempt: {x_api_key[:10]}...")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API key"
)
return x_api_key
def is_admin_key(api_key: str) -> bool:
"""
Check if API key has admin privileges
Args:
api_key: API key to verify
Returns:
True if admin key, False otherwise
"""
return api_key == settings.API_KEY
def hash_api_key(api_key: str) -> str:
"""
Hash API key for storage
Args:
api_key: Plain text API key
Returns:
Hashed API key
"""
import hashlib
return hashlib.sha256(api_key.encode()).hexdigest()
def verify_hashed_api_key(plain_key: str, hashed_key: str) -> bool:
"""
Verify plain key against hashed key
Args:
plain_key: Plain text API key
hashed_key: Hashed API key
Returns:
True if keys match, False otherwise
"""
return hash_api_key(plain_key) == hashed_key
class SecurityHeaders:
"""Security headers for responses"""
HEADERS = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"Content-Security-Policy": "default-src 'self'"
}
@staticmethod
def get_headers() -> dict:
"""Get security headers"""
return SecurityHeaders.HEADERS
def rate_limit_check(client_ip: str, max_requests: int = 100, window_seconds: int = 60) -> bool:
"""
Check if client has exceeded rate limit
(Simple in-memory implementation - use Redis for production)
Args:
client_ip: Client IP address
max_requests: Max requests allowed
window_seconds: Time window in seconds
Returns:
True if within limit, False if exceeded
"""
# In production, use Redis or similar
# This is a placeholder implementation
return True
def validate_request_size(content_length: Optional[int], max_size: int) -> bool:
"""
Validate request content size
Args:
content_length: Content-Length header value
max_size: Maximum allowed size in bytes
Returns:
True if valid, False if too large
"""
if content_length is None:
return True
return content_length <= max_size
def sanitize_input(user_input: str, max_length: int = 5000) -> str:
"""
Sanitize user input
Args:
user_input: User provided input
max_length: Maximum allowed length
Returns:
Sanitized input
"""
if not user_input:
return ""
# Truncate if too long
user_input = user_input[:max_length]
# Remove null bytes
user_input = user_input.replace('\x00', '')
return user_input.strip()
def create_cors_headers() -> dict:
"""Create CORS headers"""
return {
"Access-Control-Allow-Origin": ", ".join(settings.CORS_ORIGINS),
"Access-Control-Allow-Credentials": str(settings.CORS_CREDENTIALS).lower(),
"Access-Control-Allow-Methods": ", ".join(settings.CORS_METHODS),
"Access-Control-Allow-Headers": ", ".join(settings.CORS_HEADERS)
}
def log_security_event(event_type: str, details: dict):
"""
Log security-related events
Args:
event_type: Type of security event
details: Event details
"""
logger.warning(f"SECURITY EVENT: {event_type} - {details}")
def is_valid_submission_id(submission_id: str) -> bool:
"""
Validate submission ID format
Args:
submission_id: Submission ID to validate
Returns:
True if valid format, False otherwise
"""
if not submission_id or len(submission_id) == 0:
return False
# Check if it matches expected format (sub_xxxxxxxx)
if submission_id.startswith("sub_") and len(submission_id) > 4:
return True
return False