Browse Source

GeoIP offline build; Redis inspection tools; smart_rate/smart_key; cache KEY_PREFIX

GeoIP (docker/Dockerfile):
  Remove at-build-time MaxMind download (required BuildKit secrets, caused
  cache-miss issues). Replace with COPY from docker/geoip/GeoLite2-ASN.mmdb
  (git-ignored binary). If absent, build succeeds with ASN blocking disabled.
  Add docker/geoip/update_geoip.sh — run before each build to refresh the
  database from MaxMind using MAXMIND_LICENSE_KEY from env or .env file.

Redis inspection / synthetic test data:
  Add docker/scripts/redis_populate_test_data.py — injects synthetic rl:*
  entries into Redis DB1 to validate key schema and blocking thresholds
  without waiting for real traffic. Supports DRY_RUN and CLEAR modes.
  Add §4.5 (Redis CLI quick-reference + RedisInsight guide) to rate-limiter-v2.md.

Auth-aware @ratelimit decorators (smart_rate / smart_key):
  All 51 @ratelimit decorators across 9 files used rate=RATE_LIMITER_RATE
  (35/m) regardless of authentication, silently over-throttling logged-in
  users compared to what RateLimitMiddleware allows (120/m).
  Add smart_key() and smart_rate() to sapl/middleware/ratelimit.py:
    - smart_key: user pk for authenticated requests, masked IP for anon
    - smart_rate: RATE_LIMITER_RATE_AUTHENTICATED (120/m) for auth,
      RATE_LIMITER_RATE (35/m) for anon — mirrors middleware thresholds
  Update all 51 decorators across crud/base.py + 8 view files.
  Remove now-unused RATE_LIMITER_RATE imports from those files.

Cache KEY_PREFIX (settings.py):
  Change KEY_PREFIX from POD_NAMESPACE ("sapl") to f"cache:{POD_NAMESPACE}"
  so DB0 cache keys are unambiguously prefixed cache:{ns}:* — distinct from
  any future static or file cache key patterns.
  Update key schema table and code examples in rate-limiter-v2.md to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rate-limiter-2026
Edward Ribeiro 3 weeks ago
parent
commit
b61e3e5bd9
  1. 41
      docker/Dockerfile
  2. 3
      docker/geoip/.gitignore
  3. 73
      docker/geoip/update_geoip.sh
  4. 176
      docker/scripts/redis_populate_test_data.py
  5. 110
      rate-limiter-v2.md
  6. 12
      sapl/base/views.py
  7. 7
      sapl/comissoes/views.py
  8. 19
      sapl/crud/base.py
  9. 28
      sapl/materia/views.py
  10. 28
      sapl/middleware/ratelimit.py
  11. 12
      sapl/norma/views.py
  12. 16
      sapl/parlamentares/views.py
  13. 24
      sapl/protocoloadm/views.py
  14. 64
      sapl/relatorios/views.py
  15. 20
      sapl/sessao/views.py
  16. 7
      sapl/settings.py

41
docker/Dockerfile

