PD-16 — Retour d'expérience¶
📚 Navigation User Story
| Document | | | ---------- | -- | | 📋 [Spécification](PD-16-specification.md) | | | 🛠️ [Plan d'implémentation](PD-16-plan.md) | | | ✅ [Critères d'acceptation](PD-16-acceptability.md) | | | 📝 **Retour d'expérience** | *(ce document)* | [← Retour à backend-core](../PD-186-epic.md) · [↑ Index User Story](index.md)1. Résumé exécutif¶
La User Story PD-16 visait à créer le schéma vault_secure.documents avec RLS strict, cycle de vie probatoire (PENDING → SEALED → EXPIRED), et contraintes d'intégrité (file_hash 32 bytes, ovh_path validé). L'implémentation fournit une migration complète avec policies RLS, trigger updated_at, index GIN pour recherche déterministe et ENUM document_status. Cependant, 2 écarts MAJEURS sont identifiés : suppression EXPIRED finalement interdite (policy DELETE réécrite sans cas EXPIRED) et jobs lifecycle sans contexte RLS/rôle technique (transitions impossibles). Verdict : ACCEPTÉ AVEC RÉSERVES.
2. Points fluides¶
- Schéma vault_secure : séparation logique stricte respectée, cohérent avec PD-15
- ENUM document_status : création conditionnelle
DO $$ ... END $$évitant les erreurs idempotentes - Table complète : id UUID, user_id FK, encrypted_metadata BYTEA, file_hash CHECK(32), ovh_path, status, timestamps
- Contraintes d'intégrité : file_hash longueur 32 bytes, ovh_path regex en base
- Index performants : BTREE sur user_id, status, retention_until + GIN sur keyword_deterministic
- Index unique :
(user_id, file_hash)empêche les doublons par utilisateur - RLS activé : policies SELECT/INSERT/UPDATE/DELETE avec isolation par
app.current_user_id - Trigger updated_at : réutilisation de
vault_secure.update_timestamp()de PD-15 - Recherche déterministe : index GIN sur
keyword_deterministic[]opérationnel - DTO validation : encrypted_metadata, file_hash obligatoires avec class-validator
- FK CASCADE : suppression utilisateur cascade sur ses documents
3. Points difficiles¶
| Difficulté | Contexte |
|---|---|
| Policy DELETE EXPIRED | Migration de normalisation (1733500700000) réécrit DELETE en supprimant le cas EXPIRED introduit précédemment |
| Jobs sans contexte RLS | document-sealing.service.ts utilise createQueryBuilder sans SET LOCAL app.current_user_id |
| Rôle technique absent | Aucun rôle BYPASSRLS ou policy conditionnelle pour les transitions de statut |
| Validation ovh_path DTO | Seule contrainte DB, pas de validation applicative (erreur tardive) |
| Transitions statut | UPDATE status interdit pour user (même PENDING), seul rôle technique peut changer |
4. Hypothèses révélées tardivement¶
| Hypothèse initiale | Réalité découverte |
|---|---|
| DELETE EXPIRED autorisé pour user | FAUX — Migration normalisation supprime ce cas, seul admin peut supprimer |
| Jobs lifecycle fonctionnent avec RLS | FAUX — Sans SET LOCAL, les policies SELECT retournent 0 lignes candidates |
| ovh_path validé à l'API | FAUX — DTO n'a que @IsString @IsNotEmpty, regex uniquement en DB |
| User peut UPDATE status PENDING→SEALED | FAUX — Spec indique rôle technique uniquement pour transitions |
| RlsSubscriber couvre les QueryBuilder | FAUX — Subscriber intercepte les repository ops, pas les raw queries |
5. Invariants complexes à implémenter¶
| Invariant | Complexité |
|---|---|
| Cycle de vie irréversible | PENDING → SEALED → EXPIRED sans retour arrière |
| RLS strict par utilisateur | user_id = current_setting('app.current_user_id')::UUID sur toutes les opérations |
| Transitions par rôle technique | Nécessite rôle BYPASSRLS ou policy conditionnelle avec app.is_admin |
| file_hash 32 bytes | CHECK constraint + validation DTO pour rejet précoce |
| ovh_path format validé | Regex {user_id}/{document_id}/{version}.enc en DB et DTO |
| SEALED immutable | UPDATE/DELETE interdits pour user, seul admin/worker peut agir |
| Journalisation suppression | Suppression EXPIRED doit être loggée append-only (hors scope mais référencée) |
6. Dette technique¶
| Dette | Impact | Priorité |
|---|---|---|
| Jobs lifecycle sans RLS | Transitions PENDING→SEALED→EXPIRED impossibles | HAUTE |
| DELETE EXPIRED interdit | AC6 non respecté, user ne peut pas supprimer après rétention | HAUTE |
| Validation ovh_path DTO | Erreur tardive (DB) au lieu de rejet API précoce | MOYENNE |
| Rôle technique non créé | Worker sealing/purge bloqué par policies | HAUTE |
| Tests E2E transitions | Couverture TA-4 incomplète sans contexte RLS | MOYENNE |
| Documentation incomplète | /docs/db/vault_secure/documents.md non créé | BASSE |
7. Risques résiduels¶
| Risque | Probabilité | Impact | Mitigation suggérée |
|---|---|---|---|
| Documents jamais SEALED | Élevée | ÉLEVÉ | Créer rôle BYPASSRLS pour worker ou policy conditionnelle |
| Purge EXPIRED impossible | Élevée | ÉLEVÉ | Ajouter cas EXPIRED dans policy DELETE ou rôle admin |
| ovh_path invalide en prod | Moyenne | MOYEN | Ajouter @Matches regex dans DTO |
| QueryBuilder bypass RLS | Élevée | ÉLEVÉ | Wrapper setRlsContext avant chaque batch query |
| Accumulation documents PENDING | Élevée | MOYEN | Sans sealing fonctionnel, fenêtre acquisition illimitée |
8. Améliorations processus¶
| Amélioration | Bénéfice attendu |
|---|---|
| Tester jobs avec RLS réel | Détecter incompatibilité avant merge |
| Prévoir rôle technique dans spec | Clarifier qui peut effectuer les transitions |
| Validation DTO = DB | Éviter divergence entre validation API et contraintes DB |
| Créer docs/ avec la migration | Ne pas séparer code et documentation |
| Tests E2E cycle de vie | Valider PENDING→SEALED→EXPIRED avec vrais rôles |
| Revue migrations successives | Éviter régressions (normalisation supprimant des cas) |
9. Enseignements clés¶
-
Les migrations successives peuvent créer des régressions — La migration 1733500700000 de normalisation a supprimé le cas EXPIRED de la policy DELETE introduit juste avant. Une revue des migrations liées est essentielle.
-
Jobs batch nécessitent un contexte RLS explicite — Les services utilisant
createQueryBuilderouRepositorysansSET LOCALdans la même connexion verront leurs queries filtrées à 0 résultats par les policies SELECT. -
Rôle technique = architecture à prévoir — Le cycle de vie PENDING→SEALED→EXPIRED suppose un worker qui peut UPDATE le status, mais les policies interdisent tout UPDATE pour un user normal. Sans rôle BYPASSRLS ou policy conditionnelle, les transitions sont impossibles.
-
Validation DTO doit refléter les contraintes DB — Si ovh_path a une regex en DB, elle doit aussi être dans le DTO pour un rejet précoce et des messages d'erreur clairs.
-
TypeORM QueryBuilder échappe au Subscriber — Le RlsSubscriber intercepte les opérations
repository.save()/repository.remove(), mais pas lescreateQueryBuilder().getMany(). Pour les batch jobs, le contexte RLS doit être injecté manuellement avant la query.