From 0e1a14e12a61272a7e2fdf582e55ba48e0c0e426 Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Tue, 14 Apr 2026 00:53:38 -0300 Subject: [PATCH] Consolidate get_client_ip/ratelimit_ip into ratelimit.py; clean up settings access - Move get_client_ip() and ratelimit_ip() from utils.py to sapl/middleware/ratelimit.py (canonical location). utils.py re-exports both via a single import line so all existing callers (comissoes, crud, norma, sessao, painel, parlamentares, protocoloadm) keep working with zero changes. - get_client_ip() is now used inside RateLimitMiddleware instead of the weaker _get_ip(): gains ip_mask() for IPv6 /64 collapsing and HTTP_X_REAL_IP fallback. - Replace getattr(settings, 'X', default) with settings.X throughout __init__: settings.py always defines these vars, defaults were duplicated and would silently drift. django.conf.settings proxy also honours @override_settings in tests, unlike direct module imports. - Replace getattr(..., []) or [] with set(settings.RATE_LIMIT_WHITELIST_IPS): the cast in settings.py always returns a list, the double guard was redundant. - Remove unused _get_ip() and 'from sapl.settings import RATE_LIMITER_RATE'. Co-Authored-By: Claude Sonnet 4.6 --- sapl/middleware/ratelimit.py | 47 +++++++++++++++++++++++------------- sapl/utils.py | 18 +++----------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/sapl/middleware/ratelimit.py b/sapl/middleware/ratelimit.py index 84da8802b..b96729f97 100644 --- a/sapl/middleware/ratelimit.py +++ b/sapl/middleware/ratelimit.py @@ -51,11 +51,31 @@ def _sha256(s): return hashlib.sha256(s.encode()).hexdigest() -def _get_ip(request): - return ( - request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() - or request.META.get('REMOTE_ADDR', '') - ) +def get_client_ip(request): + """ + Return the real client IP, applying django-ratelimit's ip_mask so that + IPv6 /64 subnets are collapsed to a single key (prevents per-address + rotation attacks). Also checks HTTP_X_REAL_IP for nginx setups that + use that header instead of X-Forwarded-For. + + Canonical source — imported from here by other SAPL modules. + """ + from ratelimit.core import ip_mask + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + else: + ip = ( + request.META.get('HTTP_X_REAL_IP') + or request.META.get('REMOTE_ADDR') + or '0.0.0.0' + ) + return ip_mask(ip) + + +def ratelimit_ip(group, request): + """Key function for django-ratelimit decorators (group param is ignored).""" + return get_client_ip(request) def _is_suspicious_headers(request): @@ -80,17 +100,10 @@ class RateLimitMiddleware: def __init__(self, get_response): self.get_response = get_response - self.dry_run = getattr(settings, 'RATELIMIT_DRY_RUN', True) - - anon_rate = getattr(settings, 'RATE_LIMITER_RATE', '35/m') - auth_rate = getattr(settings, 'RATE_LIMITER_RATE_AUTHENTICATED', '120/m') - - self.anon_threshold, self.anon_window = _parse_rate(anon_rate) - self.auth_threshold, self.auth_window = _parse_rate(auth_rate) - - self.whitelist = set( - getattr(settings, 'RATE_LIMIT_WHITELIST_IPS', []) or [] - ) + self.dry_run = settings.RATELIMIT_DRY_RUN + self.anon_threshold, self.anon_window = _parse_rate(settings.RATE_LIMITER_RATE) + self.auth_threshold, self.auth_window = _parse_rate(settings.RATE_LIMITER_RATE_AUTHENTICATED) + self.whitelist = set(settings.RATE_LIMIT_WHITELIST_IPS) self._rl_cache = caches['ratelimit'] def __call__(self, request): @@ -116,7 +129,7 @@ class RateLimitMiddleware: # ------------------------------------------------------------------ def _evaluate(self, request): - ip = _get_ip(request) + ip = get_client_ip(request) if ip in self.whitelist: return {'action': 'pass', 'ip': ip} diff --git a/sapl/utils.py b/sapl/utils.py index ee97094aa..1b7ce99c0 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -401,21 +401,9 @@ def xstr(s): return '' if s is None else str(s) -def get_client_ip(request): - from ratelimit.core import ip_mask - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR') or '0.0.0.0' - return ip_mask(ip) - - -def ratelimit_ip(group, request): - """ - Ignore group param in django-ratelimit==3.0.1 - """ - return get_client_ip(request) +# Canonical implementations live in sapl.middleware.ratelimit. +# Re-exported here so existing import sites keep working unchanged. +from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip # noqa: F401, E402 def get_base_url(request):