From 3ad19ed08905e8637e9c47ac66f0a1de9f991298 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Wed, 16 Sep 2026 12:09:53 +0200 Subject: [PATCH] =?UTF-8?q?test(backend):=20g=C3=A9n=C3=A9ralise=20la=20v?= =?UTF-8?q?=C3=A9rification=20du=20403=20de=20r=C3=B4le=20=C3=A0=20toute?= =?UTF-8?q?=20route,=20pas=20seulement=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_every_administration_route_documents_the_role_refusal ne couvrait que le tag users : sites vient de passer inaperçu deux fois (tag et 403 absents) pendant le merge avec dev. Remplacé par ROUTES_A_ROLE, une liste maintenue à la main sur le modèle de ORIGINE_VERIFIEE, qui couvre toute route derrière require_role. Ajoute la checklist d'ajout d'une route métier à 20-backend.md. --- apps/backend/tests/api/test_openapi.py | 15 +++++++++++++-- docs/architecture/20-backend.md | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/apps/backend/tests/api/test_openapi.py b/apps/backend/tests/api/test_openapi.py index 96297c0..b46b5e7 100644 --- a/apps/backend/tests/api/test_openapi.py +++ b/apps/backend/tests/api/test_openapi.py @@ -22,6 +22,17 @@ ORIGINE_VERIFIEE = { ("POST", "/api/v1/auth/password"), } +# Toute route derrière `require_role` (LecteurDep, OperateurDep, AdminDep) peut rendre 403 pour +# `password_change_required`, pas seulement les routes `admin`. +ROUTES_A_ROLE = { + ("GET", "/api/v1/users"), + ("POST", "/api/v1/users"), + ("PATCH", "/api/v1/users/{id}"), + ("POST", "/api/v1/users/{id}/password-reset"), + ("GET", "/api/v1/sites"), + ("GET", "/api/v1/sites/{site_id}"), +} + @pytest.fixture(scope="module") def schema() -> dict[str, Any]: @@ -55,11 +66,11 @@ def test_every_route_demanding_an_identity_says_how_it_refuses(schema: dict[str, assert muettes == [] -def test_every_administration_route_documents_the_role_refusal(schema: dict[str, Any]) -> None: +def test_every_role_guarded_route_documents_the_role_refusal(schema: dict[str, Any]) -> None: sans_403 = [ (methode, chemin) for methode, chemin, operation in operations(schema) - if "users" in operation.get("tags", []) and "403" not in operation["responses"] + if (methode, chemin) in ROUTES_A_ROLE and "403" not in operation["responses"] ] assert sans_403 == [] diff --git a/docs/architecture/20-backend.md b/docs/architecture/20-backend.md index f48178f..920fcf1 100644 --- a/docs/architecture/20-backend.md +++ b/docs/architecture/20-backend.md @@ -235,6 +235,20 @@ Les modèles de `app/schemas/errors.py` décrivent ce que les gestionnaires renv `loc` n'apparaît dans aucune réponse de cette API : `validation_error_handler()` rend `champ` et `type`. Renommer un champ là-bas sans le faire ici rend la documentation fausse en silence. +**Ajouter une route métier** (`sites` est le gabarit, `reading`/`dataset`/`prediction`/`alert`/ +`recommendation` suivront) : + +1. Composer ses `responses=` depuis `app/api/openapi.py` : `REPONSES_LECTEUR` ou `REPONSES_ADMIN` + au niveau de l'`include_router` dans `app/api/v1/router.py` (401 et le 403 propre au rôle), + `REPONSE_VALIDATION` et les codes locaux (404, 409, ...) sur l'endpoint lui-même s'il a un + corps, un paramètre ou peut échouer par identifiant. +2. Décrire son tag dans `TAGS` (`app/api/openapi.py`). +3. Si elle passe par `require_role`, l'ajouter à `ROUTES_A_ROLE` + (`tests/api/test_openapi.py`) ; si elle passe par `require_trusted_origin`, l'ajouter à + `ORIGINE_VERIFIEE`. Ces deux listes sont maintenues à la main, pas déduites automatiquement du + code : une route protégée qui n'y figure pas ne sera pas détectée par les tests. +4. `make openapi`, puis `pytest tests/api/test_openapi.py`. + ## Sécurité Voir la vue consolidée dans [00-vue-ensemble.md](00-vue-ensemble.md) et les décisions dans les