docs: acte les décisions d'authentification et met à jour les vues

Trois ADR : le jeton d'accès et le rafraîchissement opaque, le RBAC avec
relecture du compte à chaque requête, et le journal d'audit en ajout
seul. Chacun porte ses alternatives écartées et son critère de bascule,
notamment celui vers OIDC.

`31-contrat-authentification.md` est destiné au frontend : endpoints,
codes d'erreur à traiter, et les quatre règles qui comptent. La
troisième, un seul rafraîchissement en vol, est une exigence et non une
optimisation : cinq rotations concurrentes seraient lues comme un rejeu
et révoqueraient la session à chaque chargement de page.

`owasp-traceabilite.md` remplace la revendication « couverture OWASP Top
10 et API Top 10 » de la NFR4, qui n'a pas de réponse honnête sur vingt
items en deux semaines. Un contrôle par ligne, l'item adressé, et une
section qui dit ce qui reste ouvert : portée par site, bornage des
lectures de séries, transport, et la consommation de l'API Mock.

Les vues 00, 20 et 40 suivent, comme l'impose leur propre règle de
maintenance. La question ouverte « quel mécanisme d'authentification »
est fermée ; trois autres la remplacent, dont la portée par site.
This commit is contained in:
Johan LEROY
2026-09-15 15:05:28 +02:00
parent c60081a5ac
commit 3b7383697e
13 changed files with 883 additions and 62 deletions
+71 -3
View File
@@ -35,8 +35,8 @@ Statut : `Fait`.
- `db/init/100-extensions.sql` crée l'extension `timescaledb`.
- `db/init/110-test-database.sql` crée `enervision_test`, dont le nom est attendu en dur par
`apps/backend/tests/conftest.py`.
- Une révision Alembic, `5353c0e4f094`, qui **ne crée aucune table**. Elle établit
`alembic_version` et refuse de s'appliquer si l'extension manque :
- Quatre révisions Alembic. La première, `5353c0e4f094`, **ne crée aucune table** : elle
établit `alembic_version` et refuse de s'appliquer si l'extension manque :
```sql
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') THEN
@@ -47,6 +47,9 @@ END IF;
Cette garde forme paire avec le 503 de `/api/v1/health/ready`. Un bootstrap sauté ne se voit pas
au démarrage de l'API : ces deux gardes le rendent visible tôt, des deux côtés.
Les trois suivantes créent les tables de l'authentification, décrites plus bas : `app_user`,
puis `login_attempt` et `audit_log`, puis `refresh_token`.
## Cycle de vie d'une mesure
Statut : `Cible`. Aucun de ces maillons n'existe.
@@ -65,7 +68,72 @@ flowchart LR
Les lectures de l'API et de Grafana visent l'agrégat continu, pas la table brute : c'est tout
l'intérêt de TimescaleDB, et cela doit rester vrai quand les volumes augmenteront.
## Modèle
## Tables d'authentification
Statut : `Fait`. Elles ne sont pas des séries temporelles et n'ont donc rien à voir avec les
hypertables ; elles vivent dans `apps/backend/alembic/`, qui porte le schéma exposé par l'API.
```mermaid
erDiagram
APP_USER ||--o{ REFRESH_TOKEN : ouvre
APP_USER {
uuid id PK
string email UK
text password_hash
text role
text kind
bool is_active
bool must_change_password
timestamptz credentials_changed_at
}
REFRESH_TOKEN {
uuid id PK
uuid family_id
uuid user_id FK
bytea token_hash UK
timestamptz expires_at
timestamptz rotated_at
timestamptz revoked_at
text revoked_reason
uuid replaced_by
}
LOGIN_ATTEMPT {
bigint id PK
timestamptz occurred_at
string email_tried
inet client_ip
text outcome
}
AUDIT_LOG {
bigint id PK
timestamptz occurred_at
uuid actor_id
text actor_email
text action
jsonb detail
}
```
Quatre choix de modélisation portent une intention et se défendent seuls :
- **`app_user` et non `user`** : `user` est un mot réservé PostgreSQL, raccourci de
`CURRENT_USER`. Le nom rappelle en prime qu'il s'agit d'un compte applicatif, par opposition
au rôle PostgreSQL qui portera le cantonnement de l'ETL.
- **`credentials_changed_at`, une seule colonne**, couvre le changement de mot de passe, le
changement de rôle et la désactivation. Un compteur de version ne dirait rien à un humain qui
lit un audit.
- **`refresh_token.expires_at` est absolu et hérité** du prédécesseur à chaque rotation. S'il
glissait, la promesse de sept jours serait fictive et une session active ne finirait jamais.
- **`audit_log.actor_id` n'a aucune clé étrangère**, et `actor_email` comme `actor_role` sont
dénormalisés. Une contrainte `ON DELETE SET NULL` déclencherait un `UPDATE` que le déclencheur
d'ajout seul refuserait. Voir l'[ADR 0004](../adr/0004-journal-d-audit-en-ajout-seul.md).
`audit_log` porte deux déclencheurs qui refusent `UPDATE`, `DELETE` et `TRUNCATE`. Elle n'est
donc **pas** une hypertable : une politique de rétention émettrait des `DELETE` qu'ils
refuseraient. `login_attempt`, à l'inverse, est faite pour se purger, puisque son volume est
piloté par l'attaquant.
## Modèle métier
Statut : `Cible`. Les entités ci-dessous sont des **candidates**, à valider en J2. Elles
s'appuient sur les gabarits de [`apps/backend/TESTING.md`](../../apps/backend/TESTING.md), qui