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).
170 lines
6.1 KiB
Python
170 lines
6.1 KiB
Python
"""Dossier personnel par utilisateur (#194).
|
|
|
|
Chaque utilisateur reçoit ``<OBSIGATE_HOME_ROOT>/<username>`` monté comme un
|
|
vault propre ``home-<username>`` : l'isolation profite de l'ACL par vault
|
|
déjà en place (``check_vault_access``), aucune ACL par chemin à inventer.
|
|
|
|
Tout est idempotent (``ensure_user_home``) pour être rappelé à la création
|
|
d'un compte ET au démarrage : un dossier supprimé, un registre perdu ou un
|
|
user créé hors API se réparent au boot.
|
|
|
|
La fonctionnalité est inactive tant que ``OBSIGATE_HOME_ROOT`` n'est pas
|
|
défini (dev, tests, desktop) — comportement inchangé.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger("obsigate.user_home")
|
|
|
|
# Même grammaire que CreateUserRequest.username_valid (backend/auth/router.py) :
|
|
# segment de chemin sûr, déjà passé par la validation de l'API. Re-vérifié ici
|
|
# car le nom sert à construire un chemin (défense en profondeur).
|
|
_USERNAME_RE = re.compile(r"^[a-zA-Z0-9_-]{2,32}$")
|
|
|
|
|
|
def home_root() -> Path | None:
|
|
"""Racine des dossiers persos, ou ``None`` si la fonctionnalité est inactive."""
|
|
root = os.environ.get("OBSIGATE_HOME_ROOT", "").strip()
|
|
return Path(root) if root else None
|
|
|
|
|
|
def home_vault_name(username: str) -> str:
|
|
"""Nom de vault du dossier perso — pas de ``/`` ni ``::`` (segment d'URL, clé d'index)."""
|
|
return f"home-{username}"
|
|
|
|
|
|
async def ensure_user_home(username: str) -> str | None:
|
|
"""Crée (si besoin) le dossier perso de *username*, son vault et son octroi.
|
|
|
|
Retourne le nom de vault, ou ``None`` si désactivé / nom invalide /
|
|
erreur disque (journalisée, réparée au prochain démarrage).
|
|
"""
|
|
root = home_root()
|
|
if root is None:
|
|
return None
|
|
if not _USERNAME_RE.match(username):
|
|
logger.warning(f"Home folder skipped: invalid username {username!r}")
|
|
return None
|
|
|
|
home = root / username
|
|
try:
|
|
home.mkdir(parents=True, exist_ok=True)
|
|
except OSError:
|
|
logger.exception(f"Cannot create home folder {home} for user '{username}'")
|
|
return None
|
|
|
|
name = home_vault_name(username)
|
|
try:
|
|
from backend.indexer import add_vault_to_index, index, persist_vault, vault_config
|
|
from backend.sse import sse_manager
|
|
|
|
vault_path = str(home)
|
|
if name not in index:
|
|
await add_vault_to_index(name, vault_path)
|
|
persist_vault(name)
|
|
from backend.watcher_state import get_watcher
|
|
|
|
watcher = get_watcher()
|
|
if watcher:
|
|
await watcher.add_vault(name, vault_path)
|
|
await sse_manager.broadcast("vault_added", {"vault": name})
|
|
logger.info(f"Home vault '{name}' registered at {vault_path}")
|
|
elif name not in vault_config:
|
|
vault_config[name] = {"path": vault_path, "attachmentsPath": None,
|
|
"scanAttachmentsOnStartup": True}
|
|
except Exception:
|
|
logger.exception(f"Cannot register home vault '{name}' for user '{username}'")
|
|
return None
|
|
|
|
_grant(username, name)
|
|
return name
|
|
|
|
|
|
async def release_user_home(username: str) -> None:
|
|
"""Retire le vault du dossier perso à la suppression du compte (#194).
|
|
|
|
Le dossier sur disque est **conservé** (décision produit : pas de perte
|
|
de données) ; seul l'index, le watcher et le registre le referment.
|
|
"""
|
|
name = home_vault_name(username)
|
|
try:
|
|
from backend.indexer import index, remove_vault_from_index, unpersist_vault
|
|
|
|
unpersist_vault(name)
|
|
if name not in index:
|
|
return
|
|
await remove_vault_from_index(name)
|
|
from backend.sse import sse_manager
|
|
from backend.watcher_state import get_watcher
|
|
|
|
watcher = get_watcher()
|
|
if watcher:
|
|
await watcher.remove_vault(name)
|
|
await sse_manager.broadcast("vault_removed", {"vault": name})
|
|
logger.info(f"Home vault '{name}' released (folder kept)")
|
|
except Exception:
|
|
logger.exception(f"Cannot release home vault '{name}'")
|
|
|
|
|
|
def _grant(username: str, vault_name: str) -> None:
|
|
"""Ajoute le vault à ``user.vaults`` s'il n'y est pas déjà (#194).
|
|
|
|
L'octroi est explicite même pour un admin (``vaults: ["*"]``) : ``*`` ne
|
|
couvre jamais un dossier perso (voir ``check_vault_access``).
|
|
"""
|
|
from backend.auth.user_store import get_user, update_user
|
|
|
|
user = get_user(username)
|
|
if not user:
|
|
return # créé hors API (bootstrap avant users.json) → réparé au boot suivant
|
|
vaults = user.get("vaults") or []
|
|
if vault_name in vaults:
|
|
return
|
|
update_user(username, {"vaults": [*vaults, vault_name]})
|
|
|
|
|
|
async def ensure_all_user_homes() -> int:
|
|
"""Passe de réparation/migration au démarrage : un home par user existant.
|
|
|
|
Balaye aussi les vaults orphelins (compte supprimé hors route, ex.
|
|
``create_admin.py delete``) : ils sont refermés, dossier conservé.
|
|
"""
|
|
from backend.auth.user_store import get_all_users
|
|
from backend.indexer import index, vault_config
|
|
|
|
root = home_root()
|
|
if root is None:
|
|
return 0
|
|
users = get_all_users()
|
|
usernames = {u.get("username") for u in users}
|
|
created = 0
|
|
for user in users:
|
|
username = user.get("username")
|
|
if not username:
|
|
continue
|
|
if await ensure_user_home(username):
|
|
created += 1
|
|
if created:
|
|
logger.info(f"User home folders ensured for {created} user(s)")
|
|
|
|
# Orphelins : vault home-<x> toujours indexé mais <x> n'existe plus.
|
|
# Path.parent == root → on ne touche qu'aux dossiers sous la racine Home,
|
|
# jamais à un vault admin nommé « home-… » par ailleurs.
|
|
for name in list(index):
|
|
if not name.startswith("home-"):
|
|
continue
|
|
owner = name[len("home-"):]
|
|
if owner in usernames:
|
|
continue
|
|
cfg_path = Path((vault_config.get(name) or {}).get("path", ""))
|
|
if cfg_path.parent != root:
|
|
continue
|
|
logger.warning(f"Orphan home vault '{name}' released (user deleted?)")
|
|
await release_user_home(owner)
|
|
return created
|