Aller au contenu

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

  1. 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.

  2. Jobs batch nécessitent un contexte RLS explicite — Les services utilisant createQueryBuilder ou Repository sans SET LOCAL dans la même connexion verront leurs queries filtrées à 0 résultats par les policies SELECT.

  3. 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.

  4. 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.

  5. TypeORM QueryBuilder échappe au Subscriber — Le RlsSubscriber intercepte les opérations repository.save() / repository.remove(), mais pas les createQueryBuilder().getMany(). Pour les batch jobs, le contexte RLS doit être injecté manuellement avant la query.