feat: Add rate limiting middleware with SlowAPI

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-10 16:17:53 +00:00
parent 0b3f9212d1
commit 8d347e0a53
10 changed files with 659 additions and 0 deletions
+7
View File
@@ -27,6 +27,13 @@ logger = logging.getLogger(__name__)
router = APIRouter()
def get_limiter():
"""Get the limiter from app state."""
from app.main import app
return app.state.limiter
@router.get("/files")
@require_login
def list_files_api(
+23
View File
@@ -213,6 +213,29 @@ class Settings(BaseSettings):
default=True, description="Enable X-Content-Type-Options header (always set to 'nosniff')."
)
# Rate Limiting Configuration (see SECURITY_AUDIT.md and docs/API.md)
# Protects against DoS attacks and API abuse
rate_limiting_enabled: bool = Field(
default=True,
description="Enable rate limiting middleware. Recommended for production to prevent abuse.",
)
rate_limit_default: str = Field(
default="100/minute",
description="Default rate limit for all endpoints (format: 'count/period', e.g., '100/minute', '1000/hour').",
)
rate_limit_upload: str = Field(
default="20/minute",
description="Rate limit for file upload endpoints to prevent resource exhaustion.",
)
rate_limit_process: str = Field(
default="30/minute",
description="Rate limit for document processing endpoints (OCR, metadata extraction).",
)
rate_limit_auth: str = Field(
default="10/minute",
description="Stricter rate limit for authentication endpoints to prevent brute force attacks.",
)
@validator("notification_urls", pre=True)
def parse_notification_urls(cls, v):
"""Parse notification URLs from string or list"""
+7
View File
@@ -17,9 +17,11 @@ from app.api import router as api_router
from app.auth import router as auth_router
from app.config import settings
from app.database import init_db
from app.middleware.rate_limit import create_limiter, get_rate_limit_exceeded_handler
from app.middleware.security_headers import SecurityHeadersMiddleware
from app.utils.config_validator import check_all_configs
from app.utils.notification import init_apprise, notify_shutdown, notify_startup
from slowapi.errors import RateLimitExceeded
# Import the routers - now using views directly instead of frontend
from app.views import router as frontend_router
@@ -100,6 +102,11 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="DocuElevate", lifespan=lifespan)
# Initialize rate limiter and attach to app state
limiter = create_limiter(redis_url=settings.redis_url, enabled=settings.rate_limiting_enabled)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, get_rate_limit_exceeded_handler())
# Middleware stack (order matters - applied in reverse order)
# Last added middleware is executed first
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Rate Limiting Middleware for DocuElevate.
This middleware provides rate limiting capabilities to protect API endpoints from abuse
and DoS attacks. It uses SlowAPI with Redis backend for distributed rate limiting.
Key features:
- Per-IP rate limiting by default
- Per-user rate limiting for authenticated endpoints
- Configurable global and per-endpoint limits
- Redis-backed for distributed deployments
- Fallback to in-memory for development
See docs/ConfigurationGuide.md and docs/API.md for configuration and usage.
"""
import logging
from typing import Callable
from fastapi import Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
logger = logging.getLogger(__name__)
def get_identifier(request: Request) -> str:
"""
Get unique identifier for rate limiting.
Uses authenticated user ID if available, otherwise falls back to IP address.
This provides better rate limiting for authenticated users and prevents
IP-based bypassing for authenticated endpoints.
Args:
request: FastAPI request object
Returns:
Unique identifier string for rate limiting
"""
# Check if user is authenticated (from session)
if hasattr(request, "session") and request.session.get("user"):
user = request.session.get("user")
# Use username or user_id as identifier
if isinstance(user, dict):
identifier = user.get("username") or user.get("user_id") or user.get("id")
if identifier:
logger.debug(f"Rate limiting by user: {identifier}")
return f"user:{identifier}"
# Fall back to IP address for unauthenticated requests
ip = get_remote_address(request)
logger.debug(f"Rate limiting by IP: {ip}")
return ip
def create_limiter(redis_url: str = None, enabled: bool = True) -> Limiter:
"""
Create and configure the rate limiter.
Args:
redis_url: Redis connection URL for distributed rate limiting
enabled: Whether rate limiting is enabled (default: True)
Returns:
Configured Limiter instance
"""
if not enabled:
logger.info("Rate limiting is disabled")
# Return a limiter with very high limits when disabled
return Limiter(
key_func=get_identifier,
default_limits=["10000/minute"], # Effectively unlimited
enabled=False,
)
# Use Redis if available, otherwise fall back to in-memory
storage_uri = redis_url if redis_url else "memory://"
if redis_url:
logger.info(f"Rate limiting enabled with Redis backend: {redis_url}")
else:
logger.warning(
"Rate limiting using in-memory storage (not suitable for production with multiple workers). "
"Configure REDIS_URL for distributed rate limiting."
)
# Create limiter with default limits
# Default: 100 requests per minute per IP/user
limiter = Limiter(
key_func=get_identifier,
default_limits=["100/minute"],
storage_uri=storage_uri,
strategy="fixed-window", # Can be: fixed-window, moving-window, or fixed-window-elastic-expiry
enabled=True,
)
logger.info("Rate limiter initialized successfully")
return limiter
def get_rate_limit_exceeded_handler() -> Callable:
"""
Get the rate limit exceeded exception handler.
Returns a handler that provides user-friendly 429 responses with
Retry-After header when rate limit is exceeded.
Returns:
Exception handler function
"""
return _rate_limit_exceeded_handler
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Rate limiting decorators for DocuElevate API endpoints.
This module provides convenient decorators to apply rate limits to specific endpoints.
Import the limiter from main.py state and use these decorators to protect endpoints.
"""
from functools import wraps
from fastapi import Request
# Import will happen at runtime to avoid circular dependencies
_limiter = None
def get_limiter():
"""Get the limiter instance from the app state."""
global _limiter
if _limiter is None:
from app.main import app
_limiter = app.state.limiter
return _limiter
def limit(rate_limit: str):
"""
Apply a rate limit to an endpoint.
Args:
rate_limit: Rate limit string (e.g., "10/minute", "100/hour")
Returns:
Decorator function
Example:
@router.post("/login")
@limit("10/minute")
async def login(request: Request):
...
"""
def decorator(func):
limiter = get_limiter()
# Apply the slowapi limit decorator
return limiter.limit(rate_limit)(func)
return decorator
def exempt():
"""
Exempt an endpoint from rate limiting.
Returns:
Decorator function
Example:
@router.get("/health")
@exempt()
def health_check():
...
"""
def decorator(func):
limiter = get_limiter()
# Apply the slowapi exempt decorator
return limiter.exempt(func)
return decorator