feat: dossier personnel par utilisateur — vault home-<user> + registre de vaults persistant #194
CI / lint (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / security (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s

- backend/user_home.py : ensure_user_home()/release_user_home() idempotents
  (mkdir, vault, watcher, octroi dans user.vaults), rappelés à la création
  d'un compte et au démarrage (migration + réparation + sweep des vaults
  home-* orphelins), dossier conservé à la suppression.
- backend/indexer.py : data/vaults.json, registre persistant des vaults
  ajoutés à la volée, fusionné par load_vault_config() — ils disparaissaient
  au premier reindex complet et au redémarrage. reload_single_vault remet
  vault_config[name] après remove_vault_from_index.
- backend/auth/router.py + routers/vaults.py : appels ensure/release +
  persistance à l'ajout/retrait d'un vault.
- docker-compose.yml : montage unique /NFS/OBSIDIAN_DOC/Home:/vaults/Home +
  OBSIGATE_HOME_ROOT (absente = fonctionnalité inactive).
- Tests : tests/test_user_home.py (12) + tests/test_auth_api.py::TestUserHome (2).
This commit is contained in:
2026-10-10 00:40:35 -04:00
parent f4c8504c8d
commit 8235d632b8
19 changed files with 700 additions and 17 deletions
+77
View File
@@ -0,0 +1,77 @@
# #194 — Dossier personnel par utilisateur
> **Statut :** livré | **Ouvert :** 2026-10-09 | **Type :** feature (auth + vaults)
## Problème
Un utilisateur créé dans ObsiGate n'avait aucun espace propre : il ne voyait
que les vaults qu'un admin lui attribuait à la main, et rien n'était préparé
sur le disque à sa création.
## Décision d'architecture
Deux formes étaient possibles :
1. **Un vault par utilisateur** (retenu) — `<racine>/<user>` devient le vault
`home-<user>`, octroyé dans `user.vaults`. L'ACL par vault existante
(`backend/auth/middleware.py::check_vault_access`, appliquée partout :
API, MCP, search, push, tools) fait l'isolation **sans rien inventer**.
2. Un vault `Home` unique + ACL par sous-dossier (écartée) — il n'existe
aucune ACL par chemin dans le code ; il aurait fallu la brancher dans
`files_read`, `files_media`, `history`, `backups`, `mcp/tools`, `push`…
Surface de sécurité trop grande pour un gain marginal.
Décisions produit validées : nom `home-<user>`, dossier **conservé** à la
suppression du compte, racine `/NFS/OBSIDIAN_DOC/Home` montée `/vaults/Home`.
## Mise en place
| Élément | Où |
|---|---|
| Service idempotent `ensure_user_home()` / `release_user_home()` / `ensure_all_user_homes()` | `backend/user_home.py` |
| Registre persistant des vaults dynamiques `data/vaults.json`, fusionné par `load_vault_config()` | `backend/indexer.py` (`persist_vault` / `unpersist_vault`) |
| Appel à la création et à la suppression d'un compte | `backend/auth/router.py` (`POST/DELETE /api/auth/admin/users`) |
| Passe de réparation/migration au démarrage | `backend/main.py` (`lifespan` → après le watcher) |
| Persistance des vaults ajoutés par `/api/vaults/add` + retrait | `backend/routers/vaults.py` |
| Montage + variable d'environnement | `docker-compose.yml` (`/NFS/OBSIDIAN_DOC/Home:/vaults/Home`, `OBSIGATE_HOME_ROOT=/vaults/Home`) |
## Garde-fous
- **Fonctionnalité inactive sans `OBSIGATE_HOME_ROOT`** : dev, tests et
desktop n'ont aucun comportement modifié.
- **Validation du nom** : `^[a-zA-Z0-9_-]{2,32}$` (même grammaire que
`CreateUserRequest.username_valid`), re-vérifiée avant de construire un
chemin — défense en profondeur contre le path traversal.
- **Nom de vault sans `/` ni `::`** : le nom sert de segment d'URL
(`/api/file/{vault}/…`) et de clé d'index `vault::path`.
- **Auto-réparation** : `ensure_all_user_homes()` au démarrage crée les
dossiers manquants, ré-enregistre les vaults perdus et octroie l'accès aux
users créés hors API. Toute erreur disque (NFS indisponible) est
journalisée et rattrapée au boot.
- **Suppression de compte** : index/watcher/registre refermés, dossier sur
disque conservé.
## Bug corrigé au passage
- `load_vault_config()` ne lisait que les env `VAULT_N_*`/`DIR_N_*` : tout
vault ajouté à la volée (y compris via `/api/vaults/add`) disparaissait au
premier reindex complet et au redémarrage. → `data/vaults.json`.
- `reload_single_vault()` perdait l'entrée `vault_config[vault]` après
`remove_vault_from_index()`.
## Tests
- `tests/test_user_home.py` (10) : création (dossier + vault + octroi +
registre), idempotence, username invalide, désactivation sans variable,
relecture du registre par `load_vault_config`, `release` (dossier
conservé), isolation `check_vault_access` entre users, passe de réparation.
- `tests/test_auth_api.py::TestUserHome` (2) : provisionnement via l'API
(création → dossier, vault visible, alice ne voit que son home, suppression
→ vault refermé / dossier conservé) + absence de dossier sans
`OBSIGATE_HOME_ROOT`.
## Impact utilisateur
Aucune nouvelle chaîne i18n : les noms de vault sont des données, la liste
provient déjà de `GET /api/vaults`. Le dossier apparaît tout seul dans le
sélecteur de vaults de l'utilisateur concerné.