Add 'gerenciador/' from commit '34cc9c75a57dedbf61a6e6a1ba1f35f76e72434b'

git-subtree-dir: gerenciador
git-subtree-mainline: f4f546c928
git-subtree-split: 34cc9c75a5
This commit is contained in:
gti
2026-09-29 14:43:12 -03:00
84 changed files with 8906 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
"""
Endpoint interno server-to-server, read-only, para o GTI Fiscal espelhar o
cadastro de clientes. GTI Gerenciador continua sendo a fonte única da
verdade: o Fiscal NUNCA recebe credencial MySQL direta (nem lê a tabela
CLIENTE por fora) — só chama esta rota, autenticada por um bearer token
dedicado (FISCAL_INTERNAL_API_TOKEN), nunca reaproveitado de SESSION_SECRET,
da chave privada do SSO, de senha de admin ou de token de Agent.
Rate limit em memória por IP de origem — mesmo espírito de app/rate_limit.py
(container único, sem load balancer; ver ali o aviso sobre proxy reverso).
"""
from __future__ import annotations
import hmac
import os
import time
from fastapi import APIRouter, Header, HTTPException, Request
from .db.clientes import listar_clientes_para_fiscal
router = APIRouter()
MAX_TENTATIVAS = 10
JANELA_SEGUNDOS = 5 * 60
_falhas: dict[str, list[float]] = {}
def _token_configurado() -> bool:
return bool(os.environ.get("FISCAL_INTERNAL_API_TOKEN"))
def _token_valido(fornecido: str | None) -> bool:
esperado = os.environ.get("FISCAL_INTERNAL_API_TOKEN", "")
if not esperado or not fornecido:
return False
return hmac.compare_digest(fornecido, esperado)
def _bloqueado(origem: str, agora: float | None = None) -> bool:
momento = agora if agora is not None else time.time()
limite = momento - JANELA_SEGUNDOS
return len([t for t in _falhas.get(origem, []) if t > limite]) >= MAX_TENTATIVAS
def _registrar_falha(origem: str, agora: float | None = None) -> None:
momento = agora if agora is not None else time.time()
limite = momento - JANELA_SEGUNDOS
recentes = [t for t in _falhas.get(origem, []) if t > limite]
recentes.append(momento)
_falhas[origem] = recentes
@router.get("/internal/fiscal/clients")
def listar_clientes_fiscal(request: Request, authorization: str | None = Header(default=None)):
origem = request.client.host if request.client else "unknown"
if _bloqueado(origem):
raise HTTPException(status_code=429, detail="muitas tentativas")
if not _token_configurado():
# Sem token configurado = integração desligada, nunca "aberto por padrão".
raise HTTPException(status_code=503, detail="FISCAL_INTERNAL_API_TOKEN não configurado")
fornecido = authorization.removeprefix("Bearer ").strip() if authorization and authorization.startswith("Bearer ") else None
if not _token_valido(fornecido):
_registrar_falha(origem)
raise HTTPException(status_code=401, detail="token inválido ou ausente")
return {"clients": listar_clientes_para_fiscal()}