Files
Johan LEROY e8f22bf427 feat(backend): durcit la surface exposée
En-têtes de sécurité, CORS resserré, caviardage des journaux, `/metrics`
derrière un jeton facultatif, documentation fermée en préproduction, et
la sonde de disponibilité cesse de publier la version de TimescaleDB.

HSTS et CSP sont volontairement absents : l'application ignore si TLS
termine devant elle, et une CSP sur une API JSON ne protège presque rien.
Les deux appartiennent au terminateur TLS, celle qui compte protège la
page Angular.

`/metrics` est gardé par un jeton statique et non par un rôle : coupler
la supervision au modèle d'utilisateurs casserait la collecte à chaque
panne d'authentification, c'est-à-dire quand on en a le plus besoin. Le
contrôle principal reste le réseau.

Le caviardage est la troisième ligne de défense, pas la première. On ne
passe aucun secret au logger et aucun jeton dans une URL ; le filtre
rattrape ce que personne n'a relu, à commencer par l'écho SQL qui
publiait les empreintes Argon2 quand `debug` est actif.

Corrige un défaut que le test a révélé : `create_app(settings)` ne
pilotait que la construction, les dépendances continuaient de lire
`get_settings()` depuis l'environnement. Un test « en production » ne
testait donc pas la production, et `TESTING.md` promet le contraire.
2026-09-15 14:56:17 +02:00

73 lines
2.1 KiB
Python

import logging
import pytest
from app.core.logging import CAVIARDAGE, RedactingFilter, redact
@pytest.mark.parametrize(
"message",
[
"Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.charge-utile-assez-longue.signature",
"jeton brut eyJhbGciOiJIUzI1NiJ9abcdefghijklmnopqrstuvwxyz",
"INSERT ... ('$argon2id$v=19$m=19456,t=2,p=1$sel-en-clair$empreinte-en-clair')",
'{"password": "le-mot-de-passe-du-client"}',
"current_password=le-mot-de-passe",
"Cookie: ev_refresh=abcdefghijklmnopqrstuvwxyz0123456789",
],
ids=[
"en_tete_bearer",
"jeton_jwt_nu",
"empreinte_argon2",
"mot_de_passe_json",
"mot_de_passe_en_paire",
"cookie_de_rafraichissement",
],
)
def test_redact_removes_every_known_secret_shape(message: str) -> None:
expurge = redact(message)
assert CAVIARDAGE in expurge
for suspect in ("le-mot-de-passe", "empreinte-en-clair", "abcdefghijklmnopqrstuvwxyz"):
assert suspect not in expurge
def test_redact_leaves_an_innocent_message_untouched() -> None:
message = "auth.login.success user_id=3f2a ip=203.0.113.10"
assert redact(message) == message
def test_the_filter_rewrites_the_record_before_it_reaches_the_handler() -> None:
enregistrement = logging.LogRecord(
name="app",
level=logging.INFO,
pathname=__file__,
lineno=1,
msg='requete {"password": "%s"}',
args=("secret-du-client",),
exc_info=None,
)
conserve = RedactingFilter().filter(enregistrement)
assert conserve is True
assert "secret-du-client" not in enregistrement.getMessage()
def test_the_filter_keeps_a_record_that_holds_no_secret() -> None:
enregistrement = logging.LogRecord(
name="app",
level=logging.INFO,
pathname=__file__,
lineno=1,
msg="requete %s",
args=("/api/v1/health/live",),
exc_info=None,
)
conserve = RedactingFilter().filter(enregistrement)
assert conserve is True
assert enregistrement.getMessage() == "requete /api/v1/health/live"