diff --git a/sapl/middleware/ratelimit.py b/sapl/middleware/ratelimit.py index 7893b358d..278364687 100644 --- a/sapl/middleware/ratelimit.py +++ b/sapl/middleware/ratelimit.py @@ -87,8 +87,6 @@ RL_INDEX_API_BLOCKED_IPS = 'rl:index:api_blocked_ips' # 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. # --------------------------------------------------------------------------- -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_WEEKLY = 'quota:{ns}:weekly:{week}:ip:{ip}' @@ -324,7 +322,7 @@ class RateLimitMiddleware: [p.pattern for p in self._bypass_paths] or '(none)', ) logger.info( - '[API QUOTAS] daily=%s weekly=%s (auth keyed by user pk, anon by IP)', + '[API QUOTAS] daily=%s weekly=%s (all callers keyed by IP)', settings.API_QUOTA_DAILY, settings.API_QUOTA_WEEKLY, ) @@ -428,37 +426,21 @@ class RateLimitMiddleware: response['X-RateLimit-Reason'] = f'quota_{exceeded}' return response - user = getattr(request, 'user', None) - if not (user and user.is_authenticated): - # 6. API-specific per-minute rate limit for anonymous external callers. - # Writes rl:api:ip::blocked only — never rl:ip::blocked. - if self.api_rate_limit_enabled: - count = self._incr_with_ttl(RL_API_IP_REQUESTS.format(ip=ip), self.api_window) - if count >= self.api_threshold: - _set_block(RL_API_IP_BLOCKED.format(ip=ip), RL_INDEX_API_BLOCKED_IPS, self.api_block_seconds) - logger.warning( - 'api_rate_limit_block reason=api_threshold_exceeded ' - 'ip=%s path=%s user_agent=%s count=%s threshold=%s', - ip, request.path, request.META.get('HTTP_USER_AGENT', ''), - count, self.api_threshold, - ) - self._inc_block_metric('api_threshold_exceeded') - return self._api_block_response('api_threshold_exceeded') - return self.get_response(request) - - # 7. Authenticated /api/ — existing per-user rate limiter unchanged - decision = self._evaluate(request) - if decision['action'] == 'block': - logger.warning( - 'ratelimit_block layer=django reason=%s ip=%s path=%s namespace=%s', - decision['reason'], decision['ip'], request.path, _NAMESPACE, - extra={'ua': request.META.get('HTTP_USER_AGENT', '')}, - ) - self._inc_block_metric(decision['reason']) - response = HttpResponse(status=429) - response['Retry-After'] = self.BLOCK_TTL - response['X-RateLimit-Reason'] = decision['reason'] - return response + # 6. Per-minute rate limit — 60/min for all callers (anon and auth). + # Auth is not exempt: authenticating must not bypass this cap. + # Writes rl:api:ip::blocked only — never rl:ip::blocked. + if self.api_rate_limit_enabled: + count = self._incr_with_ttl(RL_API_IP_REQUESTS.format(ip=ip), self.api_window) + if count >= self.api_threshold: + _set_block(RL_API_IP_BLOCKED.format(ip=ip), RL_INDEX_API_BLOCKED_IPS, self.api_block_seconds) + logger.warning( + 'api_rate_limit_block reason=api_threshold_exceeded ' + 'ip=%s path=%s user_agent=%s count=%s threshold=%s', + ip, request.path, request.META.get('HTTP_USER_AGENT', ''), + count, self.api_threshold, + ) + self._inc_block_metric('api_threshold_exceeded') + return self._api_block_response('api_threshold_exceeded') return self.get_response(request) # ------------------------------------------------------------------ @@ -566,8 +548,7 @@ class RateLimitMiddleware: def _check_api_quota(self, request): """ Increment daily and weekly API quota counters for all /api/ callers. - Auth users are keyed by user pk (NAT-safe); anon callers by IP. - Both groups share the same daily/weekly cap. + All callers are keyed by IP — auth status is not checked. Fails open (returns None) if Redis/cache is unavailable. """ today = date.today() @@ -575,15 +556,9 @@ class RateLimitMiddleware: date_str = today.isoformat() week_str = f'{iso[0]}-W{iso[1]:02d}' - user = getattr(request, 'user', None) - if user and user.is_authenticated: - 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) + 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: if _incr_with_ttl(d_key, 86400) > self.api_quota_daily: diff --git a/sapl/settings.py b/sapl/settings.py index 8fda3a17d..d21481457 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -437,8 +437,8 @@ RATE_LIMIT_BYPASS_PATHS = [ ] # API quota — daily and weekly call caps for all /api/ callers (anon and auth). -# Auth users are keyed by user pk (NAT-safe); anon callers by IP. -# Both groups share the same cap; weekly default is 7× the daily cap. +# All callers are keyed by IP — auth status is not checked. +# Weekly default is 7× the daily cap. API_QUOTA_DAILY = config('API_QUOTA_DAILY', default=500, cast=int) API_QUOTA_WEEKLY = config('API_QUOTA_WEEKLY', default=3500, cast=int)