fix: un dossier perso n'est plus visible par les autres comptes #194
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).
This commit is contained in:
+23
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.65.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.65.1**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,10 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.65.1] — 2026-10-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.65.0] — 2026-10-10
|
## [2.65.0] — 2026-10-10
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
@@ -31,6 +35,24 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
- Tests : `sidebar-filters.test.mjs` +2 (icône maison pour `home-*`,
|
- Tests : `sidebar-filters.test.mjs` +2 (icône maison pour `home-*`,
|
||||||
gemme violette inchangée ailleurs).
|
gemme violette inchangée ailleurs).
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
|
||||||
|
- **#194 — Un dossier perso n'est plus visible par les autres comptes**
|
||||||
|
- `check_vault_access` (`backend/auth/middleware.py`) refuse désormais tout
|
||||||
|
vault `home-*` qui n'est pas **explicitement** octroyé au compte : le
|
||||||
|
`vaults: ["*"]` de l'admin ne couvre plus les dossiers persos (avant,
|
||||||
|
l'admin voyait `home-<user>` de chaque utilisateur dans sa barre latérale).
|
||||||
|
- Les filtres `« * »` écrits en dur dans les routes ont été remplacés par
|
||||||
|
cet appel unique — dashboard, conflits, liens retour, favoris,
|
||||||
|
abonnements push : partout la même règle.
|
||||||
|
- `/api/search` filtre les résultats par vault accessible **avant**
|
||||||
|
pagination (`search_vaults(is_allowed=…)`), sinon les notes personnelles
|
||||||
|
d'un autre pouvait apparaître dans les résultats.
|
||||||
|
- `_grant` octroie désormais le home aussi aux comptes `« * »` : l'admin
|
||||||
|
voit **son** `home-admin`, réparé au démarrage pour les comptes existants.
|
||||||
|
- Tests : `test_user_home.py` +2 (`« * »` sans homes, filtrage de la
|
||||||
|
recherche), assertion API inversée dans `test_auth_api.py`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [2.64.0] — 2026-10-10
|
## [2.64.0] — 2026-10-10
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.65.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.65.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.65.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.65.1 | Dernière mise à jour : Septembre 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.65.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.65.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.65.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.65.1 | Last updated: September 2026*
|
||||||
|
|||||||
@@ -142,6 +142,11 @@ def require_admin(current_user=Depends(require_auth)):
|
|||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
def is_home_vault(vault_name: str) -> bool:
|
||||||
|
"""Un dossier personnel (#194) : vault « home-<user> »."""
|
||||||
|
return vault_name.startswith("home-")
|
||||||
|
|
||||||
|
|
||||||
def check_vault_access(vault_name: str, user: dict) -> bool:
|
def check_vault_access(vault_name: str, user: dict) -> bool:
|
||||||
"""Check if a user has access to a specific vault.
|
"""Check if a user has access to a specific vault.
|
||||||
|
|
||||||
@@ -149,8 +154,13 @@ def check_vault_access(vault_name: str, user: dict) -> bool:
|
|||||||
- vaults == ["*"] → full access (admin default)
|
- vaults == ["*"] → full access (admin default)
|
||||||
- vault_name in vaults → access granted
|
- vault_name in vaults → access granted
|
||||||
- otherwise → denied
|
- otherwise → denied
|
||||||
|
|
||||||
|
#194 : un dossier personnel n'est **jamais** couvert par ``*`` — sinon
|
||||||
|
un admin (``vaults: ["*"]``) verrait le dossier de chaque utilisateur.
|
||||||
"""
|
"""
|
||||||
vaults = user.get("_token_vaults") or user.get("vaults", [])
|
vaults = user.get("_token_vaults") or user.get("vaults", [])
|
||||||
|
if is_home_vault(vault_name):
|
||||||
|
return vault_name in vaults
|
||||||
if "*" in vaults:
|
if "*" in vaults:
|
||||||
return True
|
return True
|
||||||
return vault_name in vaults
|
return vault_name in vaults
|
||||||
|
|||||||
+4
-4
@@ -13,7 +13,7 @@ from typing import Any
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from backend.auth.middleware import require_auth
|
from backend.auth.middleware import check_vault_access, require_auth
|
||||||
|
|
||||||
logger = logging.getLogger("obsigate.push")
|
logger = logging.getLogger("obsigate.push")
|
||||||
|
|
||||||
@@ -167,9 +167,9 @@ async def subscribe_push(
|
|||||||
"""Subscribe to push notifications for a vault."""
|
"""Subscribe to push notifications for a vault."""
|
||||||
username = current_user.get("username", "unknown")
|
username = current_user.get("username", "unknown")
|
||||||
|
|
||||||
# Check if user has access to this vault
|
# Check if user has access to this vault (#194 : "*" n'inclut pas les
|
||||||
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
|
# dossiers persos, check_vault_access est la source unique).
|
||||||
if "*" not in user_vaults and request.vault not in user_vaults:
|
if not check_vault_access(request.vault, current_user):
|
||||||
raise HTTPException(status_code=403, detail="No access to this vault")
|
raise HTTPException(status_code=403, detail="No access to this vault")
|
||||||
|
|
||||||
# Check if subscription already exists
|
# Check if subscription already exists
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from pathlib import Path
|
|||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||||
|
|
||||||
from backend.ai import PROVIDERS, _read_ai_keys, get_ai_key
|
from backend.ai import PROVIDERS, _read_ai_keys, get_ai_key
|
||||||
from backend.auth.middleware import require_admin, require_auth
|
from backend.auth.middleware import check_vault_access, require_admin, require_auth
|
||||||
from backend.indexer import index
|
from backend.indexer import index
|
||||||
from backend.media_types import IMAGE_EXTENSIONS
|
from backend.media_types import IMAGE_EXTENSIONS
|
||||||
from backend.schemas import (
|
from backend.schemas import (
|
||||||
@@ -495,14 +495,13 @@ async def api_diagnostics(current_user=Depends(require_admin)):
|
|||||||
@router.get("/api/dashboard", response_model=DashboardResponse)
|
@router.get("/api/dashboard", response_model=DashboardResponse)
|
||||||
async def api_dashboard(current_user=Depends(require_auth)):
|
async def api_dashboard(current_user=Depends(require_auth)):
|
||||||
"""Aggregated dashboard statistics across all accessible vaults."""
|
"""Aggregated dashboard statistics across all accessible vaults."""
|
||||||
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
|
|
||||||
vault_stats = []
|
vault_stats = []
|
||||||
total_files = 0
|
total_files = 0
|
||||||
total_tags = set()
|
total_tags = set()
|
||||||
total_size = 0
|
total_size = 0
|
||||||
total_images = 0
|
total_images = 0
|
||||||
for vname, vdata in index.items():
|
for vname, vdata in index.items():
|
||||||
if "*" not in user_vaults and vname not in user_vaults:
|
if not check_vault_access(vname, current_user):
|
||||||
continue
|
continue
|
||||||
files = vdata.get("files", [])
|
files = vdata.get("files", [])
|
||||||
fc = len(files)
|
fc = len(files)
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ router = APIRouter(tags=["conflicts"])
|
|||||||
@router.get("/api/conflicts", response_model=ConflictsResponse)
|
@router.get("/api/conflicts", response_model=ConflictsResponse)
|
||||||
async def api_conflicts(current_user=Depends(require_auth)):
|
async def api_conflicts(current_user=Depends(require_auth)):
|
||||||
"""List sync-conflict files across accessible vaults."""
|
"""List sync-conflict files across accessible vaults."""
|
||||||
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
|
|
||||||
all_conflicts = get_conflicts()
|
all_conflicts = get_conflicts()
|
||||||
if "*" not in user_vaults:
|
# #194 : filtrage via check_vault_access ("*" n'inclut pas les homes).
|
||||||
all_conflicts = [c for c in all_conflicts if c["vault"] in user_vaults]
|
all_conflicts = [c for c in all_conflicts if check_vault_access(c["vault"], current_user)]
|
||||||
return {"conflicts": all_conflicts, "total": len(all_conflicts)}
|
return {"conflicts": all_conflicts, "total": len(all_conflicts)}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -166,12 +166,11 @@ async def api_file_backlinks(
|
|||||||
if not vault_data:
|
if not vault_data:
|
||||||
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
||||||
|
|
||||||
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
|
|
||||||
backlinks = get_backlinks(vault_name, path)
|
backlinks = get_backlinks(vault_name, path)
|
||||||
|
|
||||||
# Filter by user-accessible vaults
|
# Filter by user-accessible vaults (#194 : check_vault_access, "*" sans
|
||||||
if "*" not in user_vaults:
|
# les dossiers persos).
|
||||||
backlinks = [b for b in backlinks if b["vault"] in user_vaults]
|
backlinks = [b for b in backlinks if check_vault_access(b["vault"], current_user)]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"vault": vault_name,
|
"vault": vault_name,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ async def api_recent(limit: int | None = Query(None), vault: str | None = Query(
|
|||||||
@router.get("/api/bookmarks", response_model=BookmarksResponse)
|
@router.get("/api/bookmarks", response_model=BookmarksResponse)
|
||||||
async def api_bookmarks(vault: str | None = Query(None), current_user=Depends(require_auth)):
|
async def api_bookmarks(vault: str | None = Query(None), current_user=Depends(require_auth)):
|
||||||
username = current_user.get("username")
|
username = current_user.get("username")
|
||||||
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
|
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
return {"files": []}
|
return {"files": []}
|
||||||
@@ -68,7 +67,8 @@ async def api_bookmarks(vault: str | None = Query(None), current_user=Depends(re
|
|||||||
files_resp = []
|
files_resp = []
|
||||||
for item in history:
|
for item in history:
|
||||||
v_name = item["vault"]
|
v_name = item["vault"]
|
||||||
if "*" not in user_vaults and v_name not in user_vaults:
|
# #194 : check_vault_access — "*" n'inclut pas les dossiers persos.
|
||||||
|
if not check_vault_access(v_name, current_user):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Find in index to get metadata
|
# Find in index to get metadata
|
||||||
|
|||||||
@@ -82,7 +82,12 @@ async def api_search(
|
|||||||
# paginate in the shared service so routes and tools share the same logic.
|
# paginate in the shared service so routes and tools share the same logic.
|
||||||
return await loop.run_in_executor(
|
return await loop.run_in_executor(
|
||||||
get_search_executor(),
|
get_search_executor(),
|
||||||
partial(search_vaults, q, vault, tag, limit, offset),
|
# #194 : filtre par vault accessible AVANT pagination ("*" sans les
|
||||||
|
# dossiers persos) — sinon un user voyait les notes des autres.
|
||||||
|
partial(
|
||||||
|
search_vaults, q, vault, tag, limit, offset,
|
||||||
|
is_allowed=lambda v: check_vault_access(v, current_user),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from backend.auth.middleware import is_home_vault
|
||||||
from backend.history import get_recent_opened, is_bookmarked
|
from backend.history import get_recent_opened, is_bookmarked
|
||||||
from backend.indexer import find_file_in_index, index
|
from backend.indexer import find_file_in_index, index
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ def humanize_mtime(mtime: float) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _can_access(vault: str, user_vaults: list[str]) -> bool:
|
def _can_access(vault: str, user_vaults: list[str]) -> bool:
|
||||||
|
# #194 : un dossier perso ne bénéficie jamais de "*" (même règle que
|
||||||
|
# backend.auth.middleware.check_vault_access).
|
||||||
|
if is_home_vault(vault):
|
||||||
|
return vault in user_vaults
|
||||||
return "*" in user_vaults or vault in user_vaults
|
return "*" in user_vaults or vault in user_vaults
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
@@ -11,15 +12,19 @@ def search_vaults(
|
|||||||
tag: str | None = None,
|
tag: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
|
is_allowed: Callable[[str], bool] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Full-text search with pagination, returned as the API response payload.
|
"""Full-text search with pagination, returned as the API response payload.
|
||||||
|
|
||||||
No permission filtering is applied here: callers that need it (the tool
|
``is_allowed`` (#194) filters the raw hits **before** pagination, so a
|
||||||
layer) filter the ``results`` list themselves.
|
restricted vault set neither distorts ``total`` nor the returned page.
|
||||||
|
The tool layer filters on its own and leaves it as ``None``.
|
||||||
"""
|
"""
|
||||||
from backend.search import search
|
from backend.search import search
|
||||||
|
|
||||||
all_results = search(q, vault_filter=vault, tag_filter=tag)
|
all_results = search(q, vault_filter=vault, tag_filter=tag)
|
||||||
|
if is_allowed is not None:
|
||||||
|
all_results = [r for r in all_results if is_allowed(r.get("vault", ""))]
|
||||||
total = len(all_results)
|
total = len(all_results)
|
||||||
page = all_results[offset: offset + limit]
|
page = all_results[offset: offset + limit]
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -112,14 +112,18 @@ async def release_user_home(username: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _grant(username: str, vault_name: str) -> None:
|
def _grant(username: str, vault_name: str) -> None:
|
||||||
"""Ajoute le vault à ``user.vaults`` si l'utilisateur n'a pas déjà ``*``."""
|
"""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
|
from backend.auth.user_store import get_user, update_user
|
||||||
|
|
||||||
user = get_user(username)
|
user = get_user(username)
|
||||||
if not user:
|
if not user:
|
||||||
return # créé hors API (bootstrap avant users.json) → réparé au boot suivant
|
return # créé hors API (bootstrap avant users.json) → réparé au boot suivant
|
||||||
vaults = user.get("vaults") or []
|
vaults = user.get("vaults") or []
|
||||||
if "*" in vaults or vault_name in vaults:
|
if vault_name in vaults:
|
||||||
return
|
return
|
||||||
update_user(username, {"vaults": [*vaults, vault_name]})
|
update_user(username, {"vaults": [*vaults, vault_name]})
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.65.0"
|
version = "2.65.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.65.0"
|
version = "2.65.1"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.65.0",
|
"version": "2.65.1",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -343,6 +343,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
|
|
||||||
| 2026-10-09 | #194 | Fonctionnalité (UI) | `frontend/js/sidebar.js`, `frontend/js/viewer.js`, `docker-compose.test-win.yml`, `.gitignore`, `tests/frontend/sidebar-filters.test.mjs`, `docs/ROADMAP.md`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 (incrément UI)** : les vaults `home-<user>` s'affichent **en premier** dans l'arborescence de la sidebar — tri stable appliqué à la source (`_homeFirst` dans `loadVaults()` et dans le soft-refresh, donc `state.allVaults`, l'arborescence et les sélecteurs restent cohérents) — et portent l'icône **maison** lucide (`data-lucide="home"` via `getVaultIcon`, même `safeCreateIcons`/`stroke: currentColor` que les autres icônes, thème-aware). Compose de test : `test_home:/vaults/Home` + `OBSIGATE_HOME_ROOT=/vaults/Home` (dossier `test_home/` ignoré par git). Vérifié : `sidebar-filters.test.mjs` 8→**10/10**, `validate-imports` 42 modules / 372 exports, `unit.test.mjs` 13/13, suites voisines (ai-sidebar, toolbar-order, search-facets, pane-manager, editor-inline) vertes, **live Playwright sur l'instance 2020** : ordre `home-admin, home-bruno, TestDir, TestVault` + `svg.lucide-home` rendu (placeholder `<i>` remplacé) | ✅ livré (en attente vérif utilisateur) |
|
| 2026-10-09 | #194 | Fonctionnalité (UI) | `frontend/js/sidebar.js`, `frontend/js/viewer.js`, `docker-compose.test-win.yml`, `.gitignore`, `tests/frontend/sidebar-filters.test.mjs`, `docs/ROADMAP.md`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 (incrément UI)** : les vaults `home-<user>` s'affichent **en premier** dans l'arborescence de la sidebar — tri stable appliqué à la source (`_homeFirst` dans `loadVaults()` et dans le soft-refresh, donc `state.allVaults`, l'arborescence et les sélecteurs restent cohérents) — et portent l'icône **maison** lucide (`data-lucide="home"` via `getVaultIcon`, même `safeCreateIcons`/`stroke: currentColor` que les autres icônes, thème-aware). Compose de test : `test_home:/vaults/Home` + `OBSIGATE_HOME_ROOT=/vaults/Home` (dossier `test_home/` ignoré par git). Vérifié : `sidebar-filters.test.mjs` 8→**10/10**, `validate-imports` 42 modules / 372 exports, `unit.test.mjs` 13/13, suites voisines (ai-sidebar, toolbar-order, search-facets, pane-manager, editor-inline) vertes, **live Playwright sur l'instance 2020** : ordre `home-admin, home-bruno, TestDir, TestVault` + `svg.lucide-home` rendu (placeholder `<i>` remplacé) | ✅ livré (en attente vérif utilisateur) |
|
||||||
|
|
||||||
|
| 2026-10-09 | #194 | Correction (isolation) | `backend/auth/middleware.py`, `backend/services/recent.py`, `backend/services/search.py`, `backend/routers/search.py`, `backend/routers/config.py`, `backend/routers/conflicts.py`, `backend/routers/files_read.py`, `backend/routers/history.py`, `backend/push.py`, `backend/user_home.py`, `tests/test_user_home.py`, `tests/test_auth_api.py`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 — l'admin voyait le home de chaque utilisateur** (retour utilisateur) : `vaults: ["*"]` ouvrait **tous** les vaults, donc `home-<user>` compris. Correctif à la racine : `check_vault_access` exige un octroi **explicite** pour tout vault `home-*` (nouveau `is_home_vault()`), et les filtres `« * »` écrits en dur dans les routes ont été remplacés par cet appel unique — dashboard (`routers/config.py`), conflits, liens retour, favoris, abonnements push (`/api/search/replace` utilisait déjà `is_vault_allowed`). `/api/search` filtre désormais les bruts **avant** pagination via `search_vaults(is_allowed=…)` (sinon notes personnelles d'un autre compte dans les résultats). `_grant` n'écarte plus les comptes `« * »` → l'admin reçoit son propre `home-admin` (auto-réparé au démarrage). Vérifié : `test_user_home.py` 12→**14/14** (dont « * » sans homes + filtrage recherche), `test_auth_api.py` 2/2 (admin ne voit **plus** `home-alice`, alice voit le sien), suite **1705 passed / 2 skipped**, ruff backend 0, mypy 114 fichiers 0, **live Playwright instance 2020** : arborescence admin = `home-admin, TestDir, TestVault` (plus de `home-bruno`), `svg.lucide-home` rendu. Reste ouvert (fuite pré-existante, hors périmètre) : `/api/search/advanced`, `/api/tags`, `/api/tags/suggest`, `/api/tree-search`, `/api/suggest` ne filtrent pas par vault accessible | ✅ livré (en attente vérif utilisateur) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📜 Historique des bugs résolus
|
## 📜 Historique des bugs résolus
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.65.0 | **Dernière mise à jour :** 2026-10-10
|
> **Version :** 2.65.1 | **Dernière mise à jour :** 2026-10-10
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -50,6 +50,9 @@
|
|||||||
`docker-compose.test-win.yml` → `test_home:/vaults/Home`)
|
`docker-compose.test-win.yml` → `test_home:/vaults/Home`)
|
||||||
- [x] UI : vaults `home-*` en **premier** dans l'arborescence + icône maison
|
- [x] UI : vaults `home-*` en **premier** dans l'arborescence + icône maison
|
||||||
lucide (`getVaultIcon`), vérifié live sur l'instance 2020
|
lucide (`getVaultIcon`), vérifié live sur l'instance 2020
|
||||||
|
- [x] Isolation : « * » (admin) ne couvre **plus** les dossiers persos —
|
||||||
|
`check_vault_access` + filtres des routes + `/api/search` (avant
|
||||||
|
pagination) ; octroi du home aussi aux comptes « * »
|
||||||
- [x] Tests (création, persistance après rebuild, isolation 403 entre users) + docs
|
- [x] Tests (création, persistance après rebuild, isolation 403 entre users) + docs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -81,9 +81,24 @@ suppression du compte, racine `/NFS/OBSIDIAN_DOC/Home` montée `/vaults/Home`.
|
|||||||
- **Test local** : `docker-compose.test-win.yml` monte
|
- **Test local** : `docker-compose.test-win.yml` monte
|
||||||
`test_home:/vaults/Home` + `OBSIGATE_HOME_ROOT=/vaults/Home`
|
`test_home:/vaults/Home` + `OBSIGATE_HOME_ROOT=/vaults/Home`
|
||||||
(`test_home/` est dans `.gitignore`).
|
(`test_home/` est dans `.gitignore`).
|
||||||
- **Vérifié live** (instance 2020, Playwright) : ordre
|
- **Vérifié live** (instance 2020, Playwright) : ordre `home-admin, TestDir,
|
||||||
`home-admin, home-bruno, TestDir, TestVault`, `svg.lucide-home` rendu,
|
TestVault` (l'admin ne voit plus le home des autres), `svg.lucide-home`
|
||||||
placeholder `<i>` remplacé.
|
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
|
## Impact utilisateur
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.65.0",
|
"version": "2.65.1",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -514,8 +514,11 @@ class TestUserHome:
|
|||||||
assert (home / "alice").is_dir()
|
assert (home / "alice").is_dir()
|
||||||
|
|
||||||
names = [v["name"] for v in auth_client.get("/api/vaults", headers=admin).json()]
|
names = [v["name"] for v in auth_client.get("/api/vaults", headers=admin).json()]
|
||||||
assert "home-alice" in names
|
# #194 : « * » n'inclut pas les dossiers persos — l'admin ne voit pas
|
||||||
# L'octroi atterrit bien dans le user → visible dans son JWT/liste de vaults
|
# le home d'alice (il ne verrait sinon celui de chaque utilisateur).
|
||||||
|
assert "home-alice" not in names
|
||||||
|
assert "TestVault" in names
|
||||||
|
# L'octroi atterrit bien dans le user
|
||||||
assert "home-alice" in resp.json()["vaults"]
|
assert "home-alice" in resp.json()["vaults"]
|
||||||
|
|
||||||
# Isolation : alice ne voit QUE son propre dossier
|
# Isolation : alice ne voit QUE son propre dossier
|
||||||
|
|||||||
@@ -191,6 +191,48 @@ class TestIsolation:
|
|||||||
assert check_vault_access("home-alice", get_user("bob")) is False
|
assert check_vault_access("home-alice", get_user("bob")) is False
|
||||||
assert check_vault_access("home-bob", get_user("alice")) is False
|
assert check_vault_access("home-bob", get_user("alice")) is False
|
||||||
|
|
||||||
|
def test_wildcard_admin_does_not_cover_homes(self, home_env):
|
||||||
|
"""#194 : « * » n'inclut JAMAIS un dossier perso, même pour l'admin."""
|
||||||
|
from backend.auth.middleware import check_vault_access
|
||||||
|
from backend.auth.user_store import create_user, get_user
|
||||||
|
from backend.user_home import ensure_user_home
|
||||||
|
|
||||||
|
create_user("root", "Motdepasse1!", role="admin", vaults=["*"])
|
||||||
|
_create_user("alice")
|
||||||
|
_run(ensure_user_home("alice"))
|
||||||
|
|
||||||
|
admin = get_user("root")
|
||||||
|
assert check_vault_access("home-alice", admin) is False # pas le sien
|
||||||
|
assert check_vault_access("TestVault", admin) is True # « * » intact
|
||||||
|
|
||||||
|
# L'octroi explicite suffit (y compris pour l'admin, cf. _grant)
|
||||||
|
_run(ensure_user_home("root"))
|
||||||
|
assert check_vault_access("home-root", get_user("root")) is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestSearchFiltering:
|
||||||
|
def test_search_service_filters_before_pagination(self, monkeypatch):
|
||||||
|
"""#194 : /api/search ne doit pas renvoyer les notes d'un autre user."""
|
||||||
|
import backend.search as search_mod
|
||||||
|
from backend.services import search as search_service
|
||||||
|
|
||||||
|
hits = [
|
||||||
|
{"vault": "home-bruno", "path": "secret.md"},
|
||||||
|
{"vault": "TestVault", "path": "note.md"},
|
||||||
|
{"vault": "home-bruno", "path": "autre.md"},
|
||||||
|
]
|
||||||
|
monkeypatch.setattr(
|
||||||
|
search_mod, "search",
|
||||||
|
lambda q, vault_filter="all", tag_filter=None: list(hits),
|
||||||
|
)
|
||||||
|
|
||||||
|
page = search_service.search_vaults("x", limit=50, is_allowed=lambda v: v == "TestVault")
|
||||||
|
assert page["total"] == 1, "total = résultats filtrés"
|
||||||
|
assert [r["vault"] for r in page["results"]] == ["TestVault"]
|
||||||
|
|
||||||
|
# Sans filtre (tool layer, qui filtre lui-même) : inchangé.
|
||||||
|
assert search_service.search_vaults("x", limit=50)["total"] == 3
|
||||||
|
|
||||||
|
|
||||||
class TestEnsureAll:
|
class TestEnsureAll:
|
||||||
def test_repairs_every_existing_user(self, home_env):
|
def test_repairs_every_existing_user(self, home_env):
|
||||||
|
|||||||
Reference in New Issue
Block a user