""" Rate limiting configuration using slowapi with Redis backend. This module provides rate limiting functionality to protect against brute force attacks and DoS attempts on sensitive endpoints. Rate Limit Tiers: - standard: 60/minute - For normal CRUD operations (tasks, comments) - sensitive: 20/minute - For sensitive operations (attachments, password change) - heavy: 5/minute - For resource-intensive operations (reports, bulk operations) """ import logging import os from functools import wraps from typing import Callable, Optional from fastapi import Request, Response from slowapi import Limiter from slowapi.util import get_remote_address from app.core.config import settings logger = logging.getLogger(__name__) def _get_storage_uri() -> str: """ Determine the appropriate storage URI for rate limiting. Priority: 1. Use memory storage if TESTING environment variable is set 2. Try Redis if available 3. Fall back to memory storage if Redis is unavailable (with warning) Note: Memory storage is acceptable for development but Redis should be used in production for consistent rate limiting across workers. """ # Use memory storage for testing testing = os.environ.get("TESTING", "").lower() in ("true", "1", "yes") if testing: return "memory://" # Try to connect to Redis redis_url = settings.REDIS_URL try: import redis r = redis.Redis( host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB, socket_connect_timeout=1, ) r.ping() logger.info("Rate limiter using Redis storage") return redis_url except Exception as e: logger.warning( f"Redis unavailable for rate limiting ({e}). " "Falling back to in-memory storage. " "Note: In production, Redis should be running for consistent " "rate limiting across multiple workers." ) return "memory://" _storage_uri = _get_storage_uri() # Create limiter instance with appropriate storage # Uses the client's remote address (IP) as the key for rate limiting # Note: headers_enabled=False because slowapi's header injection requires Response objects, # which conflicts with endpoints that return Pydantic models directly. # Rate limit status can be checked via the 429 Too Many Requests response. limiter = Limiter( key_func=get_remote_address, storage_uri=_storage_uri, strategy="fixed-window", # Fixed window strategy for predictable rate limiting headers_enabled=False, # Disabled due to compatibility issues with Pydantic model responses ) # Convenience functions for rate limit tiers def get_rate_limit_standard() -> str: """Get the standard rate limit tier (60/minute by default).""" return settings.RATE_LIMIT_STANDARD def get_rate_limit_sensitive() -> str: """Get the sensitive rate limit tier (20/minute by default).""" return settings.RATE_LIMIT_SENSITIVE def get_rate_limit_heavy() -> str: """Get the heavy rate limit tier (5/minute by default).""" return settings.RATE_LIMIT_HEAVY # Pre-configured rate limit decorators for common use cases def rate_limit_standard(func: Optional[Callable] = None): """ Apply standard rate limit (60/minute) for normal CRUD operations. Use for: Task creation/update, comment creation, etc. """ return limiter.limit(get_rate_limit_standard())(func) if func else limiter.limit(get_rate_limit_standard()) def rate_limit_sensitive(func: Optional[Callable] = None): """ Apply sensitive rate limit (20/minute) for sensitive operations. Use for: File uploads, password changes, report exports, etc. """ return limiter.limit(get_rate_limit_sensitive())(func) if func else limiter.limit(get_rate_limit_sensitive()) def rate_limit_heavy(func: Optional[Callable] = None): """ Apply heavy rate limit (5/minute) for resource-intensive operations. Use for: Report generation, bulk operations, data exports, etc. """ return limiter.limit(get_rate_limit_heavy())(func) if func else limiter.limit(get_rate_limit_heavy())