Files
GTI_GERENCIADOR_FISCAL/gerenciador/app/rate_limit.py
T
gti d95165fc8f Add 'gerenciador/' from commit '34cc9c75a57dedbf61a6e6a1ba1f35f76e72434b'
git-subtree-dir: gerenciador
git-subtree-mainline: f4f546c928
git-subtree-split: 34cc9c75a5
2026-09-29 14:43:12 -03:00

53 lines
2.2 KiB
Python

"""
Limitador de tentativas de login por IP de origem — proteção simples
contra força bruta de senha em POST /login (app/routers/auth.py), que é
uma rota pré-autenticação e portanto alcançável por qualquer visitante.
Em memória (dict por processo), no mesmo espírito de app/session.py (chave
de assinatura em memória) e app/db/connection.py (sem pool): este painel
roda como container único, sem múltiplas réplicas atrás de um load
balancer, então não precisa de armazenamento compartilhado (Redis etc.)
para isso funcionar. Reinício do processo zera o histórico — efeito
colateral aceito, mesma lógica do SESSION_SECRET em memória.
Se um dia o painel passar a rodar atrás de um proxy reverso, a origem
(hoje request.client.host, lida em app/routers/auth.py) precisa passar a
vir de X-Forwarded-For validado contra uma lista de proxies confiáveis —
sem isso, toda tentativa apareceria vindo do IP do proxy e o limite
passaria a valer para todo mundo junto (um IP externo malicioso poderia
bloquear logins de todo mundo).
"""
import time
MAX_TENTATIVAS = 5
JANELA_SEGUNDOS = 5 * 60 # 5 minutos
_falhas: dict[str, list[float]] = {}
def _recentes(origem: str, momento: float) -> list[float]:
limite = momento - JANELA_SEGUNDOS
return [t for t in _falhas.get(origem, []) if t > limite]
def registrar_falha(origem: str, agora: float | None = None) -> None:
"""Chamado a cada tentativa de login com credencial inválida."""
momento = agora if agora is not None else time.time()
recentes = _recentes(origem, momento)
recentes.append(momento)
_falhas[origem] = recentes
def bloqueado(origem: str, agora: float | None = None) -> bool:
"""True quando essa origem já teve MAX_TENTATIVAS falhas nos últimos
JANELA_SEGUNDOS — POST /login deve recusar antes mesmo de checar a
credencial enviada."""
momento = agora if agora is not None else time.time()
return len(_recentes(origem, momento)) >= MAX_TENTATIVAS
def limpar(origem: str) -> None:
"""Chamado após um login bem-sucedido — não faz sentido continuar
contando falhas antigas contra quem acabou de provar a senha certa."""
_falhas.pop(origem, None)