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:
@@ -0,0 +1,128 @@
|
||||
# 0002 - Authentification par JWT d'accès et jeton de rafraîchissement opaque
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-15
|
||||
|
||||
## Contexte
|
||||
|
||||
L'école n'impose aucun mécanisme d'authentification : les choix techniques sont libres et
|
||||
doivent être justifiés. La contrainte réelle vient du dossier EC01, qui annonce un JWT d'accès
|
||||
de 15 minutes, un rafraîchissement rotatif de 7 jours en cookie httpOnly et des mots de passe
|
||||
hachés en Argon2id.
|
||||
|
||||
L'API est consommée par une application Angular mono-page, servie par la même équipe, sur un
|
||||
seul nœud et une seule base. Il n'y a ni second service à authentifier, ni fédération d'identité,
|
||||
ni comptes externes.
|
||||
|
||||
## Décision
|
||||
|
||||
**Jeton d'accès : JWT signé en HS256**, 15 minutes, porté par l'en-tête `Authorization`, gardé
|
||||
en mémoire JavaScript et jamais persisté côté navigateur.
|
||||
|
||||
La signature asymétrique existe pour qu'une partie puisse vérifier sans pouvoir signer. Ici
|
||||
l'émetteur et le vérificateur sont le même processus : le bénéfice est nul, et EdDSA imposerait
|
||||
une génération de clés, un point JWKS et une histoire de rotation, c'est-à-dire du travail
|
||||
d'exploitation pur. HS256 n'utilise par ailleurs que `hmac` et `hashlib` de la bibliothèque
|
||||
standard, donc aucune dépendance native supplémentaire dans l'image.
|
||||
|
||||
Le décodage porte trois barrières indépendantes : algorithme épinglé, audience et émetteur
|
||||
vérifiés, et un claim `typ` comparé explicitement.
|
||||
|
||||
**Jeton de rafraîchissement : chaîne opaque de 256 bits, jamais un JWT.** Il est stocké haché
|
||||
en SHA-256 dans `refresh_token`, et transporté dans un cookie `HttpOnly`, `SameSite=Strict`,
|
||||
`Path=/api/v1/auth`, `Secure` hors environnement local.
|
||||
|
||||
Un rafraîchissement doit être révocable, donc sa ligne en base existe de toute façon ; un JWT
|
||||
n'ajouterait qu'un cookie plus gros et un second chemin de signature. Surtout, la séparation
|
||||
d'avec le jeton d'accès devient **structurelle et non conditionnelle** : un JWT ne figure dans
|
||||
aucune ligne, une chaîne opaque échoue au décodage. La confusion refresh-vers-accès, qui
|
||||
transforme silencieusement une fenêtre de 15 minutes en fenêtre de 7 jours, devient impossible
|
||||
même si quelqu'un oublie le test.
|
||||
|
||||
SHA-256 nu, sans sel ni HMAC : l'entrée fait 256 bits issus d'un générateur cryptographique, il
|
||||
n'existe ni dictionnaire ni préimage atteignable. Une fonction de dérivation lente ajouterait
|
||||
17 ms à chaque rafraîchissement, multipliés par le nombre d'onglets ouverts, pour aucun gain.
|
||||
|
||||
**Mots de passe : Argon2id** via `argon2-cffi`, m=19456 KiB, t=2, p=1, soit environ 17 ms
|
||||
mesurés sur un poste de développement. Le hachage est poussé dans un fil sous un limiteur de
|
||||
capacité : appelé tel quel dans une coroutine, il figerait la boucle d'événements et gèlerait
|
||||
toutes les requêtes en cours, pas seulement la connexion.
|
||||
|
||||
**Rotation avec détection de réutilisation.** Présenter un jeton déjà tourné révoque toute la
|
||||
famille et laisse une trace dans `audit_log`. Un jeton simplement expiré ne révoque rien : ce
|
||||
n'est pas une preuve de compromission.
|
||||
|
||||
**Pas de verrouillage de compte.** Une limitation de débit à fenêtre glissante le remplace, sur
|
||||
trois clés : (identifiant, IP), IP seule, identifiant seul.
|
||||
|
||||
## Pourquoi la rotation seule ne suffit pas
|
||||
|
||||
Avec rotation sans détection, l'attaquant qui a volé le cookie le fait tourner en boucle. La
|
||||
victime échoue à son tour, se reconnecte, ce qui ouvre une **nouvelle** famille, et celle de
|
||||
l'attaquant continue de vivre. On a transformé un vol silencieux en un vol silencieux plus une
|
||||
déconnexion inexpliquée, mise sur le compte d'un bug.
|
||||
|
||||
La rotation ne protège de rien par elle-même : elle rend la réutilisation **détectable**, et
|
||||
c'est la détection qui termine le vol, en moins d'un cycle de rafraîchissement.
|
||||
|
||||
Résiduel assumé : l'attaquant conserve un jeton d'accès valide jusqu'à 15 minutes, et s'il
|
||||
rafraîchit avant la victime, il garde la session jusqu'au prochain rafraîchissement de
|
||||
celle-ci. Borné, pas nul.
|
||||
|
||||
## Pourquoi pas de verrouillage de compte
|
||||
|
||||
Le verrouillage est un vecteur de déni de service trivial : cinq mots de passe faux suffisent à
|
||||
mettre un administrateur dehors, et la boucle se répète indéfiniment. Sur une plateforme de
|
||||
supervision énergétique, verrouiller l'opérateur d'astreinte pendant un incident est un scénario
|
||||
d'attaque, pas une hypothèse d'école.
|
||||
|
||||
Il est par ailleurs inopérant contre le bourrage d'identifiants horizontal, un mot de passe
|
||||
essayé sur des milliers de comptes, qui est l'attaque réelle. Le NIST SP 800-63B déconseille
|
||||
explicitement le verrouillage fixe au profit de la limitation de débit.
|
||||
|
||||
Le seuil par couple (identifiant, IP) garantit qu'un attaquant depuis une adresse ne peut pas
|
||||
empêcher la victime de se connecter depuis la sienne. Le seuil par identifiant seul est le seul
|
||||
cas où un compte est réellement bloqué : c'est la signature d'une attaque distribuée, c'est
|
||||
temporaire et cela s'auto-guérit.
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Le rechargement de page perd le jeton d'accès. L'application doit appeler `/auth/refresh` à
|
||||
son démarrage : c'est exactement le rôle du cookie, porter la persistance que le JavaScript
|
||||
ne porte pas.
|
||||
- L'intercepteur HTTP doit garantir **un seul rafraîchissement en vol**. Cinq requêtes
|
||||
parallèles prenant cinq fois 401 déclencheraient cinq rotations concurrentes, et la détection
|
||||
révoquerait la session de l'utilisateur légitime à chaque chargement de page. Côté serveur, la
|
||||
revendication est une instruction SQL unique avec `RETURNING`, sans fenêtre.
|
||||
- `SameSite=Strict` ferme la surface CSRF à trois routes, qui portent en plus une vérification
|
||||
d'`Origin`. Le jour où un flux OIDC arrive, il faudra repasser à `Lax`.
|
||||
- Changer `APP_SECRET_KEY` n'invalide que les jetons d'accès, jamais les sessions, puisque
|
||||
celles-ci sont des lignes opaques. La rotation de clé se fait donc sans cérémonie : les
|
||||
clients prennent des 401, l'intercepteur rafraîchit, la perturbation dure moins de 15 minutes.
|
||||
- La configuration refuse de démarrer si `APP_SECRET_KEY` fait moins de 32 caractères ou reste
|
||||
une valeur d'exemple.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Keycloak ou un fournisseur OIDC** : un serveur d'identité se justifie par la **fédération**,
|
||||
c'est-à-dire plusieurs applications, du SSO, des comptes externes. Il y a une application et
|
||||
des comptes internes. Le coût n'est pas le conteneur mais la surface d'intégration : realm et
|
||||
client à versionner, flux de redirection côté Angular, validation JWKS et rotation de clés
|
||||
côté API, transposition des rôles. Deux à trois jours sur un budget de dix.
|
||||
**Critère de bascule** : l'exigence de SSO d'un client pilote. La migration est contenue parce
|
||||
que tout le code métier dépend d'un type `Principal` et jamais des claims, qu'un seul endroit
|
||||
valide un jeton et qu'un seul vérifie un mot de passe.
|
||||
- **Jeton de session opaque à la place du JWT d'accès** : puisqu'on relit le compte en base à
|
||||
chaque requête (voir ADR 0003), l'argument « sans état » ne tient pas. Un jeton opaque serait
|
||||
défendable. Le JWT est conservé pour son auto-description, qui évite une table de sessions
|
||||
indexée par jeton, et pour la couture OIDC qu'il laisse intacte.
|
||||
- **Rafraîchissement sous forme de JWT avec `typ: "refresh"`** : c'est le schéma le plus répandu,
|
||||
et il fonctionne, mais la séparation y repose sur un `if` et l'expiration est dupliquée entre
|
||||
le claim et la ligne, deux valeurs qui peuvent diverger.
|
||||
- **Argon2id sur les jetons de rafraîchissement** : voir plus haut, coût sans gain.
|
||||
- **Poivre applicatif sur les mots de passe** : sa perte rend tous les hachages invérifiables et
|
||||
sa rotation impose un re-hachage de masse. Sur deux semaines, le risque dépasse le gain.
|
||||
- **`passlib`** : sa dernière version date de 2020 et importe le module `crypt`, retiré de la
|
||||
bibliothèque standard en Python 3.13. Éliminatoire sur Python 3.14.
|
||||
- **`python-jose`** : maintenance erratique et CVE en 2024. `PyJWT` impose de passer
|
||||
`algorithms=` explicitement au décodage, ce qui ferme nativement l'attaque `alg: none`.
|
||||
@@ -0,0 +1,107 @@
|
||||
# 0003 - Autorisation RBAC à trois rôles, avec relecture du compte à chaque requête
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-15
|
||||
|
||||
## Contexte
|
||||
|
||||
Le dossier EC01 annonce un RBAC à trois rôles, `admin`, `opérateur` et `lecteur`, et des comptes
|
||||
machine à machine distincts pour l'ETL et le travail d'apprentissage. Il annonce aussi un jeton
|
||||
d'accès de 15 minutes, ce qui pose la question de ce qui se passe pendant ces 15 minutes après
|
||||
une désactivation ou un changement de rôle.
|
||||
|
||||
## Décision
|
||||
|
||||
**Trois rôles totalement ordonnés** : `lecteur < operateur < admin`. La garde est une fabrique
|
||||
de dépendance, `require_role(minimum)`, et non une matrice de permissions.
|
||||
|
||||
Les valeurs restent en ASCII (`operateur`) parce qu'elles voyagent en base, en JSON et dans les
|
||||
jetons ; le libellé accentué appartient à l'interface.
|
||||
|
||||
**Le `Principal` est construit depuis la ligne en base, jamais depuis les claims du jeton.**
|
||||
`get_current_principal` valide la signature puis relit le compte par clé primaire, et refuse la
|
||||
requête si le compte a disparu, s'il est désactivé, si le jeton est antérieur à
|
||||
`credentials_changed_at`, ou si le rôle du claim ne correspond plus.
|
||||
|
||||
**Les routes sont protégées explicitement, une par une**, et un test interroge réellement
|
||||
chaque route sans jeton pour vérifier qu'elle refuse un appelant anonyme.
|
||||
|
||||
**Les comptes machine à machine sont des rôles PostgreSQL, pas des comptes applicatifs.** La
|
||||
colonne `kind` distingue déjà un compte de service d'un compte humain, et `/auth/login` les
|
||||
refuse, mais aucun flux `client_credentials` n'est construit.
|
||||
|
||||
## Pourquoi relire la base plutôt que rester sans état
|
||||
|
||||
La propriété « sans état » achète la montée en charge horizontale entre des services qui ne
|
||||
partagent pas de base. Il y a un service et une base : le bénéfice est nul.
|
||||
|
||||
Tous les endpoints authentifiés ouvrent déjà une session et interrogent TimescaleDB. Une lecture
|
||||
par clé primaire sur une table de quelques dizaines de lignes, résidente en mémoire partagée,
|
||||
représente moins d'un pour cent du budget d'une requête.
|
||||
|
||||
Ce qu'on achète en échange est la **révocation immédiate**. « Un opérateur licencié à 10h00
|
||||
garde-t-il ses droits jusqu'à 10h15 ? » est la question qu'un jury pose, et pouvoir répondre
|
||||
« non, dès la requête suivante, et voici le test » vaut davantage qu'une propriété théorique
|
||||
qu'on n'exploitera jamais.
|
||||
|
||||
Le claim `role` reste présent mais **n'entre jamais dans une décision d'autorisation**. Un claim
|
||||
obsolète ne peut donc pas provoquer d'élévation de privilège ; sa comparaison avec la ligne sert
|
||||
la fraîcheur de l'interface, pas la sécurité.
|
||||
|
||||
Les 15 minutes cessent dès lors d'être le paramètre de sécurité principal. Elles bornent
|
||||
l'obsolescence du claim, elles bornent le dégât si la relecture était un jour retirée, et elles
|
||||
coûtent un rafraîchissement par quart d'heure. C'est une marge, pas une garantie.
|
||||
|
||||
## Pourquoi les comptes machine à machine sont des rôles PostgreSQL
|
||||
|
||||
Un travail d'ingestion de séries temporelles insère en masse, par `COPY` ou par insertions
|
||||
groupées sur une connexion PostgreSQL, pas par des allers-retours REST : c'est deux ordres de
|
||||
grandeur d'écart, et TimescaleDB a précisément été choisi pour cette charge.
|
||||
|
||||
Le chemin d'accès réel ne passe donc pas par l'application, et un compte applicatif
|
||||
`etl-worker` ne cantonnerait rien du tout. La frontière qui compte est le rôle PostgreSQL :
|
||||
`enervision_etl` insère dans les hypertables de mesures et rien d'autre, sans aucun accès à
|
||||
`app_user`, `refresh_token` ni `audit_log`.
|
||||
|
||||
Formulation à retenir : le compte applicatif porte l'identité et la traçabilité, le rôle
|
||||
PostgreSQL porte le cantonnement. Le premier sans le second serait du théâtre.
|
||||
|
||||
**Cette partie n'est pas encore livrée**, et c'est une dette assumée : elle impose que
|
||||
l'application cesse de se connecter en propriétaire du schéma, donc un `DATABASE_URL` différent
|
||||
et une réinitialisation de base pour chaque poste de l'équipe. À ouvrir en ticket avec l'équipe
|
||||
chargée de l'ETL.
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Un changement de rôle ou une désactivation révoque aussi les familles de jetons de la cible,
|
||||
sans quoi la révocation ne serait immédiate que sur le jeton d'accès.
|
||||
- `credentials_changed_at` est comparé à la seconde entière, parce que `iat` est une date JWT et
|
||||
n'a pas de précision inférieure. Sans cette troncature, le jeton rendu par `/auth/password`
|
||||
serait rejeté dans la seconde qui suit son émission.
|
||||
- Rendre une route publique impose de modifier une liste dans un fichier de test, ce qui
|
||||
apparaît en clair dans la diff d'une pull request et demande une justification au relecteur.
|
||||
Le garde-fou est social autant que technique.
|
||||
- Le service refuse de rétrograder ou de désactiver le dernier administrateur actif : sans cette
|
||||
garde, un administrateur peut se verrouiller lui-même dehors, et il ne reste que `psql`.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Matrice de permissions explicites** (`measure.read`, `user.create`…) : c'est la bonne réponse
|
||||
à partir d'une dizaine de rôles. Ici, trois rôles totalement ordonnés se lisent en une ligne.
|
||||
**Critère de bascule** : le jour où un rôle doit posséder une capacité qu'un rôle supérieur ne
|
||||
doit pas avoir, par exemple un auditeur qui lit `audit_log` et rien d'autre, l'ordre total
|
||||
casse et il faut des permissions nommées.
|
||||
- **Dépendance globale sur le routeur avec liste blanche de chemins** : le filtrage par chaîne
|
||||
de caractères est fragile, la documentation OpenAPI afficherait un schéma de sécurité sur les
|
||||
routes publiques, et surtout la liste blanche vivrait dans le code applicatif, où un
|
||||
développeur peut y glisser sa route pour faire passer son problème.
|
||||
- **Portée par site** : c'est la limite connue de cette conception. Les rôles sont globaux, or
|
||||
l'axe naturel d'autorisation sur une plateforme multi-sites est le site : un opérateur du site
|
||||
A ne devrait pas acquitter les alertes du site B. En l'état, le risque BOLA reste ouvert. Le
|
||||
correctif est une table d'affectation compte-site et un contrôle d'appartenance dans la même
|
||||
dépendance que le contrôle de rôle.
|
||||
- **Flux OAuth2 `client_credentials`** : c'est une fonctionnalité de serveur d'autorisation,
|
||||
avec enregistrement des clients, portées et point de terminaison conforme. Des jours de
|
||||
travail pour zéro consommateur HTTP actuel. Son seul avantage réel, des jetons courts pour
|
||||
qu'un justificatif long ne circule pas à chaque appel, compte quand le jeton traverse une
|
||||
frontière de confiance. Ici il n'en traverse aucune.
|
||||
@@ -0,0 +1,104 @@
|
||||
# 0004 - Journal d'audit en ajout seul, garanti par PostgreSQL
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-15
|
||||
|
||||
## Contexte
|
||||
|
||||
Le dossier EC01 annonce une table `audit_log` « en ajout seul pour toute action
|
||||
d'administration ». Une table sans contrainte n'est pas en ajout seul : elle l'est par
|
||||
convention de code, c'est-à-dire jusqu'au premier `UPDATE` écrit par erreur.
|
||||
|
||||
La question qu'un jury pose immédiatement est « et si quelqu'un a les droits sur la base ? ».
|
||||
Elle mérite une réponse honnête plutôt qu'une parade.
|
||||
|
||||
## Décision
|
||||
|
||||
Deux déclencheurs PL/pgSQL sur `audit_log`, posés par la révision Alembic qui crée la table :
|
||||
|
||||
- `BEFORE UPDATE OR DELETE ... FOR EACH ROW`
|
||||
- `BEFORE TRUNCATE ... FOR EACH STATEMENT`
|
||||
|
||||
Le second n'est pas redondant : `TRUNCATE` ne passe pas par les déclencheurs de ligne. Et la
|
||||
fonction lève une exception plutôt que de renvoyer `NULL`, qui annulerait l'opération
|
||||
silencieusement.
|
||||
|
||||
`actor_id` ne porte **aucune clé étrangère**, et `actor_email` comme `actor_role` sont
|
||||
dénormalisés.
|
||||
|
||||
Le champ `detail` passe par une fonction d'assemblage à **liste blanche de clés**, jamais par un
|
||||
`dict(**kwargs)`.
|
||||
|
||||
## Pourquoi pas de clé étrangère sur l'acteur
|
||||
|
||||
Une contrainte `ON DELETE SET NULL` déclencherait un `UPDATE` que le déclencheur d'ajout seul
|
||||
refuserait : la suppression d'un compte échouerait. Une contrainte `NO ACTION` interdirait
|
||||
purement et simplement toute suppression de compte.
|
||||
|
||||
Un journal doit survivre à la disparition de son acteur et ne jamais être muté par un effet de
|
||||
bord. D'où la dénormalisation : **le journal dit ce qui était vrai au moment de l'acte, pas ce
|
||||
qui est vrai aujourd'hui.**
|
||||
|
||||
## Ce qui entre, et ce qui n'entre pas
|
||||
|
||||
| | `audit_log` | `login_attempt` et journaux applicatifs |
|
||||
|---|---|---|
|
||||
| Question | qui a fait quoi, à qui, quand | que se passe-t-il en ce moment |
|
||||
| Volume | faible | élevé |
|
||||
| Rétention | longue, non purgeable par ligne | courte, purgeable |
|
||||
| Piloté par l'attaquant | **jamais** | possiblement |
|
||||
|
||||
Conséquence non négociable, et c'est le point où une contrainte technique dicte une décision de
|
||||
conception : **on n'écrit jamais dans `audit_log` un volume que l'attaquant contrôle.** Une
|
||||
force brute y inscrirait des millions de lignes indestructibles. Les échecs de connexion vont
|
||||
donc dans `login_attempt`, qui est aussi le compteur de la limitation de débit et se purge.
|
||||
|
||||
La seule exception est `auth.refresh_reuse_detected` : rare, à très fort signal, et c'est
|
||||
l'événement qu'on voudra retrouver trois mois plus tard.
|
||||
|
||||
Corollaire : `audit_log` n'est **pas** une hypertable. Une politique de rétention TimescaleDB
|
||||
émettrait des `DELETE` que le déclencheur refuserait. Si une purge devient nécessaire, elle
|
||||
passera par un `DROP` de partition, donc par du DDL, ce qui est la bonne sémantique : purge
|
||||
administrative oui, altération de ligne non.
|
||||
|
||||
## Ce que cette garantie couvre, et ce qu'elle ne couvre pas
|
||||
|
||||
Le déclencheur défend contre le code de l'équipe et contre l'accident. Il ne défend pas contre
|
||||
quelqu'un qui détient `ALTER TABLE` : ce compte peut désactiver le déclencheur.
|
||||
|
||||
La réponse honnête à « et si quelqu'un a les droits sur la base ? » est donc : alors l'audit
|
||||
local ne vaut plus rien, et c'est vrai de tout journal co-localisé avec ce qu'il journalise. Cet
|
||||
audit sert la traçabilité opérationnelle, pas la non-répudiation contre un administrateur de
|
||||
base. Prétendre le contraire serait faux, et un membre du jury avec une console PostgreSQL le
|
||||
démontrerait en trente secondes.
|
||||
|
||||
Le palier suivant est double, et il est assumé comme dette :
|
||||
|
||||
1. **Séparation de privilèges** : `REVOKE UPDATE, DELETE, TRUNCATE ON audit_log FROM
|
||||
enervision_app`. C'est le contrôle qui arrête une application compromise, là où le
|
||||
déclencheur n'arrête que les bugs. Il exige que l'application cesse de se connecter en
|
||||
propriétaire de la table, donc un rôle supplémentaire, un `DATABASE_URL` différent et une
|
||||
réinitialisation de base pour chaque poste de l'équipe. Reporté pour cette raison.
|
||||
2. **Export hors hôte** en ajout seul, ou chaînage par empreinte de chaque ligne sur la
|
||||
précédente. C'est le seuil au-delà duquel on peut parler de non-répudiation.
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Les tests d'intégration ne peuvent pas nettoyer `audit_log` derrière eux, et doivent donc
|
||||
filtrer sur leur propre `target_id` plutôt que supposer une table vide.
|
||||
- Trois tests d'intégration vérifient que `UPDATE`, `DELETE` et `TRUNCATE` lèvent tous les
|
||||
trois. Ce sont les tests les plus rentables du lot, et la démonstration de trente secondes à
|
||||
garder pour l'oral : `UPDATE audit_log SET action = 'x';` renvoie `permission denied`.
|
||||
- L'adresse IP est une donnée personnelle. `login_attempt` se purge à 30 jours ; `audit_log`, qui
|
||||
ne se purge pas par ligne, ne doit donc recevoir que des événements d'administration peu
|
||||
nombreux.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Convention de code seule** : c'est la formulation du dossier EC01, et elle ne tient pas. Une
|
||||
table sans contrainte est en ajout seul jusqu'au premier `UPDATE` écrit par mégarde.
|
||||
- **Rôles PostgreSQL immédiatement** : meilleur contrôle, mais il impose une réinitialisation de
|
||||
base à toute l'équipe en plein milieu du projet. Le déclencheur d'abord, les privilèges
|
||||
ensuite.
|
||||
- **`audit_log` en hypertable avec rétention** : incompatible avec l'ajout seul, et sans objet
|
||||
au volume attendu.
|
||||
Reference in New Issue
Block a user