From 85e971ae9bebe409c596c8a9f111e4ed0248fe95 Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Tue, 14 Apr 2026 02:02:50 -0300 Subject: [PATCH] Update ratelimit.py docstring; add Redis service to docker-compose ratelimit.py: fix module docstring to reflect current _NAMESPACE resolution (settings.POD_NAMESPACE, not K8s SA files read inside the middleware). docker-compose.yaml: - Add saplredis service (redis:7-alpine, no persistence, 512 MB maxmemory, allkeys-lru, 4 databases, same policy as k8s ConfigMap). - Add REDIS_URL=redis://saplredis:6379 and CACHE_BACKEND=redis to the sapl service so local docker-compose runs use Redis out of the box. - sapl depends_on now includes saplredis. Co-Authored-By: Claude Sonnet 4.6 --- docker/docker-compose.yaml | 34 ++++++++++++++++++++++++++++++++++ sapl/middleware/ratelimit.py | 25 +++++++++++++------------ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 69e3559da..77aca0129 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -18,6 +18,7 @@ services: - "5433:5432" networks: - sapl-net + saplsolr: image: solr:8.11 restart: always @@ -32,6 +33,34 @@ services: - "8983:8983" networks: - sapl-net + + saplredis: + image: redis:7-alpine + restart: always + container_name: redis + labels: + NAME: "redis" + command: > + redis-server + --save "" + --appendonly no + --maxmemory 512mb + --maxmemory-policy allkeys-lru + --maxmemory-samples 10 + --maxclients 1000 + --timeout 300 + --tcp-keepalive 60 + --hz 20 + --lazyfree-lazy-eviction yes + --lazyfree-lazy-expire yes + --lazyfree-lazy-server-del yes + --databases 4 + --protected-mode no + ports: + - "6379:6379" + networks: + - sapl-net + sapl: image: sapl:local # build: @@ -57,20 +86,25 @@ services: IS_ZK_EMBEDDED: 'True' ENABLE_SAPN: 'False' TZ: America/Sao_Paulo + REDIS_URL: redis://saplredis:6379 + CACHE_BACKEND: redis volumes: - sapl_data:/var/interlegis/sapl/data - sapl_media:/var/interlegis/sapl/media depends_on: - sapldb - saplsolr + - saplredis ports: - "80:80" networks: - sapl-net + networks: sapl-net: name: sapl-net driver: bridge + volumes: sapldb_data: sapl_data: diff --git a/sapl/middleware/ratelimit.py b/sapl/middleware/ratelimit.py index 103983d33..0ab70b7d5 100644 --- a/sapl/middleware/ratelimit.py +++ b/sapl/middleware/ratelimit.py @@ -16,11 +16,12 @@ Decision flow (per request): All decisions are no-ops when RATELIMIT_DRY_RUN=True (logged only). Degrades gracefully to non-atomic counting when Redis is unavailable. -Tenant namespace (_NAMESPACE) is resolved once at module load from: - 1. POD_NAMESPACE env var (K8s Downward API — preferred) - 2. K8s service-account namespace file (always present in-cluster) - 3. 'global' (local development fallback) -Since each pod serves exactly one tenant, this is a startup constant — +_NAMESPACE is settings.POD_NAMESPACE, resolved once at startup: + - K8s: start.sh reads the k8s namespace from the Downward API env var + or the service-account namespace file, writes it to .env as POD_NAMESPACE. + - Bare-metal / VM / docker-compose: defaults to the machine hostname + (socket.gethostbyname_ex result computed in settings.py). +Since a deployment serves exactly one tenant, this is a startup constant — no per-request lookup is needed or correct. """ @@ -46,11 +47,11 @@ _NAMESPACE = settings.POD_NAMESPACE # Redis key templates — module-level constants, never inline strings # --------------------------------------------------------------------------- -RL_IP_REQUESTS = 'rl:ip:{ip}:reqs' -RL_IP_BLOCKED = 'rl:ip:{ip}:blocked' +RL_IP_REQUESTS = 'rl:ip:{ip}:reqs' +RL_IP_BLOCKED = 'rl:ip:{ip}:blocked' RL_USER_REQUESTS = 'rl:{ns}:user:{uid}:reqs' -RL_USER_BLOCKED = 'rl:{ns}:user:{uid}:blocked' -RL_NS_WINDOW = 'rl:{ns}:ip:{ip}:w:{bucket}' +RL_USER_BLOCKED = 'rl:{ns}:user:{uid}:blocked' +RL_NS_WINDOW = 'rl:{ns}:ip:{ip}:w:{bucket}' # --------------------------------------------------------------------------- # Bot UA fragments @@ -95,9 +96,9 @@ def get_client_ip(request): 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' + request.META.get('HTTP_X_REAL_IP') + or request.META.get('REMOTE_ADDR') + or '0.0.0.0' ) return ip_mask(ip)