FASHIONISTAR CI/CD
🔄 Celery Queues Deploy: 4aae6106b1530c3cc7d7d64f5e3c3e1d5015691d [GitHub Actions]
27c799c
Raw
History Blame
11 kB
# apps/authentication/throttles.py
"""
Advanced Throttling & Rate Limiting Framework for Authentication API.
This module implements a three-tier throttling strategy:
1. BurstRateThrottle: Strict limits for anonymous/sensitive endpoints.
2. SustainedRateThrottle: Standard limits for authenticated users.
3. RoleBasedAdaptiveThrottle: Dynamic scaling based on user role.
All throttle violations are:
- Logged to the Django logger (existing)
- Written to AuditEventLog as LOGIN_BLOCKED / SECURITY events (NEW)
- Recorded as LoginEvent with outcome=BLOCKED (NEW, where applicable)
"""
import logging
from typing import Optional
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
logger = logging.getLogger('application')
# ════════════════════════════════════════════════════════════════════════════
# Helper: fire audit event for throttle violation (non-blocking, never raises)
# ════════════════════════════════════════════════════════════════════════════
def _audit_throttle_violation(
request,
scope: str,
wait_time,
endpoint_hint: str = "",
) -> None:
"""
Write a throttle violation to AuditEventLog.
Called from throttle_failure() in every throttle class.
Guaranteed never to raise — all errors swallowed.
Args:
request: The DRF request object.
scope: Throttle scope string (e.g. 'auth_burst').
wait_time: Seconds until the client may retry.
endpoint_hint: URL path string (from request.path or passed in).
"""
try:
from apps.audit_logs.services.audit import AuditService
from apps.audit_logs.models import EventType, EventCategory, SeverityLevel
ip_address = None
user_agent = None
request_path = endpoint_hint
actor = None
actor_email = None
if request:
xff = getattr(request, 'META', {}).get('HTTP_X_FORWARDED_FOR', '')
ip_address = xff.split(',')[0].strip() if xff else getattr(
request, 'META', {}
).get('REMOTE_ADDR', '0.0.0.0')
user_agent = getattr(request, 'META', {}).get('HTTP_USER_AGENT', '')
request_path = request_path or getattr(request, 'path', '')
drf_user = getattr(request, 'user', None)
if drf_user and getattr(drf_user, 'is_authenticated', False):
actor = drf_user
actor_email = getattr(drf_user, 'email', None)
AuditService.log(
event_type=EventType.LOGIN_BLOCKED,
event_category=EventCategory.SECURITY,
severity=SeverityLevel.WARNING,
action=(
f"Rate limit exceeded — scope={scope} | "
f"endpoint={request_path} | retry_in={wait_time}s"
),
request=request,
actor=actor,
actor_email=actor_email,
ip_address=ip_address,
user_agent=user_agent,
request_path=request_path,
response_status=429,
metadata={
"throttle_scope": scope,
"retry_after_seconds": wait_time,
"endpoint": request_path,
},
error_message=f"Throttle scope '{scope}' exceeded",
is_compliance=True,
)
except Exception:
# Never block the HTTP response path — this is purely informational
pass
def _record_throttle_login_event(request, scope: str) -> None:
"""
Record a LoginEvent with outcome=BLOCKED for throttle violations on
auth/login and password-reset endpoints.
Only fires for endpoints that look like login or password-reset.
Non-auth endpoints skip this (to avoid noise in the security dashboard).
"""
try:
path = getattr(request, 'path', '') or ''
is_auth_endpoint = any(
p in path for p in (
'/login', '/password', '/reset', '/otp', '/verify',
'/register', '/token',
)
)
if not is_auth_endpoint:
return
from apps.authentication.models import LoginEvent
xff = getattr(request, 'META', {}).get('HTTP_X_FORWARDED_FOR', '')
ip = xff.split(',')[0].strip() if xff else getattr(
request, 'META', {}
).get('REMOTE_ADDR', '0.0.0.0')
ua = getattr(request, 'META', {}).get('HTTP_USER_AGENT', '')
# Resolve user (may be anonymous on throttled endpoints)
drf_user = getattr(request, 'user', None)
user = drf_user if (drf_user and getattr(drf_user, 'is_authenticated', False)) else None
LoginEvent.record(
user=user,
ip_address=ip or '0.0.0.0',
user_agent=ua,
auth_method=LoginEvent.METHOD_EMAIL,
outcome=LoginEvent.OUTCOME_BLOCKED,
failure_reason=f'rate_limited:{scope}',
is_successful=False,
)
except Exception:
pass
# ============================================================================
# TIER 1: BURST RATE THROTTLE (Sensitive Endpoints)
# ============================================================================
class BurstRateThrottle(AnonRateThrottle):
"""
Strict Rate Limiting for Sensitive Operations.
Limit: 10 requests per minute per IP.
Throttle violations → AuditEventLog + LoginEvent (for auth paths).
"""
scope = 'auth_burst'
rate = '10/min'
def throttle_success(self):
result = super().throttle_success()
return result
def throttle_failure(self):
try:
wait_time = self.wait() if hasattr(self, 'wait') and callable(self.wait) else 60
ip_address = self.get_ident(self.request) if hasattr(self, 'request') else 'UNKNOWN'
path = getattr(self.request, 'path', 'UNKNOWN') if hasattr(self, 'request') else 'UNKNOWN'
logger.warning(
"⛔ BURST THROTTLE TRIGGERED | Scope: %s | IP: %s | "
"Retry-After: %ss | Endpoint: %s",
self.scope, ip_address, wait_time, path,
)
req = getattr(self, 'request', None)
# Write to AuditEventLog
_audit_throttle_violation(req, self.scope, wait_time, path)
# Write to LoginEvent (for auth-related endpoints only)
if req:
_record_throttle_login_event(req, self.scope)
except Exception as e:
logger.error("Error in BurstRateThrottle.throttle_failure: %s", e)
return super().throttle_failure()
def allow_request(self, request, view) -> bool:
try:
self.request = request
return super().allow_request(request, view)
except Exception as e:
logger.error("Error in BurstRateThrottle.allow_request: %s", e)
return True
# ============================================================================
# TIER 2: SUSTAINED RATE THROTTLE (Standard Users)
# ============================================================================
class SustainedRateThrottle(UserRateThrottle):
"""
Standard Rate Limiting for Authenticated Users.
Limit: 1000 requests per day.
Sustained violations are written to AuditEventLog (lower severity — INFO).
"""
scope = 'auth_sustained'
rate = '1000/day'
def get_rate(self) -> Optional[str]:
return self.rate
def throttle_failure(self):
try:
wait_time = self.wait() if hasattr(self, 'wait') and callable(self.wait) else 86400
req = getattr(self, 'request', None)
path = getattr(req, 'path', 'UNKNOWN') if req else 'UNKNOWN'
logger.warning(
"⛔ SUSTAINED THROTTLE TRIGGERED | Scope: %s | Retry-After: %ss | Endpoint: %s",
self.scope, wait_time, path,
)
_audit_throttle_violation(req, self.scope, wait_time, path)
except Exception as e:
logger.error("Error in SustainedRateThrottle.throttle_failure: %s", e)
return super().throttle_failure()
def allow_request(self, request, view) -> bool:
try:
self.request = request
return super().allow_request(request, view)
except Exception as e:
logger.error("Error in SustainedRateThrottle.allow_request: %s", e)
return True
# ============================================================================
# TIER 3: ROLE-BASED ADAPTIVE THROTTLE (Dynamic Scaling)
# ============================================================================
class RoleBasedAdaptiveThrottle(UserRateThrottle):
"""
Dynamic Throttling Based on User Role (RBAC Integration).
Violations written to AuditEventLog with role context.
"""
scope = 'auth_adaptive'
def get_rate(self) -> str:
try:
user = self.request.user if hasattr(self, 'request') else None
if not user or not user.is_authenticated:
return '100/day'
role = getattr(user, 'role', 'client').lower()
if role in ['admin', 'superuser', 'staff']:
limit = '100000/day'
elif role == 'vendor':
limit = '10000/day'
else:
limit = '2000/day'
return limit
except Exception as e:
logger.warning("Error determining adaptive throttle rate: %s | Defaulting to 1000/day", e)
return '1000/day'
def throttle_failure(self):
try:
wait_time = self.wait() if hasattr(self, 'wait') and callable(self.wait) else 86400
req = getattr(self, 'request', None)
path = getattr(req, 'path', 'UNKNOWN') if req else 'UNKNOWN'
logger.warning(
"⛔ ADAPTIVE THROTTLE TRIGGERED | Scope: %s | Role: %s | Retry-After: %ss | Endpoint: %s",
self.scope,
getattr(getattr(req, 'user', None), 'role', 'unknown') if req else 'unknown',
wait_time,
path,
)
_audit_throttle_violation(req, self.scope, wait_time, path)
except Exception as e:
logger.error("Error in RoleBasedAdaptiveThrottle.throttle_failure: %s", e)
return super().throttle_failure()
def allow_request(self, request, view) -> bool:
try:
self.request = request
self.rate = self.get_rate()
self.num_requests, self.duration = self.parse_rate(self.rate)
return super().allow_request(request, view)
except Exception as e:
logger.error("Error in RoleBasedAdaptiveThrottle.allow_request: %s", e)
return True