Aller au contenu

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 : PENDINGSEALEDEXPIRED
  • 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/INSERT isolés par app.current_user_id
  • UPDATE/DELETE autorisés uniquement si status = PENDING pour l'utilisateur
  • DELETE admin (rôle probatio_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/DELETE autorisés (RLS + policy)
  • SEALED : état probatoire immuable
  • UPDATE/DELETE interdits 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 :

  • PENDINGSEALED (irréversible)
  • SEALEDEXPIRED (quand retention_until < now())
  • aucun retour arrière (ex. SEALEDPENDING interdit)

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.documents avec :
  • id UUID PRIMARY KEY DEFAULT gen_random_uuid()
  • user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE
  • encrypted_metadata BYTEA NOT NULL
  • keyword_deterministic TEXT[] NULL
  • file_hash BYTEA NOT NULL CHECK (octet_length(file_hash)=32)
  • ovh_path TEXT NOT NULL + contrainte regex de format
  • status 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 sur keyword_deterministic)
  • idx_documents_status
  • idx_documents_retention_until

RLS / Policies

RLS activé sur vault_secure.documents :

  • SELECT : autorisé si user_id = current_setting('app.current_user_id', true)::uuid
  • INSERT : WITH CHECK identique (user_id forcé par le contexte)
  • UPDATE : autorisé uniquement si propriétaire et status='PENDING'
  • DELETE : autorisé uniquement si propriétaire et status='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_id du JWT.
  • Le user_id est injecté dans la connexion PostgreSQL via SET 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 : obligatoire
  • file_hash : obligatoire, longueur 32 bytes
  • ovh_path : obligatoire, conforme regex
  • status :
  • à l'insertion : PENDING par 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/DELETE les lignes de B
  • INSERT forcé sur user_id = current_user

TA-2 — Règles statut / permissions

  • DELETE par user sur PENDING : OK
  • DELETE par user sur SEALED/EXPIRED : refusé
  • UPDATE par user sur SEALED/EXPIRED : refusé
  • DELETE par probatio_admin sur SEALED/EXPIRED : OK

TA-3 — Contraintes

  • file_hash longueur ≠ 32 : rejet
  • ovh_path invalide : rejet
  • status invalide : rejet

TA-4 — Transitions de cycle

  • PENDING → SEALED : possible via rôle technique
  • retour arrière : impossible
  • SEALED → EXPIRED : possible après retention_until < now()

TA-5 — Recherche déterministe

  • Insertion avec keyword_deterministic indexé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/DELETE alignées sur status
  • AC4 : Recherche déterministe opérationnelle (GIN) + filtrée par RLS
  • AC5 : Conformité probatoire minimale : file_hash 32 bytes + ovh_path valide + 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, format ovh_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