L'admin (vaults: ["*"]) voyait le home de chaque utilisateur dans sa barre latérale : "*" ouvrait tous les vaults, home-* compris. - backend/auth/middleware.py : check_vault_access exige un octroi explicite pour tout vault home-* (nouveau is_home_vault()). - Filtres « * » en dur remplacés par check_vault_access : dashboard, conflits, liens retour, favoris, abonnements push. - /api/search : search_vaults(is_allowed=…) filtre les bruts avant pagination (total et page restent justes). - backend/user_home.py : _grant n'écarte plus les comptes « * » — l'admin reçoit son propre home-admin (auto-réparé au démarrage). - Tests : test_user_home.py +2, assertion API inversée dans test_auth_api.py (admin ne voit plus home-alice).
108 lines
5.5 KiB
Markdown
108 lines
5.5 KiB
Markdown
# #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`.
|
|
|
|
## UI — arborescence (incrément)
|
|
|
|
- **Ordre** : tri stable `home-*` en premier, appliqué à la source
|
|
(`frontend/js/sidebar.js::_homeFirst`, sur les deux sites qui alimentent
|
|
`state.allVaults`) → arborescence, sélecteurs et refreshs homogènes.
|
|
- **Icône** : `getVaultIcon()` (`frontend/js/viewer.js`) renvoie
|
|
`icon("home", size)` pour `home-*` → même pipeline lucide que le reste de
|
|
l'app (`safeCreateIcons`, `stroke: currentColor`, suit le thème).
|
|
- **Test local** : `docker-compose.test-win.yml` monte
|
|
`test_home:/vaults/Home` + `OBSIGATE_HOME_ROOT=/vaults/Home`
|
|
(`test_home/` est dans `.gitignore`).
|
|
- **Vérifié live** (instance 2020, Playwright) : ordre `home-admin, TestDir,
|
|
TestVault` (l'admin ne voit plus le home des autres), `svg.lucide-home`
|
|
rendu, placeholder `<i>` remplacé.
|
|
|
|
## Isolation — « * » ne couvre pas les dossiers persos
|
|
|
|
- **Racine** : `check_vault_access` (`backend/auth/middleware.py`) renvoie
|
|
``vault_name in vaults`` pour tout vault ``home-*``, quel que soit le ``*``.
|
|
Toutes les routes (89 usages) passent par cette fonction.
|
|
- **Filtres « * » en dur remplacés** par cet appel : dashboard
|
|
(`routers/config.py`), conflits, liens retour, favoris, abonnements push.
|
|
- **Recherche** : `services/search.search_vaults(is_allowed=…)` filtre les
|
|
bruts **avant** pagination — ``total`` et page restent justes.
|
|
- **Octroi** : `_grant` n'écarte plus les comptes ``*`` → l'admin reçoit son
|
|
propre ``home-admin`` (réparé au démarrage pour les comptes existants).
|
|
- **Reste ouvert** (fuite pré-existante, hors #194) : `/api/search/advanced`,
|
|
`/api/tags`, `/api/tags/suggest`, `/api/tree-search` et `/api/suggest` ne
|
|
filtrent pas par vault accessible — à traiter dans une tâche dédiée.
|
|
|
|
## 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é.
|