PD-15 — Créer schéma DB users avec RLS¶
📚 Navigation User Story
| Document | | | ---------- | -- | | 📋 **Spécification** | *(ce document)* | | 🛠️ [Plan d'implémentation](PD-15-plan.md) | | | ✅ [Critères d'acceptation](PD-15-acceptability.md) | | | 📝 [Retour d'expérience](PD-15-rex.md) | | [← Retour à backend-core](../PD-186-epic.md) · [↑ Index User Story](index.md)Références¶
- EPIC : PD-186 — BACKEND-CORE
- JIRA : PD-15
- Dépendances liées : PD-24/PD-25 (SRP-6a), PD-16 (documents)
Objectif¶
Créer la table vault_secure.users comme fondation du modèle multi-compte ProbatioVault, avec :
- Isolation stricte par utilisateur via RLS (Row-Level Security)
- Compatibilité SRP-6a : stockage serveur uniquement du verifier SRP (
password_hash) — jamais le mot de passe - Gouvernance minimale (plan d'abonnement, timestamps)
- Mécanisme standard d'injection de contexte :
app.current_user_id
Contexte¶
ProbatioVault impose un backend conforme à :
- Zero-Knowledge : aucune donnée sensible en clair et aucun mot de passe stocké côté serveur
- Séparation logique stricte : schéma
vault_secure - RLS : un utilisateur ne peut lire/modifier que sa ligne
- SRP-6a : le backend stocke uniquement
salt+verifier(icipassword_hash= verifier), jamais un hash classique de mot de passe
vault_secure.users est la base de toute la gouvernance d'accès, et doit rester simple, robuste, auditable, et sécurisée.
Périmètre¶
Inclus¶
- Création du schéma
vault_secure(si absent) - Création table
vault_secure.users - Contraintes d'intégrité (unicité email, plan, non-null)
- Indexation pour lookup SRP (email)
- Trigger
updated_at - Activation RLS + policies
SELECT/UPDATE/DELETE - Convention applicative :
SET app.current_user_id = '<uuid>' - Tests sécurité (RLS, intégrité, règles)
- Documentation :
/docs/db/vault_secure/users.md
Exclu (hors périmètre)¶
- Implémentation SRP-6a phase ½ (PD-24/PD-25)
- Gestion des documents (PD-16)
- Audit logs signés HSM (PD-37)
Modèle de données¶
Table : vault_secure.users¶
id: UUID (PK) — identifiant unique utilisateuremail: TEXT — unique, non nul — identifiant de connexion (SRP init)password_hash: BYTEA — non nul — verifier SRP-6a (pas un hash de mot de passe classique)plan: TEXT — non nul — valeurs autorisées :free | premium | business(défautfree)created_at: TIMESTAMPTZ — défautnow()updated_at: TIMESTAMPTZ — défautnow()
Contraintes¶
emailuniquepassword_hashnon nulplan∈ {free,premium,business}idimmuable (par convention API + blocage logique côté service)
Index¶
- Index de lookup sur
email(SRP init) - Contrainte UNIQUE sur
email(détection doublons)
RLS (Row-Level Security)¶
Convention applicative¶
Avant toute requête applicative (hors endpoints publics d'inscription), l'application positionne :
app.current_user_id = '<uuid>'
Règles d'accès¶
- SELECT : un utilisateur ne peut lire que sa propre ligne
- UPDATE : un utilisateur ne peut modifier que sa propre ligne
- DELETE : interdit pour tous les utilisateurs standards ; réservé à un rôle admin/probatoire (ou à une procédure contrôlée)
- INSERT : effectué via backend (endpoint register) — modèle recommandé :
- l'insertion n'est pas faite "au nom" de l'utilisateur authentifié (il ne l'est pas encore),
- elle doit être contrôlée par un rôle serveur dédié (ex.
role_auth) et par la logique applicative.
Décision : pas de policy RLS "INSERT utilisateur". Les créations de compte passent via un rôle/service d'auth, qui applique ses propres validations.
Règles API / Backend¶
Création de compte¶
password_hash= verifier SRP (généré côté client)- aucun mot de passe ne transite ni n'est stocké
plan=freepar défaut (sauf logique métier)emailnormalisé (minimisation des variantes, ex. trim + lowercase)
Connexion (SRP init)¶
- lookup rapide par
email(index requis) - extraction des paramètres SRP stockés (verifier + autres champs associés dans les US SRP)
Mise à jour profil¶
idnon modifiableemailreste uniqueplanmodifiable uniquement via logique de facturation (pas en libre-service)
Logs¶
- interdiction stricte de logguer
password_hash - aucune donnée SRP sensible (x, A, a, M1, M2, etc.) stockée ni logguée
Diagrammes¶
Diagramme d'états — Cycle de vie utilisateur¶
Le compte utilisateur traverse plusieurs états, de la création à la suppression éventuelle par un administrateur. L'isolation RLS (AC3) et l'interdiction de suppression standard (AC6) contraignent les transitions.
stateDiagram-v2
[*] --> pending : INSERT via role_auth\n(endpoint register)
pending --> active : email confirmé\n(hors périmètre PD-15)
active --> active : UPDATE profil\n(RLS — AC3 : propre ligne uniquement)
active --> deleted : DELETE par rôle admin\n(AC6 : user standard interdit)
pending --> deleted : DELETE par rôle admin\n(AC6 : user standard interdit)
deleted --> [*]
note right of active
RLS SELECT/UPDATE : id = app.current_user_id
email unique (AC4)
password_hash = verifier SRP (AC2)
end note
note right of deleted
Suppression réservée rôle admin
ou procédure contrôlée (AC6)
end note Diagramme de séquence — Création de compte (SRP-6a)¶
Le flux de création implique le client, le backend (rôle role_auth) et PostgreSQL avec injection du contexte RLS. Aucun mot de passe ne transite ni n'est stocké (AC2/AC5 — Zero-Knowledge).
sequenceDiagram
participant C as Client
participant B as Backend (role_auth)
participant DB as PostgreSQL (vault_secure)
C->>C : Générer verifier SRP-6a localement
C->>B : POST /register {email, verifier}
B->>B : Normaliser email (trim + lowercase)
B->>DB : INSERT INTO vault_secure.users\n(id, email, password_hash, plan='free')
alt Email déjà existant
DB-->>B : UNIQUE violation (AC4)
B-->>C : 409 Conflict
else Succès
DB-->>B : Row inserted
B-->>C : 201 Created {id}
end
Note over C,DB : password_hash = verifier SRP uniquement (AC2)\nAucun mot de passe stocké (AC5) Diagramme de séquence — Requête avec isolation RLS¶
Toute requête applicative post-authentification injecte le contexte RLS via app.current_user_id. L'isolation stricte garantit qu'un utilisateur ne peut accéder qu'à sa propre ligne (AC3).
sequenceDiagram
participant C as Client
participant B as Backend
participant DB as PostgreSQL (vault_secure)
C->>B : GET /users/me (JWT)
B->>DB : SET app.current_user_id = '<user_uuid>'
B->>DB : SELECT * FROM vault_secure.users\nWHERE id = app.current_user_id
DB->>DB : RLS policy évalue :\nid = current_setting('app.current_user_id')
alt Ligne propre (AC3 — OK)
DB-->>B : Row retournée
B-->>C : 200 {id, email, plan, created_at, updated_at}
else Tentative accès autre utilisateur (AC3 — DENIED)
DB-->>B : 0 rows (filtrage RLS silencieux)
B-->>C : 404 Not Found
end
Note over B,DB : password_hash JAMAIS exposé\nni loggué (Zero-Knowledge) Critères d'acceptation¶
- AC1 — Table créée conformément au schéma
- colonnes, types, contraintes, indexes
- trigger
updated_atopérationnel - AC2 — Confidentialité Zero-Knowledge
password_hash= verifier SRP uniquement- aucun mot de passe / hash classique / clé crypto stockés
- AC3 — RLS strictement isolant
- user A ne voit/modifie que sa ligne
- tentative d'accès à user B rejetée
- AC4 — Unicité email
- INSERT email existant → erreur
- UPDATE vers email existant → erreur
- AC5 — Compatibilité SRP
- lookup email rapide (index)
- aucune donnée SRP sensible stockée
- AC6 — Sécurité
- DELETE impossible pour user standard
- suppression possible uniquement via rôle admin/procédure contrôlée
Tests d'acceptation¶
- TA-1 — Création utilisateur
- création via API : entrée correcte,
plan=free,password_hashprésent - TA-2 — RLS lecture
- user A lit sa ligne : OK
- user A lit user B : DENIED
- TA-3 — RLS update
- user A modifie son email : OK
- user A tente de modifier user B : DENIED
- user A tente de changer
id: rejet (API + DB si garde-fou) - TA-4 — Duplicate email
- email X déjà pris → violation unicité sur INSERT/UPDATE
- TA-5 — Conformité
password_hash - la valeur stockée correspond au verifier SRP envoyé par le client
- TA-6 — Delete utilisateur
- user standard : DELETE DENIED
- rôle admin/probatoire : DELETE OK (si activé)
Livrables¶
- Migration SQL complète (schéma + table + contraintes + indexes + trigger + RLS + policies)
- Mise en place du "RLS plumbing" applicatif (
app.current_user_id) - Tests SQL + tests API couvrant : RLS, intégrité, unicité, non-régression
- Documentation :
/docs/db/vault_secure/users.md