@ -87,39 +87,24 @@ COPY --from=builder ${VENV_DIR} ${VENV_DIR}
# Código da aplicação (depois do venv para aproveitar cache)
COPY . /var/interlegis/sapl/
# Nginx (somente se instalado)
# Nginx config + GeoLite2-ASN database (somente se instalado).
#
# GeoLite2-ASN.mmdb is NOT downloaded at build time.
# Run docker/geoip/update_geoip.sh before each build to refresh it.
# The .mmdb file lives at docker/geoip/GeoLite2-ASN.mmdb (git-ignored binary).
# If the file is absent the build still succeeds but ASN-based blocking is
# disabled and nginx will emit a startup warning.
RUN if [ "$WITH_NGINX" = "1" ]; then \
rm -f /etc/nginx/conf.d/*; \
cp docker/config/nginx/sapl.conf /etc/nginx/conf.d/sapl.conf; \
cp docker/config/nginx/nginx.conf /etc/nginx/nginx.conf; \
fi
# GeoLite2-ASN database for nginx ASN-based bot blocking.
# The key is injected via BuildKit secret — it is NEVER stored in any image layer.
#
# Build command:
# DOCKER_BUILDKIT=1 docker build \
# --secret id=maxmind_key,src=.env \
# -f docker/Dockerfile .
#
# .env must contain: MAXMIND_LICENSE_KEY=your_key
# The weekly host cron (/etc/cron.weekly/update-geoip) refreshes the db in production.
#
# Pass --build-arg GEOIP_CACHE_BUST=$(date +%s) to force re-download.
ARG GEOIP_CACHE_BUST=0
RUN --mount=type=secret,id=maxmind_key \
if [ "$WITH_NGINX" = "1" ]; then \
MAXMIND_LICENSE_KEY=$(grep -E '^MAXMIND_LICENSE_KEY=' /run/secrets/maxmind_key 2>/dev/null | cut -d= -f2- | tr -d '[:space:]' || true); \
if [ -n "$MAXMIND_LICENSE_KEY" ]; then \
tmpdir=$(mktemp -d) \
&& curl -fsSL \
"https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
| tar -xz --strip-components=1 -C "$tmpdir" \
&& mv "$tmpdir"/*.mmdb /etc/nginx/geoip/GeoLite2-ASN.mmdb \
&& rm -rf "$tmpdir" \
&& echo "GeoLite2-ASN.mmdb downloaded successfully."; \
if [ -f "docker/geoip/GeoLite2-ASN.mmdb" ]; then \
cp docker/geoip/GeoLite2-ASN.mmdb /etc/nginx/geoip/GeoLite2-ASN.mmdb; \
echo "[geoip] GeoLite2-ASN.mmdb installed."; \
else \
echo "MAXMIND_LICENSE_KEY not set in secret — GeoLite2-ASN.mmdb skipped. Add it to .env and rebuild."; \
echo "[geoip] WARNING: docker/geoip/GeoLite2-ASN.mmdb not found."; \
echo "[geoip] Run docker/geoip/update_geoip.sh then rebuild."; \
echo "[geoip] ASN-based blocking will be DISABLED in this image."; \
fi; \
fi

3
docker/geoip/.gitignore

@ -0,0 +1,3 @@
# GeoLite2 binary databases are git-ignored (large binaries, updated frequently).
# Run update_geoip.sh before each docker build to refresh.
*.mmdb

73
docker/geoip/update_geoip.sh

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# update_geoip.sh — download / refresh GeoLite2-ASN.mmdb
#
# Run this script before building a new Docker image so the image bundles
# an up-to-date MaxMind ASN database. The .mmdb binary is git-ignored;
# only this script is tracked.
#
# Usage:
# # Option 1 — key in environment
# MAXMIND_LICENSE_KEY=your_key bash docker/geoip/update_geoip.sh
#
# # Option 2 — key in project .env file
# bash docker/geoip/update_geoip.sh
#
# The script writes GeoLite2-ASN.mmdb to the same directory as itself so
# the Dockerfile COPY step can find it at docker/geoip/GeoLite2-ASN.mmdb.
#
# Suggested automation: run via a host cron job or CI pipeline step
# before triggering a docker build, e.g.:
#
# # /etc/cron.weekly/update-sapl-geoip
# #!/bin/bash
# cd /path/to/sapl && bash docker/geoip/update_geoip.sh
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_FILE="$SCRIPT_DIR/GeoLite2-ASN.mmdb"
# ── Resolve the license key ────────────────────────────────────────────────
if [[ -z "${MAXMIND_LICENSE_KEY:-}" ]]; then
# Try the project .env (two directories up from docker/geoip/)
ENV_FILE="$(dirname "$(dirname "$SCRIPT_DIR")")/.env"
if [[ -f "$ENV_FILE" ]]; then
MAXMIND_LICENSE_KEY="$(grep -E '^MAXMIND_LICENSE_KEY=' "$ENV_FILE" 2>/dev/null \
| cut -d= -f2- | tr -d '[:space:]' || true)"
fi
fi
if [[ -z "${MAXMIND_LICENSE_KEY:-}" ]]; then
echo "ERROR: MAXMIND_LICENSE_KEY is not set." >&2
echo " Set it in the environment or add MAXMIND_LICENSE_KEY=<key> to .env" >&2
exit 1
fi
# ── Download ───────────────────────────────────────────────────────────────
URL="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz"
echo "[geoip] Downloading GeoLite2-ASN from MaxMind..."
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL --max-time 60 "$URL" | tar -xz --strip-components=1 -C "$tmpdir"
mv "$tmpdir"/GeoLite2-ASN.mmdb "$OUT_FILE"
echo "[geoip] Saved: $OUT_FILE"
echo "[geoip] Build date: $(python3 -c "
import struct, datetime, pathlib
data = pathlib.Path('$OUT_FILE').read_bytes()
# MaxMind DB build epoch is in the last 16 bytes of the metadata section
marker = b'\xab\xcd\xefMaxMind.com'
idx = data.rfind(marker)
if idx >= 0:
# search for 'build_epoch' key nearby
chunk = data[idx:idx+512]
pos = chunk.find(b'build_epoch')
if pos >= 0:
val_start = pos + len(b'build_epoch') + 1
epoch = struct.unpack('>Q', chunk[val_start+1:val_start+9])[0]
print(datetime.datetime.utcfromtimestamp(epoch).strftime('%Y-%m-%d'))
exit()
print('unknown')
" 2>/dev/null || echo "unknown")"

176
docker/scripts/redis_populate_test_data.py

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""
redis_populate_test_data.py inject synthetic rate-limiter entries into Redis.
Purpose: validate that RateLimitMiddleware reads the expected key schema,
that Redis CLI / RedisInsight shows the right structure, and that blocking
logic fires correctly without waiting for real traffic.
Usage:
# Against docker-compose Redis (default)
python3 docker/scripts/redis_populate_test_data.py
# Against a different host/port
REDIS_URL=redis://localhost:6379 python3 docker/scripts/redis_populate_test_data.py
# Show what would be written without actually writing
DRY_RUN=1 python3 docker/scripts/redis_populate_test_data.py
# Clear all synthetic keys written by a previous run
CLEAR=1 python3 docker/scripts/redis_populate_test_data.py
Key schema (DB 1 rate limiter):
rl:ip:{ip}:reqs INCR counter anonymous request count (TTL 60s)
rl:ip:{ip}:blocked string "1" IP hard-blocked (TTL 300s)
rl:{ns}:user:{uid}:reqs INCR counter auth user request count (TTL 60s)
rl:{ns}:user:{uid}:blocked string "1" user hard-blocked (TTL 300s)
rl:{ns}:ip:{ip}:w:{bucket} INCR namespace/IP sliding window (TTL 120s)
"""
import os
import sys
import time
# ── dependency check ──────────────────────────────────────────────────────
try:
import redis
except ImportError:
print("ERROR: redis-py not installed. Run: pip install redis", file=sys.stderr)
sys.exit(1)
# ── config ────────────────────────────────────────────────────────────────
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379")
RATELIMIT_DB = 1 # DB1 is the rate-limiter database
DRY_RUN = os.environ.get("DRY_RUN", "0").lower() in ("1", "true", "yes")
CLEAR = os.environ.get("CLEAR", "0").lower() in ("1", "true", "yes")
# Synthetic values — tweak to exercise different code paths
NAMESPACE = "sapl" # POD_NAMESPACE value (hostname or k8s namespace)
ANON_WINDOW = 60 # seconds — must match settings.RATE_LIMITER_RATE period
AUTH_WINDOW = 60
BLOCK_TTL = 300
TEST_IPS = [
"203.0.113.1", # below threshold (20 reqs)
"203.0.113.2", # AT threshold (35 reqs — should trigger block)
"203.0.113.3", # already blocked
"203.0.113.4", # namespace/window counter near threshold
]
TEST_USERS = [
{"uid": "42", "reqs": 50, "blocked": False}, # normal auth user
{"uid": "99", "reqs": 120, "blocked": False}, # AT auth threshold
{"uid": "7", "reqs": 10, "blocked": True}, # pre-blocked user
]
# ── helpers ───────────────────────────────────────────────────────────────
def key_ip_reqs(ip):
return f"rl:ip:{ip}:reqs"
def key_ip_blocked(ip):
return f"rl:ip:{ip}:blocked"
def key_user_reqs(ns, uid):
return f"rl:{ns}:user:{uid}:reqs"
def key_user_blocked(ns, uid):
return f"rl:{ns}:user:{uid}:blocked"
def key_ns_window(ns, ip, bucket):
return f"rl:{ns}:ip:{ip}:w:{bucket}"
def write(r, key, value, ttl, label):
if DRY_RUN:
print(f" [dry-run] SET {key!r} = {value!r} EX {ttl} ({label})")
return
if isinstance(value, int):
pipe = r.pipeline()
pipe.set(key, value, ex=ttl)
pipe.execute()
else:
r.set(key, value, ex=ttl)
print(f" SET {key!r} = {value!r} EX {ttl}s ({label})")
def delete_pattern(r, pattern):
keys = r.keys(pattern)
if keys:
r.delete(*keys)
print(f" DEL {len(keys)} keys matching {pattern!r}")
else:
print(f" (no keys matching {pattern!r})")
# ── main ──────────────────────────────────────────────────────────────────
def main():
r = redis.from_url(REDIS_URL, db=RATELIMIT_DB, decode_responses=True)
try:
r.ping()
except redis.ConnectionError as exc:
print(f"ERROR: cannot connect to Redis at {REDIS_URL}: {exc}", file=sys.stderr)
sys.exit(1)
print(f"Redis: {REDIS_URL} DB={RATELIMIT_DB} dry_run={DRY_RUN} clear={CLEAR}")
print()
# ── clear mode ────────────────────────────────────────────────────────
if CLEAR:
print("=== Clearing synthetic test keys ===")
for ip in TEST_IPS:
delete_pattern(r, f"rl:ip:{ip}:*")
delete_pattern(r, f"rl:{NAMESPACE}:ip:{ip}:*")
for u in TEST_USERS:
delete_pattern(r, f"rl:{NAMESPACE}:user:{u['uid']}:*")
print("Done.")
return
# ── anonymous IP counters ─────────────────────────────────────────────
print("=== Anonymous IP request counters (DB1) ===")
write(r, key_ip_reqs(TEST_IPS[0]), 20, ANON_WINDOW, "below threshold")
write(r, key_ip_reqs(TEST_IPS[1]), 35, ANON_WINDOW, "AT threshold → middleware will block on next req")
write(r, key_ip_reqs(TEST_IPS[3]), 30, ANON_WINDOW, "below threshold")
print()
# ── blocked IPs ───────────────────────────────────────────────────────
print("=== Blocked IPs (DB1) ===")
write(r, key_ip_blocked(TEST_IPS[2]), "1", BLOCK_TTL, "hard-blocked")
print()
# ── namespace/IP sliding window ───────────────────────────────────────
print("=== Namespace/IP sliding window (DB1) ===")
bucket = int(time.time() // ANON_WINDOW)
write(r, key_ns_window(NAMESPACE, TEST_IPS[3], bucket), 34, ANON_WINDOW * 2,
"near window threshold (next req triggers ua_rotation block)")
print()
# ── authenticated user counters ───────────────────────────────────────
print("=== Authenticated user request counters (DB1) ===")
for u in TEST_USERS:
if not u["blocked"]:
write(r, key_user_reqs(NAMESPACE, u["uid"]), u["reqs"], AUTH_WINDOW,
f"uid={u['uid']} reqs={u['reqs']}")
print()
# ── blocked users ─────────────────────────────────────────────────────
print("=== Blocked users (DB1) ===")
for u in TEST_USERS:
if u["blocked"]:
write(r, key_user_blocked(NAMESPACE, u["uid"]), "1", BLOCK_TTL,
f"uid={u['uid']} hard-blocked")
print()
# ── summary ───────────────────────────────────────────────────────────
if not DRY_RUN:
all_keys = r.keys("rl:*")
print(f"=== DB{RATELIMIT_DB} now contains {len(all_keys)} rl:* keys ===")
for k in sorted(all_keys):
ttl = r.ttl(k)
val = r.get(k)
print(f" {k!r:55s} val={val!r:5} ttl={ttl}s")
if __name__ == "__main__":
main()

110
rate-limiter-v2.md

@ -53,8 +53,9 @@ graph TD
| Key type | Key schema | TTL | DB | Est. size |
|---|---|---|---|---|
| Static cache (images/logos) | `static:{ns}:{sha256}` | 3–24 h | 0 | ~2.4 GB |
| PDF cache (≤ 360 KB) | `file:{ns}:{sha256}` | 1 h | 0 | ~0.9 GB |
| Page / view cache | `cache:{ns}:<django-generated>` | 60–600 s | 0 | ~0.5 GB |
| Static cache (images/logos) | `cache:{ns}:static:{sha256}` | 3–24 h | 0 | ~2.4 GB |
| PDF cache (≤ 360 KB) | `cache:{ns}:file:{sha256}` | 1 h | 0 | ~0.9 GB |
| IP request counter | `rl:ip:{ip}:reqs` | 60 s | 1 | ~0.6 MB |
| IP blocked marker | `rl:ip:{ip}:blocked` | 300 s | 1 | ~0.06 MB |
| User request counter | `rl:{ns}:user:{id}:reqs` | 60 s | 1 | negligible |
@ -68,8 +69,8 @@ graph TD
**Key conventions:**
- `{ns}` = Kubernetes namespace (tenant identifier). All path and user keys include it.
- `{user}` / `{id}` = normalized user PK: `str(user.pk).lower().strip()`.
- Django `CACHES` uses `KEY_PREFIX: "sapl"` to namespace DB0 cache keys.
DB1 (rate limiter) uses raw keys — no prefix — for compatibility with the Lua / middleware INCR scripts.
- Django `CACHES` uses `KEY_PREFIX: "cache:{ns}"` (e.g. `cache:sapl:`) to namespace all DB0 cache keys.
DB1 (rate limiter) uses raw `rl:*` keys — no prefix — for compatibility with the Lua / middleware INCR scripts.
- DB2 is reserved for Django Channels; allocate separately when WebSocket work resumes.
---
@ -636,9 +637,9 @@ spec:
| Use case | Key prefix | DB | TTL | Notes |
|---|---|---|---|---|
| Page / view cache | `sapl:cache:*` | 0 | 60–3600 s | `KEY_PREFIX=sapl` in Django CACHES |
| Static file cache (logos) | `static:{ns}:{sha256}` | 0 | 3–24 h | ns = namespace/tenant |
| PDF cache (≤ 360 KB) | `file:{ns}:{sha256}` | 0 | 1 h | ns required |
| Page / view cache | `cache:{ns}:*` | 0 | 60–600 s | `KEY_PREFIX=cache:{ns}` in Django CACHES |
| Static file cache (logos) | `cache:{ns}:static:{sha256}` | 0 | 3–24 h | ns = POD_NAMESPACE |
| PDF cache (≤ 360 KB) | `cache:{ns}:file:{sha256}` | 0 | 1 h | ns required |
| Rate limiter counters | `rl:*` | 1 | 60–300 s | Raw keys, no prefix |
| UA deny list | `rl:bot:ua:blocked` | 1 | permanent SET | Seed once after deploy |
| WebSocket / Channels | `channels:*` | 2 | session TTL | **Future — Phase 5** |
@ -661,7 +662,7 @@ CACHES = {
else 'django.core.cache.backends.filebased.FileBasedCache'
),
'LOCATION': REDIS_URL + '/0' if _redis_ready else '/var/tmp/django_cache',
'KEY_PREFIX': 'sapl',
'KEY_PREFIX': f'cache:{POD_NAMESPACE}', # e.g. "cache:sapl:" or "cache:patobranco-pr:"
**(
{
'OPTIONS': {
@ -841,6 +842,99 @@ kubectl exec -n redis deploy/sapl-redis -- redis-cli slowlog get 25
---
### 4.5 Inspecting Redis State
#### CLI quick-reference (redis-cli or `kubectl exec`)
```bash
# ── Connection ─────────────────────────────────────────────────────────────
# docker-compose
redis-cli -h localhost -p 6379
# k8s pod
kubectl exec -n <namespace> deploy/sapl-redis -- redis-cli
# ── DB selection (always specify -n for rate-limiter work) ─────────────────
# DB0 = page cache DB1 = rate limiter DB2 = channels (future)
redis-cli -n 1 # select DB1
# ── Key inspection ─────────────────────────────────────────────────────────
# List all rate-limiter keys
redis-cli -n 1 KEYS "rl:*"
# Request counter for a specific IP
redis-cli -n 1 GET "rl:ip:203.0.113.1:reqs"
# Remaining TTL on a counter
redis-cli -n 1 TTL "rl:ip:203.0.113.1:reqs"
# Check if an IP is hard-blocked
redis-cli -n 1 EXISTS "rl:ip:203.0.113.1:blocked"
# Authenticated user counter (ns = POD_NAMESPACE, uid = user pk)
redis-cli -n 1 GET "rl:sapl:user:42:reqs"
# Namespace/IP sliding window (bucket = epoch // 60)
redis-cli -n 1 KEYS "rl:sapl:ip:203.0.113.1:w:*"
# ── Manual block / unblock ─────────────────────────────────────────────────
# Block an IP for 5 minutes
redis-cli -n 1 SET "rl:ip:1.2.3.4:blocked" 1 EX 300
# Immediately unblock an IP
redis-cli -n 1 DEL "rl:ip:1.2.3.4:blocked"
# Unblock a user
redis-cli -n 1 DEL "rl:sapl:user:42:blocked"
# ── Aggregate stats ────────────────────────────────────────────────────────
# Count all blocked IPs right now
redis-cli -n 1 KEYS "rl:ip:*:blocked" | wc -l
# Count all blocked users
redis-cli -n 1 KEYS "rl:*:user:*:blocked" | wc -l
# Total DB1 key count
redis-cli -n 1 DBSIZE
# Memory used by DB1
redis-cli INFO keyspace | grep "db1"
# ── Cache DB inspection (DB0) ───────────────────────────────────────────────
# Count cached page responses (KEY_PREFIX = cache:{ns}, e.g. "cache:sapl:")
redis-cli -n 0 KEYS "cache:sapl:*" | wc -l
# Memory used by DB0
redis-cli INFO keyspace | grep "db0"
```
#### RedisInsight
Open `http://localhost:5540` (or whatever port you mapped) and connect to:
- **Host**: `localhost` (or the k8s service name)
- **Port**: `6379`
- **Database**: switch between DB0 (cache) and DB1 (rate limiter) using the database selector
Filter keys by prefix `rl:ip:` to see all anonymous IP counters, `rl:*:user:` for authenticated users.
#### Populate synthetic test data
```bash
# Inject test entries to validate key schema and blocking thresholds
python3 docker/scripts/redis_populate_test_data.py
# Preview what would be written (no side effects)
DRY_RUN=1 python3 docker/scripts/redis_populate_test_data.py
# Point at a non-default Redis
REDIS_URL=redis://sapl-redis.redis.svc:6379 python3 docker/scripts/redis_populate_test_data.py
# Clear all synthetic entries written by the script
CLEAR=1 python3 docker/scripts/redis_populate_test_data.py
```
---
## 5. Phase 2 — Rate Limiting & Bot Mitigation
**Goal**: Effective cross-pod throttling using shared Redis.

12
sapl/base/views.py

@ -48,11 +48,11 @@ from sapl.parlamentares.models import (
from sapl.protocoloadm.models import (Anexado, Protocolo)
from sapl.relatorios.views import (relatorio_estatisticas_acesso_normas)
from sapl.sessao.models import (Bancada, SessaoPlenaria)
from sapl.settings import EMAIL_SEND_USER, RATE_LIMITER_RATE
from sapl.settings import EMAIL_SEND_USER
from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured,
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, google_recaptcha_configured,
sapn_is_enabled, is_weak_password)
from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip
from sapl.middleware.ratelimit import smart_key, smart_rate
from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, EstatisticasAcessoNormasForm)
from .models import AppConfig, CasaLegislativa
@ -68,8 +68,8 @@ class IndexView(TemplateView):
return TemplateView.get(self, request, *args, **kwargs)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
method=ratelimit.UNSAFE,
block=True),
name='dispatch')
@ -1401,8 +1401,8 @@ class SaplSearchView(SearchView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarAuditLogView(PermissionRequiredMixin, FilterView):

7
sapl/comissoes/views.py

@ -29,7 +29,7 @@ from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud,
from sapl.materia.models import (MateriaEmTramitacao, MateriaLegislativa,
PautaReuniao, Tramitacao)
from sapl.middleware.page_cache import AnonCachePageMixin
from sapl.middleware.ratelimit import ratelimit_ip
from sapl.middleware.ratelimit import smart_key, smart_rate
from sapl.utils import show_results_filter_set
from .models import (CargoComissao, Comissao, Composicao, DocumentoAcessorio,
@ -38,7 +38,6 @@ from .models import (CargoComissao, Comissao, Composicao, DocumentoAcessorio,
from ratelimit.decorators import ratelimit
from django.utils.decorators import method_decorator
from ..settings import RATE_LIMITER_RATE
def pegar_url_composicao(pk):
@ -344,8 +343,8 @@ class RemovePautaView(PermissionRequiredMixin, CreateView):
return HttpResponseRedirect(success_url)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class AdicionaPautaView(PermissionRequiredMixin, FilterView):

19
sapl/crud/base.py

@ -26,8 +26,7 @@ from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display
from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL,
RP_LIST)
from sapl.settings import RATE_LIMITER_RATE
from sapl.middleware.ratelimit import ratelimit_ip
from sapl.middleware.ratelimit import smart_key, smart_rate
from sapl.utils import normalize
from ratelimit.decorators import ratelimit
@ -391,8 +390,8 @@ class CrudBaseMixin(CrispyLayoutFormMixin):
return self.model._meta.verbose_name_plural
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
@ -731,8 +730,8 @@ class CrudCreateView(PermissionRequiredContainerCrudMixin,
return super().form_valid(form)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class CrudDetailView(PermissionRequiredContainerCrudMixin,
@ -1189,8 +1188,8 @@ class MasterDetailCrud(Crud):
context['title'] = title
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class ListView(Crud.ListView):
@ -1425,8 +1424,8 @@ class MasterDetailCrud(Crud):
else:
return self.resolve_url(ACTION_LIST, args=(pk,))
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class DetailView(Crud.DetailView):

28
sapl/materia/views.py

@ -52,13 +52,13 @@ from sapl.materia.forms import (AnexadaForm, AutoriaForm, AutoriaMultiCreateForm
from sapl.norma.models import LegislacaoCitada
from sapl.parlamentares.models import Legislatura
from sapl.protocoloadm.models import Protocolo
from sapl.settings import MAX_DOC_UPLOAD_SIZE, MEDIA_ROOT, RATE_LIMITER_RATE
from sapl.settings import MAX_DOC_UPLOAD_SIZE, MEDIA_ROOT
from sapl.utils import (autor_label, autor_modal, gerar_hash_arquivo, get_base_url,
get_mime_type_from_file_extension, lista_anexados,
mail_service_configured, montar_row_autor, SEPARADOR_HASH_PROPOSICAO,
show_results_filter_set, get_tempfile_dir,
google_recaptcha_configured, MultiFormatOutputMixin)
from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip
from sapl.middleware.ratelimit import get_client_ip, smart_key, smart_rate
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AnexadaEmLoteFilterSet, AdicionarVariasAutoriasFilterSet,
@ -136,8 +136,8 @@ def proposicao_texto(request, pk):
raise Http404
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class AdicionarVariasAutorias(PermissionRequiredForAppCrudMixin, FilterView):
@ -362,8 +362,8 @@ class StatusTramitacaoCrud(CrudAux):
return reverse('sapl.materia:pesquisar_statustramitacao')
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarStatusTramitacaoView(FilterView):
@ -2014,8 +2014,8 @@ class AcompanhamentoExcluirView(TemplateView):
return HttpResponseRedirect(self.get_success_url())
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class MateriaLegislativaPesquisaView(MultiFormatOutputMixin, FilterView):
@ -2279,8 +2279,8 @@ class AcompanhamentoMateriaView(CreateView):
kwargs={'pk': self.kwargs['pk']})
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
@ -2395,8 +2395,8 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
return self.get(request, self.kwargs)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView):
@ -2523,8 +2523,8 @@ class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView):
return HttpResponseRedirect(success_url)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):

28
sapl/middleware/ratelimit.py

@ -108,6 +108,34 @@ def ratelimit_ip(group, request):
return get_client_ip(request)
def smart_key(group, request):
"""
Auth-aware key for @ratelimit decorators.
Authenticated users are keyed by user pk so that office workers sharing
a NAT IP don't count against each other. Anonymous requests fall back to
the masked IP (IPv6 /64 collapsed via ip_mask).
"""
user = getattr(request, 'user', None)
if user is not None and user.is_authenticated:
return str(user.pk)
return ratelimit_ip(group, request)
def smart_rate(group, request):
"""
Auth-aware rate string for @ratelimit decorators.
Returns RATE_LIMITER_RATE_AUTHENTICATED for authenticated users,
RATE_LIMITER_RATE for anonymous users mirrors the thresholds applied
by RateLimitMiddleware so view-level and middleware-level limits agree.
"""
user = getattr(request, 'user', None)
if user is not None and user.is_authenticated:
return settings.RATE_LIMITER_RATE_AUTHENTICATED
return settings.RATE_LIMITER_RATE
def _is_suspicious_headers(request):
"""Real browsers send Accept-Language + Accept; bots frequently omit them."""
missing = sum([

12
sapl/norma/views.py

@ -30,7 +30,7 @@ from sapl.compilacao.views import IntegracaoTaView
from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
MasterDetailCrud, make_pagination)
from sapl.materia.models import Orgao
from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip
from sapl.middleware.ratelimit import get_client_ip, smart_key, smart_rate
from sapl.utils import show_results_filter_set, \
sapn_is_enabled, MultiFormatOutputMixin
@ -39,7 +39,7 @@ from .forms import (AnexoNormaJuridicaForm, NormaFilterSet, NormaJuridicaForm,
AutoriaNormaForm, AssuntoNormaFilterSet)
from .models import (AnexoNormaJuridica, AssuntoNorma, NormaJuridica, NormaRelacionada,
TipoNormaJuridica, TipoVinculoNormaJuridica, AutoriaNorma, NormaEstatisticas)
from ..settings import RATE_LIMITER_RATE
# LegislacaoCitadaCrud = Crud.build(LegislacaoCitada, '')
TipoNormaCrud = CrudAux.build(
@ -61,8 +61,8 @@ class AssuntoNormaCrud(CrudAux):
return reverse('sapl.norma:pesquisar_assuntonorma')
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarAssuntoNormaView(FilterView):
@ -155,8 +155,8 @@ class NormaRelacionadaCrud(MasterDetailCrud):
layout_key = 'NormaRelacionadaDetail'
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class NormaPesquisaView(MultiFormatOutputMixin, FilterView):

16
sapl/parlamentares/views.py

@ -34,7 +34,7 @@ from sapl.norma.models import AutoriaNorma, NormaJuridica
from sapl.parlamentares.apps import AppConfig
from sapl.rules import SAPL_GROUP_VOTANTE
from sapl.middleware.page_cache import AnonCachePageMixin
from sapl.middleware.ratelimit import ratelimit_ip
from sapl.middleware.ratelimit import smart_key, smart_rate
from sapl.utils import (parlamentares_ativos, show_results_filter_set)
from .forms import (ColigacaoFilterSet, FiliacaoForm, FrenteForm, LegislaturaForm, MandatoForm,
@ -50,7 +50,7 @@ from .models import (CargoMesa, Coligacao, ComposicaoColigacao, ComposicaoMesa,
from ratelimit.decorators import ratelimit
from django.utils.decorators import method_decorator
from ..settings import RATE_LIMITER_RATE
FrenteCargoCrud = CrudAux.build(FrenteCargo, 'frente_cargo')
BlocoCargoCrud = CrudAux.build(BlocoCargo, 'bloco_cargo')
@ -190,8 +190,8 @@ class ProposicaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
_('Texto Eletrônico'))
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarParlamentarView(FilterView):
@ -256,8 +256,8 @@ class PesquisarParlamentarView(FilterView):
return self.render_to_response(context)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarColigacaoView(FilterView):
@ -316,8 +316,8 @@ class PesquisarColigacaoView(FilterView):
return self.render_to_response(context)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarPartidoView(FilterView):

24
sapl/protocoloadm/views.py

@ -44,7 +44,7 @@ from sapl.protocoloadm.forms import VinculoDocAdminMateriaForm,\
from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo,\
VinculoDocAdminMateria
from sapl.relatorios.views import relatorio_doc_administrativos
from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip
from sapl.middleware.ratelimit import get_client_ip, smart_key, smart_rate
from sapl.utils import (create_barcode, get_base_url,
get_mime_type_from_file_extension, lista_anexados,
show_results_filter_set, mail_service_configured, from_date_to_datetime_utc,
@ -63,7 +63,7 @@ from .forms import (AcompanhamentoDocumentoForm, AnexadoEmLoteFilterSet, Anexado
from .models import (Anexado, AcompanhamentoDocumento, DocumentoAcessorioAdministrativo,
DocumentoAdministrativo, StatusTramitacaoAdministrativo,
TipoDocumentoAdministrativo, TramitacaoAdministrativo)
from ..settings import MEDIA_ROOT, RATE_LIMITER_RATE
from ..settings import MEDIA_ROOT
from ratelimit.decorators import ratelimit
from django.utils.decorators import method_decorator
@ -539,8 +539,8 @@ class StatusTramitacaoAdministrativoCrud(CrudAux):
ordering = 'sigla'
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView):
@ -1040,8 +1040,8 @@ class ProtocoloMateriaTemplateView(PermissionRequiredMixin, TemplateView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
@ -1177,8 +1177,8 @@ class AnexadoCrud(MasterDetailCrud):
return 'AnexadoDetail'
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class DocumentoAnexadoEmLoteView(PermissionRequiredMixin, FilterView):
@ -1658,8 +1658,8 @@ class FichaSelecionaAdmView(PermissionRequiredMixin, FormView):
'materia/impressos/ficha_adm_pdf.html')
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PrimeiraTramitacaoEmLoteAdmView(PermissionRequiredMixin, FilterView):
@ -1898,8 +1898,8 @@ class VinculoDocAdminMateriaCrud(MasterDetailCrud):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class VinculoDocAdminMateriaEmLoteView(PermissionRequiredMixin, FilterView):

64
sapl/relatorios/views.py

@ -48,11 +48,11 @@ from sapl.sessao.views import (get_identificacao_basica, get_mesa_diretora,
get_oradores_explicacoes_pessoais, get_consideracoes_finais,
get_ocorrencias_da_sessao, get_assinaturas,
get_correspondencias)
from sapl.settings import MEDIA_URL, RATE_LIMITER_RATE
from sapl.settings import MEDIA_URL
from sapl.settings import STATIC_ROOT
from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data, create_barcode, show_results_filter_set, \
num_materias_por_tipo, parlamentares_ativos, MultiFormatOutputMixin
from sapl.middleware.ratelimit import ratelimit_ip
from sapl.middleware.ratelimit import smart_key, smart_rate
from .templates import (pdf_capa_processo_gerar,
pdf_documento_administrativo_gerar, pdf_espelho_gerar,
pdf_etiqueta_protocolo_gerar, pdf_materia_gerar,
@ -1866,8 +1866,8 @@ class RelatorioMixin:
return self.render_to_response(context)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioDocumentosAcessoriosView(RelatorioMixin, FilterView):
@ -1914,8 +1914,8 @@ class RelatorioDocumentosAcessoriosView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioVotacoesNominaisView(RelatorioMixin, MultiFormatOutputMixin, FilterView):
@ -1987,8 +1987,8 @@ class RelatorioVotacoesNominaisView(RelatorioMixin, MultiFormatOutputMixin, Filt
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioAtasView(RelatorioMixin, FilterView):
@ -2016,8 +2016,8 @@ class RelatorioAtasView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
@ -2254,8 +2254,8 @@ class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioHistoricoTramitacaoView(RelatorioMixin, FilterView):
@ -2315,8 +2315,8 @@ class RelatorioHistoricoTramitacaoView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioDataFimPrazoTramitacaoView(RelatorioMixin, FilterView):
@ -2382,8 +2382,8 @@ class RelatorioDataFimPrazoTramitacaoView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioReuniaoView(RelatorioMixin, FilterView):
@ -2420,8 +2420,8 @@ class RelatorioReuniaoView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioAudienciaView(RelatorioMixin, FilterView):
@ -2458,8 +2458,8 @@ class RelatorioAudienciaView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioMateriasTramitacaoView(RelatorioMixin, FilterView):
@ -2576,8 +2576,8 @@ class RelatorioMateriasTramitacaoView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioMateriasPorAnoAutorTipoView(RelatorioMixin, FilterView):
@ -2659,8 +2659,8 @@ class RelatorioMateriasPorAnoAutorTipoView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioMateriasPorAutorView(RelatorioMixin, FilterView):
@ -2734,8 +2734,8 @@ class RelatorioMateriaAnoAssuntoView(ListView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioNormasPublicadasMesView(RelatorioMixin, FilterView):
@ -2778,8 +2778,8 @@ class RelatorioNormasPublicadasMesView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioNormasVigenciaView(RelatorioMixin, FilterView):
@ -2846,8 +2846,8 @@ class RelatorioNormasVigenciaView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioHistoricoTramitacaoAdmView(RelatorioMixin, FilterView):
@ -2900,8 +2900,8 @@ class RelatorioHistoricoTramitacaoAdmView(RelatorioMixin, FilterView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class RelatorioNormasPorAutorView(RelatorioMixin, FilterView):

20
sapl/sessao/views.py

@ -47,8 +47,8 @@ from sapl.sessao.apps import AppConfig
from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm, OrdemExpedienteLeituraForm, \
CorrespondenciaForm, CorrespondenciaEmLoteFilterSet
from sapl.sessao.models import Correspondencia
from sapl.settings import TIME_ZONE, RATE_LIMITER_RATE
from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip
from sapl.settings import TIME_ZONE
from sapl.middleware.ratelimit import get_client_ip, smart_key, smart_rate
from sapl.utils import show_results_filter_set, remover_acentos, \
MultiFormatOutputMixin, PautaMultiFormatOutputMixin
@ -3798,8 +3798,8 @@ class SessaoListView(ListView):
return context
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PautaSessaoView(TemplateView):
@ -3817,8 +3817,8 @@ class PautaSessaoView(TemplateView):
reverse('sapl.sessao:pauta_sessao_detail', kwargs={'pk': sessao.pk}))
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
@ -4002,8 +4002,8 @@ class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
return self.render_to_response(context)
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class PesquisarSessaoPlenariaView(MultiFormatOutputMixin, FilterView):
@ -5394,8 +5394,8 @@ class CorrespondenciaCrud(MasterDetailCrud):
return obj
@method_decorator(ratelimit(key=ratelimit_ip,
rate=RATE_LIMITER_RATE,
@method_decorator(ratelimit(key=smart_key,
rate=smart_rate,
block=True),
name='dispatch')
class CorrespondenciaEmLoteView(PermissionRequiredMixin, FilterView):

7
sapl/settings.py

@ -34,7 +34,7 @@ PROJECT_DIR = Path(__file__).ancestor(2)
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='32jk1h412l3kjh421lkj4hlkj234')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
DEBUG = config('DEBUG', default=True, cast=bool)
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
@ -205,7 +205,8 @@ SPECTACULAR_SETTINGS = {
}
# ---------------------------------------------------------------------------
# Tenant namespace — used as Redis KEY_PREFIX and rate-limiter scope.
# Tenant namespace — used as Redis cache KEY_PREFIX (cache:{ns}:*) and
# as the rate-limiter scope for per-namespace keys.
# Defaults to the machine hostname so self-hosted (bare-metal / VM /
# docker-compose) deployments work without any extra config.
# On Kubernetes, POD_NAMESPACE is set by start.sh via the Downward API or
@ -237,7 +238,7 @@ CACHES = {
else 'django.core.cache.backends.filebased.FileBasedCache'
),
'LOCATION': REDIS_URL + '/0' if _redis_ready else '/var/tmp/django_cache',
'KEY_PREFIX': POD_NAMESPACE,
'KEY_PREFIX': f'cache:{POD_NAMESPACE}',
**(
{
'OPTIONS': {

Loading…
Cancel
Save