PD-16 — Créer schéma vault_secure.documents¶
📚 Navigation User Story
| Document | | | ---------- | -- | | 📋 **Spécification** | *(ce document)* | | 🛠️ [Plan d'implémentation](PD-16-plan.md) | | | ✅ [Critères d'acceptation](PD-16-acceptability.md) | | | 📝 [Retour d'expérience](PD-16-rex.md) | | [← Retour à backend-core](../PD-186-epic.md) · [↑ Index User Story](index.md)Références¶
- EPIC : PD-186 — BACKEND-CORE
- JIRA : PD-16
- Sources : Architecture TechLead v4.1 (Zero-Knowledge + RLS), Spécifications crypto (AES-256-GCM, K_doc), politique de cycle probatoire (PENDING/SEALED/EXPIRED)
Objectif¶
Créer le schéma PostgreSQL et la table pivot vault_secure.documents servant d'ancrage backend aux documents chiffrés côté client :
- Stockage exclusif de métadonnées chiffrées (
encrypted_metadata) - Isolation stricte par utilisateur via RLS
- Traçabilité probatoire via empreinte SHA3-256 du fichier chiffré (
file_hash) - Référence vers le stockage objet OVH (
ovh_path) - Gestion d'un cycle de vie probatoire :
PENDING→SEALED→EXPIRED - Contraintes / indexes garantissant intégrité, performance et invariants métiers
Contexte¶
Dans ProbatioVault, les documents sont stockés chiffrés côté client (AES-256-GCM, clé documentaire K_doc). Le backend ne doit jamais voir :
- contenu en clair,
- métadonnées en clair,
- informations permettant de déduire les clés.
La table vault_secure.documents est donc le registre minimal côté serveur permettant :
- l'indexation et la récupération (via ID, hash, status, dates),
- la gestion probatoire (statut SEALED et rétention),
- la recherche déterministe minimale (optionnelle) sur mots-clés hachés,
- l'application des règles d'accès via RLS.
Périmètre¶
Inclus¶
- Création du schéma
vault_secure - Création du type ENUM
document_status(PENDING,SEALED,EXPIRED) - Création de la table
vault_secure.documents(DDL, contraintes) - Index (BTREE, GIN, uniques)
- Trigger
updated_at - Activation RLS + policies :
SELECT/INSERTisolés parapp.current_user_idUPDATE/DELETEautorisés uniquement sistatus = PENDINGpour l'utilisateurDELETEadmin (rôleprobatio_admin) autorisé- Spécification des invariants et règles de transitions de statut
- Tests DB (RLS/contraintes/transitions) + tests API (validation input, invariants)
- Documentation :
/docs/db/vault_secure/documents.md
Exclu (hors périmètre)¶
- Upload objet OVH (PD-5)
- CRR Glacier / réplication (PD-6)
- Merkle / ancrage blockchain / preuve composite (PD-60+)
- Chiffrement client-side (PD-97)
Spécification fonctionnelle¶
Données stockées¶
Chaque document doit avoir une entrée en base contenant :
user_id: propriétaire (issu du contexte d'auth, pas du payload client)encrypted_metadata: blob chiffré client-side (obligatoire)keyword_deterministic[]: liste optionnelle de tokens déterministes (hachés côté client)file_hash: hash SHA3-256 du fichier chiffré (32 bytes)ovh_path: chemin objet OVH du fichier chiffré (format validé)status:PENDING/SEALED/EXPIRED- dates :
sealed_at,retention_until,expired_at,created_at,updated_at
Cycle de vie¶
- PENDING : fenêtre d'acquisition (ex. ~60 minutes)
- autorise correction/suppression par le propriétaire
UPDATE/DELETEautorisés (RLS + policy)- SEALED : état probatoire immuable
UPDATE/DELETEinterdits pour l'utilisateur- transitions pilotées par un worker/role technique
- EXPIRED : rétention échue
- document éligible à suppression (manuelle user ou automatique)
- suppression doit être journalisée (append-only, signé HSM) — journalisation traitée ailleurs
Transitions autorisées :
PENDING→SEALED(irréversible)SEALED→EXPIRED(quandretention_until < now())- aucun retour arrière (ex.
SEALED→PENDINGinterdit)
Diagrammes¶
Diagramme d'états — Cycle de vie probatoire¶
Réf. invariants : AC5 (transitions maîtrisées), TA-4 (transitions de cycle), RLS UPDATE/DELETE conditionnel sur status.
stateDiagram-v2
[*] --> PENDING : INSERT (user via API)
PENDING --> SEALED : Rôle technique\n(irréversible, sets sealed_at)
SEALED --> EXPIRED : Worker\n(retention_until < now(),\nsets expired_at)
PENDING --> [*] : DELETE par propriétaire\n(RLS status=PENDING)
EXPIRED --> [*] : Suppression journalisée\n(admin ou automatique,\naudit append-only)
state PENDING {
[*] --> Modifiable
Modifiable : UPDATE/DELETE autorisés\n(propriétaire uniquement, RLS)
}
state SEALED {
[*] --> Immutable
Immutable : UPDATE/DELETE interdits\n(user). Seul rôle technique\npeut transitionner.
}
state EXPIRED {
[*] --> Éligible
Éligible : Éligible à suppression.\nJournalisation append-only\nobligatoire (AC6).
}
note right of PENDING
Fenêtre ~60 min (acquisition)
file_hash 32 bytes (AC5)
ovh_path validé regex
end note
note right of SEALED
Aucun retour arrière
SEALED → PENDING interdit (TA-4)
end note Diagramme de séquence — Insertion document (flux nominal)¶
Réf. invariants : AC2 (métadonnées chiffrées), AC3 (RLS strict), AC5 (file_hash 32 bytes, ovh_path valide).
sequenceDiagram
participant Client
participant API as API Backend
participant RLS as PostgreSQL (RLS)
participant OVH as OVH Object Storage
Client->>Client : Chiffrement AES-256-GCM (K_doc)<br/>Hash SHA3-256 du fichier chiffré
Client->>API : POST /documents<br/>{encrypted_metadata, file_hash, ovh_path, keyword_deterministic[]}
API->>API : Validation input :<br/>- file_hash = 32 bytes (AC5)<br/>- ovh_path conforme regex (AC5)<br/>- encrypted_metadata obligatoire (AC2)<br/>- user_id ignoré du payload,<br/> remplacé par JWT (AC3)
API->>RLS : SET LOCAL app.current_user_id = <jwt.sub><br/>(AsyncLocalStorage)
RLS->>RLS : INSERT vault_secure.documents<br/>WITH CHECK (user_id = app.current_user_id)<br/>status = 'PENDING' (défaut)
alt Contrainte violée
RLS-->>API : Erreur (hash dupliqué,<br/>format invalide)
API-->>Client : 400 / 409
else Succès
RLS-->>API : Document créé (id, status=PENDING)
API-->>Client : 201 Created
end Diagramme de séquence — Recherche déterministe (flux nominal)¶
Réf. invariants : AC4 (recherche GIN + filtrée par RLS), AC2 (aucun plaintext).
sequenceDiagram
participant Client
participant API as API Backend
participant RLS as PostgreSQL (RLS + GIN)
Client->>Client : HMAC-SHA256(mot-clé normalisé)
Client->>API : GET /documents?keyword=<token_hash>
API->>RLS : SET LOCAL app.current_user_id = <jwt.sub>
API->>RLS : SELECT ... WHERE keyword_deterministic @> ARRAY[<token_hash>]
RLS->>RLS : Filtrage RLS automatique<br/>(user_id = app.current_user_id)<br/>+ Index GIN (AC4)
RLS-->>API : Résultats (documents du propriétaire uniquement)
API-->>Client : 200 OK [{id, encrypted_metadata, status, ...}] Spécification technique¶
Schéma et DDL (référence)¶
La migration doit implémenter le DDL cible suivant (ajustements mineurs permis uniquement si justifiés par contraintes Postgres) :
CREATE SCHEMA IF NOT EXISTS vault_secure;- création conditionnelle de
document_status - table
vault_secure.documentsavec : id UUID PRIMARY KEY DEFAULT gen_random_uuid()user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADEencrypted_metadata BYTEA NOT NULLkeyword_deterministic TEXT[] NULLfile_hash BYTEA NOT NULL CHECK (octet_length(file_hash)=32)ovh_path TEXT NOT NULL+ contrainte regex de formatstatus document_status NOT NULL DEFAULT 'PENDING'- timestamps (
sealed_at,retention_until,expired_at,created_at,updated_at) - trigger de mise à jour
updated_at
Indexation¶
Obligatoire :
idx_documents_user_id(BTREE)idx_documents_user_file_hash(UNIQUE sur(user_id, file_hash))idx_documents_created_at(DESC)idx_documents_keywords_gin(GIN surkeyword_deterministic)idx_documents_statusidx_documents_retention_until
RLS / Policies¶
RLS activé sur vault_secure.documents :
SELECT: autorisé siuser_id = current_setting('app.current_user_id', true)::uuidINSERT:WITH CHECKidentique (user_id forcé par le contexte)UPDATE: autorisé uniquement si propriétaire etstatus='PENDING'DELETE: autorisé uniquement si propriétaire etstatus='PENDING'DELETE TO probatio_admin: autorisé (policy dédiée)
Note : les transitions PENDING→SEALED→EXPIRED sont effectuées par un rôle technique (policy optionnelle à définir en US dédiée si nécessaire).
Architecture RLS v2 (AsyncLocalStorage)¶
L'injection du contexte utilisateur (app.current_user_id) dans PostgreSQL est réalisée via AsyncLocalStorage (Node.js 16+) :
- Chaque requête HTTP dispose d'un contexte isolé.
- Le middleware d'authentification extrait le
user_iddu JWT. - Le
user_idest injecté dans la connexion PostgreSQL viaSET LOCAL app.current_user_id. - Les policies RLS utilisent ce contexte pour filtrer automatiquement les données.
Validation côté API¶
Le backend doit appliquer les règles suivantes :
user_id: ignoré si fourni par le client, remplacé par l'ID authentifiéencrypted_metadata: obligatoirefile_hash: obligatoire, longueur 32 bytesovh_path: obligatoire, conforme regexstatus:- à l'insertion :
PENDINGpar défaut - modifiable uniquement par rôle technique (pas par l'utilisateur)
Recherche déterministe¶
keyword_deterministic[]contient des tokens déterministes calculés côté client (ex. HMAC-SHA256 d'un mot-clé normalisé)- requêtes de recherche supportées via GIN, et filtrées implicitement par RLS
Tests d'acceptation¶
TA-1 — Isolation RLS¶
- Un utilisateur A ne peut pas
SELECT/UPDATE/DELETEles lignes de B INSERTforcé suruser_id = current_user
TA-2 — Règles statut / permissions¶
DELETEpar user surPENDING: OKDELETEpar user surSEALED/EXPIRED: refuséUPDATEpar user surSEALED/EXPIRED: refuséDELETEparprobatio_adminsurSEALED/EXPIRED: OK
TA-3 — Contraintes¶
file_hashlongueur ≠ 32 : rejetovh_pathinvalide : rejetstatusinvalide : rejet
TA-4 — Transitions de cycle¶
PENDING → SEALED: possible via rôle technique- retour arrière : impossible
SEALED → EXPIRED: possible aprèsretention_until < now()
TA-5 — Recherche déterministe¶
- Insertion avec
keyword_deterministicindexée (GIN) - requêtes renvoyant uniquement les documents du propriétaire (RLS)
Critères d'acceptation¶
- AC1 : Schéma conforme (ENUM, colonnes, contraintes, index, trigger)
- AC2 : Métadonnées toujours chiffrées (aucun plaintext en DB/log)
- AC3 : RLS strict + règles
UPDATE/DELETEalignées surstatus - AC4 : Recherche déterministe opérationnelle (GIN) + filtrée par RLS
- AC5 : Conformité probatoire minimale :
file_hash32 bytes +ovh_pathvalide + transitions maîtrisées - AC6 : Documents
EXPIREDéligibles à suppression (manuelle/auto) avec exigence de journalisation append-only (référencée)
Livrables¶
- Migration SQL : schéma + enum + table + contraintes + index + trigger + policies RLS
- Plumbing RLS applicatif : mise en place/usage de
app.current_user_id - Tests Postgres + tests API (RLS, contraintes, lifecycle)
- Documentation :
/docs/db/vault_secure/documents.md(cycle, règles, formatovh_path, exemples de requêtes) - Mise à jour Swagger/OpenAPI des endpoints concernés (si déjà existants)
Definition of Done¶
- Migration appliquée (dev + staging) sans erreur
- Toutes les policies RLS vérifiées par tests automatisés
- Tests API verts (validation input + invariants)
- Documentation publiée et référencée depuis l'EPIC PD-186
- Revue backend + revue sécurité validées