@ -87,6 +87,8 @@ RL_INDEX_API_BLOCKED_IPS = 'rl:index:api_blocked_ips'
# TTL set only on first INCR (Lua); daily=24h, weekly=7d — cleanup only,
# TTL set only on first INCR (Lua); daily=24h, weekly=7d — cleanup only,
# resets are implicit in the date/week embedded in the key name.
# resets are implicit in the date/week embedded in the key name.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
QUOTA_USER_DAILY = ' quota: {ns} :daily: {date} :user: {uid} '
QUOTA_USER_WEEKLY = ' quota: {ns} :weekly: {week} :user: {uid} '
QUOTA_IP_DAILY = ' quota: {ns} :daily: {date} :ip: {ip} '
QUOTA_IP_DAILY = ' quota: {ns} :daily: {date} :ip: {ip} '
QUOTA_IP_WEEKLY = ' quota: {ns} :weekly: {week} :ip: {ip} '
QUOTA_IP_WEEKLY = ' quota: {ns} :weekly: {week} :ip: {ip} '
@ -306,8 +308,8 @@ class RateLimitMiddleware:
self . _bypass_paths = [
self . _bypass_paths = [
re . compile ( p ) for p in getattr ( settings , ' RATE_LIMIT_BYPASS_PATHS ' , [ ] )
re . compile ( p ) for p in getattr ( settings , ' RATE_LIMIT_BYPASS_PATHS ' , [ ] )
]
]
self . api_quota_anon_ daily = settings . API_QUOTA_ANON _DAILY
self . api_quota_daily = settings . API_QUOTA_DAILY
self . api_quota_anon_ weekly = settings . API_QUOTA_ANON _WEEKLY
self . api_quota_weekly = settings . API_QUOTA_WEEKLY
self . api_rate_limit_enabled = getattr ( settings , ' API_RATE_LIMIT_ENABLED ' , True )
self . api_rate_limit_enabled = getattr ( settings , ' API_RATE_LIMIT_ENABLED ' , True )
self . api_threshold = getattr ( settings , ' API_RATE_LIMIT_THRESHOLD ' , 60 )
self . api_threshold = getattr ( settings , ' API_RATE_LIMIT_THRESHOLD ' , 60 )
self . api_window = getattr ( settings , ' API_RATE_LIMIT_WINDOW_SECONDS ' , 60 )
self . api_window = getattr ( settings , ' API_RATE_LIMIT_WINDOW_SECONDS ' , 60 )
@ -322,9 +324,9 @@ class RateLimitMiddleware:
[ p . pattern for p in self . _bypass_paths ] or ' (none) ' ,
[ p . pattern for p in self . _bypass_paths ] or ' (none) ' ,
)
)
logger . info (
logger . info (
' [API QUOTAS] daily_anon = %s weekly_anon = %s (auth: governed by per-user 240/min rate ) ' ,
' [API QUOTAS] daily= %s weekly= %s (auth keyed by user pk, anon by IP ) ' ,
settings . API_QUOTA_ANON_ DAILY ,
settings . API_QUOTA_DAILY ,
settings . API_QUOTA_ANON_ WEEKLY ,
settings . API_QUOTA_WEEKLY ,
)
)
logger . info (
logger . info (
' [API RATE LIMIT] enabled= %s threshold= %s window= %s s block= %s s same_origin_bypass= %s ' ,
' [API RATE LIMIT] enabled= %s threshold= %s window= %s s block= %s s same_origin_bypass= %s ' ,
@ -563,28 +565,30 @@ class RateLimitMiddleware:
def _check_api_quota ( self , request ) :
def _check_api_quota ( self , request ) :
"""
"""
Increment per - IP daily and weekly API quota counters for anonymous callers .
Increment daily and weekly API quota counters for all / api / callers .
Authenticated users are rate - limited per - user by _evaluate ( 240 / min ) and
Auth users are keyed by user pk ( NAT - safe ) ; anon callers by IP .
are not subject to a daily quota — returns None immediately for auth users .
Both groups share the same daily / weekly cap .
Fails open ( returns None ) if Redis / cache is unavailable .
Fails open ( returns None ) if Redis / cache is unavailable .
"""
"""
user = getattr ( request , ' user ' , None )
if user and user . is_authenticated :
return None
today = date . today ( )
today = date . today ( )
iso = today . isocalendar ( )
iso = today . isocalendar ( )
date_str = today . isoformat ( )
date_str = today . isoformat ( )
week_str = f ' { iso [ 0 ] } -W { iso [ 1 ] : 02d } '
week_str = f ' { iso [ 0 ] } -W { iso [ 1 ] : 02d } '
ip = get_client_ip ( request )
user = getattr ( request , ' user ' , None )
d_key = QUOTA_IP_DAILY . format ( ns = _NAMESPACE , date = date_str , ip = ip )
if user and user . is_authenticated :
w_key = QUOTA_IP_WEEKLY . format ( ns = _NAMESPACE , week = week_str , ip = ip )
uid = str ( user . pk )
d_key = QUOTA_USER_DAILY . format ( ns = _NAMESPACE , date = date_str , uid = uid )
w_key = QUOTA_USER_WEEKLY . format ( ns = _NAMESPACE , week = week_str , uid = uid )
else :
ip = get_client_ip ( request )
d_key = QUOTA_IP_DAILY . format ( ns = _NAMESPACE , date = date_str , ip = ip )
w_key = QUOTA_IP_WEEKLY . format ( ns = _NAMESPACE , week = week_str , ip = ip )
try :
try :
if _incr_with_ttl ( d_key , 86400 ) > self . api_quota_anon_daily :
if _incr_with_ttl ( d_key , 86400 ) > self . api_quota_daily :
return ' daily '
return ' daily '
if _incr_with_ttl ( w_key , 7 * 86400 ) > self . api_quota_anon_weekly :
if _incr_with_ttl ( w_key , 7 * 86400 ) > self . api_quota_weekly :
return ' weekly '
return ' weekly '
except Exception :
except Exception :
pass # fail open — quota not enforced when Redis unavailable
pass # fail open — quota not enforced when Redis unavailable