## Security Enhancements (P0) - Add input validation with max_length and numeric range constraints - Implement WebSocket token authentication via first message - Add path traversal prevention in file storage service ## Permission Enhancements (P0) - Add project member management for cross-department access - Implement is_department_manager flag for workload visibility ## Cycle Detection (P0) - Add DFS-based cycle detection for task dependencies - Add formula field circular reference detection - Display user-friendly cycle path visualization ## Concurrency & Reliability (P1) - Implement optimistic locking with version field (409 Conflict on mismatch) - Add trigger retry mechanism with exponential backoff (1s, 2s, 4s) - Implement cascade restore for soft-deleted tasks ## Rate Limiting (P1) - Add tiered rate limits: standard (60/min), sensitive (20/min), heavy (5/min) - Apply rate limits to tasks, reports, attachments, and comments ## Frontend Improvements (P1) - Add responsive sidebar with hamburger menu for mobile - Improve touch-friendly UI with proper tap target sizes - Complete i18n translations for all components ## Backend Reliability (P2) - Configure database connection pool (size=10, overflow=20) - Add Redis fallback mechanism with message queue - Add blocker check before task deletion ## API Enhancements (P3) - Add standardized response wrapper utility - Add /health/ready and /health/live endpoints - Implement project templates with status/field copying ## Tests Added - test_input_validation.py - Schema and path traversal tests - test_concurrency_reliability.py - Optimistic locking and retry tests - test_backend_reliability.py - Connection pool and Redis tests - test_api_enhancements.py - Health check and template tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
"""
|
|
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())
|