Compare commits

..
Author SHA1 Message Date
Johan LEROY 3ad19ed089 test(backend): généralise la vérification du 403 de rôle à toute route, pas seulement admin
Backend / Lint, typage et tests (push) Successful in 1m6s
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.
2026-09-16 12:09:53 +02:00
7 changed files with 42 additions and 41 deletions
+3 -21
View File
@@ -1,36 +1,18 @@
BACKEND := apps/backend BACKEND := apps/backend
FRONTEND := apps/frontend
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.PHONY: help install install-backend install-frontend dev dev-backend dev-frontend \ .PHONY: help install dev lint format typecheck test test-cov test-integration check \
lint format typecheck test test-cov test-integration check \
openapi docker-build db-up db-down db-reset db-logs db-psql migrate bootstrap-admin openapi docker-build db-up db-down db-reset db-logs db-psql migrate bootstrap-admin
help: ## Liste les cibles disponibles help: ## Liste les cibles disponibles
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
install: install-backend install-frontend ## Installe les dépendances backend et frontend install: ## Installe les dépendances du backend
install-backend: ## Installe les dépendances du backend
cd $(BACKEND) && uv sync --all-groups cd $(BACKEND) && uv sync --all-groups
install-frontend: ## Installe les dépendances du frontend dev: ## Lance l'API en rechargement à chaud
cd $(FRONTEND) && npm ci
dev: ## Lance toute la stack (backend + frontend) en rechargement à chaud
@trap 'kill 0' EXIT INT TERM; \
$(MAKE) --no-print-directory dev-backend & \
$(MAKE) --no-print-directory dev-frontend & \
wait
dev-backend: ## Lance l'API seule en rechargement à chaud
@echo "backend -> http://localhost:8000 (docs sur /docs)"
cd $(BACKEND) && uv run uvicorn app.main:create_app --factory --reload --host 0.0.0.0 --port 8000 cd $(BACKEND) && uv run uvicorn app.main:create_app --factory --reload --host 0.0.0.0 --port 8000
dev-frontend: ## Lance le frontend seul en rechargement à chaud
@echo "frontend -> http://localhost:4200"
cd $(FRONTEND) && npm start
lint: ## Analyse statique du backend lint: ## Analyse statique du backend
cd $(BACKEND) && uv run ruff check . cd $(BACKEND) && uv run ruff check .
+6 -9
View File
@@ -63,17 +63,16 @@ L'etat detaille de chaque brique et les vues d'architecture sont dans
## Demarrage ## Demarrage
Prerequis : uv, Docker, Node 24 LTS (npm fourni). Le poste doit disposer de Python 3.14, que Prerequis : uv, Docker. Le poste doit disposer de Python 3.14, que `uv` installe seul.
`uv` installe seul.
```bash ```bash
cp .env.example .env # variables de docker-compose cp .env.example .env # variables de docker-compose
cp apps/backend/.env.example apps/backend/.env # variables du backend hors conteneur cp apps/backend/.env.example apps/backend/.env # variables du backend hors conteneur
make db-up # PostgreSQL + TimescaleDB, publie sur le port 5433 make db-up # PostgreSQL + TimescaleDB, publie sur le port 5433
make install # dependances du backend et du frontend make install # dependances du backend
make migrate # applique les migrations Alembic make migrate # applique les migrations Alembic
make dev # backend sur http://localhost:8000 (docs sur /docs), frontend sur http://localhost:4200 make dev # API sur http://localhost:8000, docs sur /docs
make check # lint + typage + tests make check # lint + typage + tests
``` ```
@@ -84,11 +83,9 @@ Deux fichiers d'environnement, deux usages : `.env` a la racine alimente `docker
5432, souvent deja pris par une autre base. 5432, souvent deja pris par une autre base.
La boucle de developpement est `make db-up` puis `make dev` : seule la base tourne en La boucle de developpement est `make db-up` puis `make dev` : seule la base tourne en
conteneur, le backend et le frontend tournent tous les deux sur le poste, lances ensemble par conteneur. Le service `backend` du `docker-compose.yml` sert la stack complete et la recette,
`make dev` (logs entrelaces dans le meme terminal, Ctrl+C arrete les deux). `make dev-backend` et n'embarque pas le source, donc toute modification y demande un
et `make dev-frontend` restent disponibles pour lancer un seul des deux. Le service `backend` `docker compose up -d --build backend`.
du `docker-compose.yml` sert la stack complete et la recette, et n'embarque pas le source, donc
toute modification y demande un `docker compose up -d --build backend`.
Verifier que la base repond et que l'extension est chargee : Verifier que la base repond et que l'extension est chargee :
+13 -2
View File
@@ -22,6 +22,17 @@ ORIGINE_VERIFIEE = {
("POST", "/api/v1/auth/password"), ("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") @pytest.fixture(scope="module")
def schema() -> dict[str, Any]: 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 == [] 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 = [ sans_403 = [
(methode, chemin) (methode, chemin)
for methode, chemin, operation in operations(schema) 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 == [] assert sans_403 == []
+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1, "version": 1,
"cli": { "cli": {
"packageManager": "npm", "packageManager": "npm"
"analytics": false
}, },
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
+3 -4
View File
@@ -35,10 +35,9 @@ flowchart TB
| `backend` | Construite depuis `apps/backend` | `depends_on: db, condition: service_healthy`. **N'embarque pas le source** : toute modification impose `docker compose up -d --build backend` | | `backend` | Construite depuis `apps/backend` | `depends_on: db, condition: service_healthy`. **N'embarque pas le source** : toute modification impose `docker compose up -d --build backend` |
**La boucle de développement n'utilise pas le service `backend`.** `make db-up` puis `make dev` : **La boucle de développement n'utilise pas le service `backend`.** `make db-up` puis `make dev` :
seule la base tourne en conteneur, l'API et `ng serve` tournent sur le poste avec le rechargement seule la base tourne en conteneur, l'API tourne sur le poste avec le rechargement à chaud. Le
à chaud, lancés ensemble par `make dev` (`make dev-backend`/`make dev-frontend` pour lancer l'un service `backend` sert la stack complète et la recette. Les deux occupent le port 8000, ils ne se
des deux seul). Le service `backend` sert la stack complète et la recette. Les deux occupent le lancent donc pas ensemble.
port 8000, ils ne se lancent donc pas ensemble.
Deux pièges sont documentés en tête du `docker-compose.yml`, ils ne se devinent pas : Deux pièges sont documentés en tête du `docker-compose.yml`, ils ne se devinent pas :
+14
View File
@@ -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 `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. `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é ## Sécurité
Voir la vue consolidée dans [00-vue-ensemble.md](00-vue-ensemble.md) et les décisions dans les Voir la vue consolidée dans [00-vue-ensemble.md](00-vue-ensemble.md) et les décisions dans les
+2 -3
View File
@@ -108,9 +108,8 @@ déploiement, en même temps que sera tranchée la question de l'ingress dans
le message d'erreur arrive avant toute compilation. Un poste en 22.21 ou en 24.12 ne peut donc ni le message d'erreur arrive avant toute compilation. Un poste en 22.21 ou en 24.12 ne peut donc ni
tester ni construire le frontend. tester ni construire le frontend.
Le frontend a ses cibles dans le `Makefile` racine (`install-frontend`, `dev-frontend`, Le frontend **n'a pas de cible dans le `Makefile` racine** et **aucun service dans
englobées par `install` et `dev`), mais **aucun service dans `docker-compose.yml`** : en `docker-compose.yml`** : il se pilote uniquement par `npm`, depuis `apps/frontend`. Le port 4200
développement il tourne toujours directement via `npm`, depuis `apps/frontend`. Le port 4200
n'apparaît dans le compose que comme valeur par défaut d'`APP_CORS_ORIGINS`, côté backend. n'apparaît dans le compose que comme valeur par défaut d'`APP_CORS_ORIGINS`, côté backend.
Un `Dockerfile` frontend existe sur la branche `feat/pipeline-cd`, mais il est mono-étage et sans Un `Dockerfile` frontend existe sur la branche `feat/pipeline-cd`, mais il est mono-étage et sans