Browse Source

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 <noreply@anthropic.com>
rate-limiter-2026
Edward Ribeiro 3 weeks ago
parent
commit
85e971ae9b
  1. 34
      docker/docker-compose.yaml
  2. 25
      sapl/middleware/ratelimit.py

34
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:

25
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)

Loading…
Cancel
Save