Fail-fast sur la configuration. Une limite de debit illisible (10/hours, 10/banana, -1/hour) arrete desormais le demarrage avec un message nommant la variable, au lieu de desactiver silencieusement la protection : seules les valeurs 0, off, none, disabled et vide desactivent volontairement. Meme traitement pour MAX_SHARED_MAPS, MAX_SHARE_SIZE_MB, DEFAULT_SHARE_EXPIRY_DAYS et chaque entree de TRUSTED_PROXY_CIDRS. X-Forwarded-For n'est plus cru sur la seule foi de TRUST_PROXY_HEADERS : la connexion doit aussi provenir d'un reseau declare dans TRUSTED_PROXY_CIDRS (ipaddress, IPv4/IPv6/CIDR). Activer la confiance sans declarer de reseau est refuse au demarrage, faute de quoi la configuration semblerait active sans l'etre. L'adresse retenue est normalisee : les differentes ecritures d'une meme IPv6 ne donnent plus des compteurs distincts. MAX_SHARED_MAPS : comptage et insertion serialises par un verrou de processus, pour que deux creations simultanees ne franchissent pas ensemble la derniere place. Documente comme valable pour un service applicatif unique. Ajoute APP_BIND pour ne pas publier 8080 sur toutes les interfaces en production, une option backup --keep pour la retention, et documente Nginx, Traefik en override, cron et systemd. Corrige un bug de rendu du build local : mon edition avait reecrit docker-entrypoint.sh en CRLF, rendant le shebang introuvable dans le conteneur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74 lines
2.6 KiB
Python
74 lines
2.6 KiB
Python
"""Dependances communes aux routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends, Header, Request
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.config import Settings, get_settings
|
|
from app.db.session import get_db
|
|
from app.services.client_ip import resolve_client_ip
|
|
from app.services.errors import RateLimitedError, ShareForbiddenError, SharingDisabledError
|
|
from app.services.rate_limit import limiter, parse_rate
|
|
|
|
MANAGEMENT_TOKEN_HEADER = "X-Management-Token"
|
|
|
|
DbSession = Annotated[Session, Depends(get_db)]
|
|
AppSettings = Annotated[Settings, Depends(get_settings)]
|
|
|
|
|
|
def require_management_token(
|
|
x_management_token: Annotated[str | None, Header(alias=MANAGEMENT_TOKEN_HEADER)] = None,
|
|
) -> str:
|
|
"""Recupere le secret de gestion depuis l'en-tete (jamais depuis l'URL)."""
|
|
if not x_management_token:
|
|
raise ShareForbiddenError("Jeton de gestion manquant.")
|
|
return x_management_token
|
|
|
|
|
|
ManagementToken = Annotated[str, Depends(require_management_token)]
|
|
|
|
|
|
def require_sharing_enabled(settings: AppSettings) -> None:
|
|
"""Coupe toute l'API de partage quand l'instance est privee.
|
|
|
|
Les cartes locales continuent de fonctionner : seul le service serveur est
|
|
indisponible, et il le dit clairement.
|
|
"""
|
|
if not settings.sharing_enabled:
|
|
raise SharingDisabledError()
|
|
|
|
|
|
SharingEnabled = Annotated[None, Depends(require_sharing_enabled)]
|
|
|
|
|
|
def _rate_limit(bucket: str, spec_name: str): # type: ignore[no-untyped-def]
|
|
"""Fabrique une dependance de limitation pour une famille d'endpoints.
|
|
|
|
La limitation est posee sur les routes concernees, jamais en middleware
|
|
global : /api/health et /api/ready ne peuvent donc pas etre bloques, ce qui
|
|
garantit que le healthcheck du conteneur reste fiable sous charge.
|
|
"""
|
|
|
|
def dependency(request: Request, settings: AppSettings) -> None:
|
|
rule = parse_rate(getattr(settings, spec_name))
|
|
if rule is None:
|
|
return
|
|
client = resolve_client_ip(
|
|
request,
|
|
trust_proxy_headers=settings.trust_proxy_headers,
|
|
trusted_networks=settings.trusted_proxy_networks,
|
|
)
|
|
retry_after = limiter.check(f"{bucket}:{client}", rule)
|
|
if retry_after is not None:
|
|
raise RateLimitedError(retry_after)
|
|
|
|
return dependency
|
|
|
|
|
|
RateLimitCreate = Annotated[None, Depends(_rate_limit("share:create", "share_create_rate_limit"))]
|
|
RateLimitRead = Annotated[None, Depends(_rate_limit("share:read", "share_read_rate_limit"))]
|
|
RateLimitWrite = Annotated[None, Depends(_rate_limit("share:write", "share_write_rate_limit"))]
|