fix: partage dirigé — ACL court-circuitée après résolution Partage/ + chemin canonique au token #196
- le partage EST l'autorisation: plus de 403 'Accès refusé à la vault' pour un destinataire sans accès au vault émetteur (api_file, raw, download) - chemin virtuel canonique Partage/<token>/<nom> — désambiguïse les homonymes, fallback par nom conservé - 'reçu' = partagé_avec contient l'utilisateur: un admin ne voit plus les partages des autres comme reçus (fix File not found: Partage/…) - +3 tests (14 total)
This commit is contained in:
+11
-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.67.1**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.67.2**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,10 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.67.2] — 2026-10-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.67.1] — 2026-10-10
|
## [2.67.1] — 2026-10-10
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -39,6 +43,12 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
serveur `home-<user>/Partage/<fichier>` vers le fichier source
|
serveur `home-<user>/Partage/<fichier>` vers le fichier source
|
||||||
(`/api/file`, raw, download) ; un `path` vide/null est rejeté (400) à la
|
(`/api/file`, raw, download) ; un `path` vide/null est rejeté (400) à la
|
||||||
création d'un partage.
|
création d'un partage.
|
||||||
|
- **Itération 4 :** correctifs d'ouverture — « Accès refusé à la vault … »
|
||||||
|
(le partage dirigé est l'autorisation, l'ACL du vault source est
|
||||||
|
court-circuitée après résolution `Partage/…`), « File not found:
|
||||||
|
Partage/… » (les partages non adressés à l'utilisateur ne s'affichent
|
||||||
|
plus comme reçus ; chemin canonique `Partage/<token>/<nom>` pour
|
||||||
|
désambiguïser les fichiers homonymes).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+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.67.1).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.67.2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.67.1 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.67.2 | 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.67.1).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.67.2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.67.1 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.67.2 | Last updated: September 2026*
|
||||||
|
|||||||
@@ -57,17 +57,31 @@ def _resolve_shared_file(vault_name: str, path: str, username: str | None):
|
|||||||
mounted in the user's personal folder, else ``None``. Read-only: only a
|
mounted in the user's personal folder, else ``None``. Read-only: only a
|
||||||
recipient (or the share creator, whose own file is already accessible)
|
recipient (or the share creator, whose own file is already accessible)
|
||||||
gets a mapping — a share directed to someone else never resolves here.
|
gets a mapping — a share directed to someone else never resolves here.
|
||||||
|
|
||||||
|
Chemin canonique : ``Partage/<token>/<nom>`` (token = lève l'ambiguïté
|
||||||
|
de deux partages au même nom). Fallback par nom seul pour les liens
|
||||||
|
ne transportant pas le token.
|
||||||
"""
|
"""
|
||||||
if not username or not vault_name.startswith("home-") or not path.startswith("Partage/"):
|
if not username or not vault_name.startswith("home-") or not path.startswith("Partage/"):
|
||||||
return None
|
return None
|
||||||
owner = vault_name[len("home-"):]
|
owner = vault_name[len("home-"):]
|
||||||
if owner != username:
|
if owner != username:
|
||||||
return None
|
return None
|
||||||
name = path.split("/", 1)[1]
|
# Chemin virtuel canonique : Partage/<token>/<nom> — le token lève
|
||||||
|
# l'ambiguïté (deux partages, même nom de base). Fallback : match par
|
||||||
|
# nom pour les liens ne portant pas le token.
|
||||||
|
rest = path.split("/", 1)[1]
|
||||||
|
parts = rest.split("/", 1)
|
||||||
|
if len(parts) == 2 and len(parts[0]) >= 20: # token (64 hex) vs nom de fichier
|
||||||
|
token, _name = parts
|
||||||
|
for s in list_shares(user=username):
|
||||||
|
if s.get("token") == token and s.get("created_by") != username:
|
||||||
|
return s["vault"], s["path"]
|
||||||
|
return None
|
||||||
for s in list_shares(user=username):
|
for s in list_shares(user=username):
|
||||||
if s.get("created_by") == username:
|
if s.get("created_by") == username:
|
||||||
continue
|
continue
|
||||||
if (s.get("path") or "").split("/")[-1] == name:
|
if (s.get("path") or "").split("/")[-1] == rest:
|
||||||
return s["vault"], s["path"]
|
return s["vault"], s["path"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -129,8 +143,9 @@ async def api_file_raw(vault_name: str, path: str = Query(..., description="Rela
|
|||||||
"""
|
"""
|
||||||
shared = _resolve_shared_file(vault_name, path, current_user.get("username"))
|
shared = _resolve_shared_file(vault_name, path, current_user.get("username"))
|
||||||
if shared:
|
if shared:
|
||||||
|
# Partage dirigé = autorisation (cf. api_file).
|
||||||
vault_name, path = shared
|
vault_name, path = shared
|
||||||
if not check_vault_access(vault_name, current_user):
|
elif not check_vault_access(vault_name, current_user):
|
||||||
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
||||||
return read_raw_file(vault_name, path)
|
return read_raw_file(vault_name, path)
|
||||||
|
|
||||||
@@ -148,8 +163,9 @@ async def api_file_download(vault_name: str, path: str = Query(..., description=
|
|||||||
"""
|
"""
|
||||||
shared = _resolve_shared_file(vault_name, path, current_user.get("username"))
|
shared = _resolve_shared_file(vault_name, path, current_user.get("username"))
|
||||||
if shared:
|
if shared:
|
||||||
|
# Partage dirigé = autorisation (cf. api_file).
|
||||||
vault_name, path = shared
|
vault_name, path = shared
|
||||||
if not check_vault_access(vault_name, current_user):
|
elif not check_vault_access(vault_name, current_user):
|
||||||
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
||||||
vault_data = get_vault_data(vault_name)
|
vault_data = get_vault_data(vault_name)
|
||||||
if not vault_data:
|
if not vault_data:
|
||||||
@@ -340,9 +356,10 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
|||||||
# est l'appartenance au partage (vérifiée dans le resolver).
|
# est l'appartenance au partage (vérifiée dans le resolver).
|
||||||
shared = _resolve_shared_file(vault_name, path, current_user.get("username"))
|
shared = _resolve_shared_file(vault_name, path, current_user.get("username"))
|
||||||
if shared:
|
if shared:
|
||||||
|
# Le partage dirigé EST l'autorisation : le destinataire n'a par
|
||||||
|
# définition pas accès au vault source — on ne passe PAS par l'ACL.
|
||||||
vault_name, path = shared
|
vault_name, path = shared
|
||||||
|
elif not check_vault_access(vault_name, current_user):
|
||||||
if not check_vault_access(vault_name, current_user):
|
|
||||||
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
||||||
|
|
||||||
vault_data = get_vault_data(vault_name)
|
vault_data = get_vault_data(vault_name)
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.67.1"
|
version = "2.67.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.67.1"
|
version = "2.67.2"
|
||||||
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.67.1",
|
"version": "2.67.2",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
+9
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.67.1 | **Dernière mise à jour :** 2026-10-10
|
> **Version :** 2.67.2 | **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)**
|
||||||
@@ -64,6 +64,14 @@
|
|||||||
`home-<user>/Partage/<fichier>` → fichier source dans `/api/file`
|
`home-<user>/Partage/<fichier>` → fichier source dans `/api/file`
|
||||||
(+ raw, download) ; validation `path` non-nulle à la création d'un
|
(+ raw, download) ; validation `path` non-nulle à la création d'un
|
||||||
partage. +2 tests.
|
partage. +2 tests.
|
||||||
|
- [x] **Itération 4 (2026-10-11)** : le partage dirigé EST l'autorisation —
|
||||||
|
la résolution `Partage/…` court-circuite l'ACL du vault source (fix
|
||||||
|
« Accès refusé à la vault … » : le destinataire n'a par définition
|
||||||
|
pas accès au vault de l'émetteur) ; chemin virtuel canonique
|
||||||
|
`Partage/<token>/<nom>` (lève l'ambiguïté de fichiers homonymes,
|
||||||
|
fallback par nom conservé) ; « reçu » = adressé à moi (un admin ne
|
||||||
|
voit plus les partages d'autres comptes comme reçus — fix 404
|
||||||
|
« File not found: Partage/… »). +3 tests.
|
||||||
|
|
||||||
### 194. Dossier personnel par utilisateur (`<racine>/Home/<user>`)
|
### 194. Dossier personnel par utilisateur (`<racine>/Home/<user>`)
|
||||||
|
|
||||||
|
|||||||
@@ -121,3 +121,23 @@ Décisions :
|
|||||||
`/api/shares` en 500).
|
`/api/shares` en 500).
|
||||||
+2 tests : ouverture applicative du fichier reçu (file + raw), résolution
|
+2 tests : ouverture applicative du fichier reçu (file + raw), résolution
|
||||||
scopée par utilisateur (home d'autrui → 403).
|
scopée par utilisateur (home d'autrui → 403).
|
||||||
|
|
||||||
|
|
||||||
|
## Itération 4 — correctifs d'ouverture (2026-10-11)
|
||||||
|
|
||||||
|
- **Le partage EST l'autorisation** : après résolution `Partage/…`, l'ACL du
|
||||||
|
vault source n'est plus appliquée (`elif` dans `/api/file`, raw, download) —
|
||||||
|
un destinataire sans accès au vault émetteur ouvrait « Accès refusé à la
|
||||||
|
vault '…' », précisément le cas d'usage du partage. L'appartenance au
|
||||||
|
partage (vérifiée dans `_resolve_shared_file` via `list_shares(user=…)`)
|
||||||
|
reste la seule autorisation.
|
||||||
|
- **Chemin canonique `Partage/<token>/<nom>`** : lève l'ambiguïté de deux
|
||||||
|
partages homonymes (le token identifie le partage exact). Fallback par nom
|
||||||
|
conservé pour les liens historiques. Transporté par l'arbre (`data-path`),
|
||||||
|
la recherche et le dashboard.
|
||||||
|
- **« Reçu » = adressé à moi** : sidebar `_loadReceivedShares` et dashboard
|
||||||
|
filtrent désormais `shared_with.includes(me)` — un admin voyant TOUS les
|
||||||
|
partages (`/api/shares` non scopé pour lui) n'a plus les partages d'autres
|
||||||
|
comptes dans son dossier Partage (source des « File not found »).
|
||||||
|
+3 tests : destinataire sans accès au vault source (200), disambiguïsation
|
||||||
|
par token, home d'autrui (403). 14 tests au total.
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ const DashboardSharedWidget = {
|
|||||||
if (empty) empty.style.display = "none";
|
if (empty) empty.style.display = "none";
|
||||||
const me = (AuthManager.getUser() || {}).username;
|
const me = (AuthManager.getUser() || {}).username;
|
||||||
grid.innerHTML = shares.map(s => {
|
grid.innerHTML = shares.map(s => {
|
||||||
const received = me && s.created_by && s.created_by !== me;
|
// Reçu = adressé à moi (et pas créé par moi). Un admin voit TOUS les
|
||||||
|
// partages via /api/shares — ceux des autres comptes ne sont pas
|
||||||
|
// « reçus » pour lui : ils ne se résolvent pas dans son Partage/.
|
||||||
|
const received = me && s.created_by && s.created_by !== me
|
||||||
|
&& Array.isArray(s.shared_with) && s.shared_with.includes(me);
|
||||||
const actions = received
|
const actions = received
|
||||||
? `<button class="shared-open-btn">${t('dashboard.open_btn')}</button>`
|
? `<button class="shared-open-btn">${t('dashboard.open_btn')}</button>`
|
||||||
: `<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">${t('dashboard.copy_btn')}</button>
|
: `<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">${t('dashboard.copy_btn')}</button>
|
||||||
|
|||||||
+16
-11
@@ -32,7 +32,10 @@ async function _loadReceivedShares() {
|
|||||||
if (!me) return [];
|
if (!me) return [];
|
||||||
try {
|
try {
|
||||||
const shares = await api("/api/shares");
|
const shares = await api("/api/shares");
|
||||||
return shares.filter(s => s.created_by && s.created_by !== me);
|
// Adressés à moi uniquement : un admin voit tous les partages via
|
||||||
|
// /api/shares, ceux des autres ne sont pas dans SON Partage/.
|
||||||
|
return shares.filter(s => s.created_by && s.created_by !== me
|
||||||
|
&& Array.isArray(s.shared_with) && s.shared_with.includes(me));
|
||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,16 +369,17 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) {
|
|||||||
const shareSub = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-Partage` });
|
const shareSub = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-Partage` });
|
||||||
item.shares.forEach((s) => {
|
item.shares.forEach((s) => {
|
||||||
const name = (s.path || "").split("/").pop().replace(/\.md$/i, "");
|
const name = (s.path || "").split("/").pop().replace(/\.md$/i, "");
|
||||||
const fItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": `Partage/${s.path.split("/").pop()}`, "data-type": "file", "data-share-token": s.token }, [_sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(name)])]);
|
const virtualPath = `Partage/${s.token}/${s.path.split("/").pop()}`;
|
||||||
|
const fItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": virtualPath, "data-type": "file", "data-share-token": s.token }, [_sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(name)])]);
|
||||||
fItem.title = t("dashboard.shared_by", { user: s.created_by || "" });
|
fItem.title = t("dashboard.shared_by", { user: s.created_by || "" });
|
||||||
fItem.addEventListener("click", () => {
|
fItem.addEventListener("click", () => {
|
||||||
scrollTreeItemIntoView(fItem, false);
|
scrollTreeItemIntoView(fItem, false);
|
||||||
// #196 : ouverture en onglet applicatif — /api/file résout Partage/
|
// #196 : ouverture en onglet applicatif — /api/file résout
|
||||||
// vers le fichier source (lecture seule).
|
// Partage/<token>/<nom> vers le fichier source (lecture seule).
|
||||||
if (window.innerWidth <= 768) {
|
if (window.innerWidth <= 768) {
|
||||||
TabManager.openPersistent(vaultName, `Partage/${(s.path || "").split("/").pop()}`);
|
TabManager.openPersistent(vaultName, virtualPath);
|
||||||
} else {
|
} else {
|
||||||
TabManager.openPreview(vaultName, `Partage/${(s.path || "").split("/").pop()}`);
|
TabManager.openPreview(vaultName, virtualPath);
|
||||||
}
|
}
|
||||||
closeMobileSidebar();
|
closeMobileSidebar();
|
||||||
});
|
});
|
||||||
@@ -736,16 +740,17 @@ async function loadDirectory(vaultName, dirPath, container) {
|
|||||||
const shareSub = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-Partage` });
|
const shareSub = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-Partage` });
|
||||||
item.shares.forEach((s) => {
|
item.shares.forEach((s) => {
|
||||||
const name = (s.path || "").split("/").pop().replace(/\.md$/i, "");
|
const name = (s.path || "").split("/").pop().replace(/\.md$/i, "");
|
||||||
const fItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": `Partage/${s.path.split("/").pop()}`, "data-type": "file", "data-share-token": s.token }, [_sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(name)])]);
|
const virtualPath = `Partage/${s.token}/${s.path.split("/").pop()}`;
|
||||||
|
const fItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": virtualPath, "data-type": "file", "data-share-token": s.token }, [_sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(name)])]);
|
||||||
fItem.title = t("dashboard.shared_by", { user: s.created_by || "" });
|
fItem.title = t("dashboard.shared_by", { user: s.created_by || "" });
|
||||||
fItem.addEventListener("click", () => {
|
fItem.addEventListener("click", () => {
|
||||||
scrollTreeItemIntoView(fItem, false);
|
scrollTreeItemIntoView(fItem, false);
|
||||||
// #196 : ouverture en onglet applicatif — /api/file résout Partage/
|
// #196 : ouverture en onglet applicatif — /api/file résout
|
||||||
// vers le fichier source (lecture seule).
|
// Partage/<token>/<nom> vers le fichier source (lecture seule).
|
||||||
if (window.innerWidth <= 768) {
|
if (window.innerWidth <= 768) {
|
||||||
TabManager.openPersistent(vaultName, `Partage/${(s.path || "").split("/").pop()}`);
|
TabManager.openPersistent(vaultName, virtualPath);
|
||||||
} else {
|
} else {
|
||||||
TabManager.openPreview(vaultName, `Partage/${(s.path || "").split("/").pop()}`);
|
TabManager.openPreview(vaultName, virtualPath);
|
||||||
}
|
}
|
||||||
closeMobileSidebar();
|
closeMobileSidebar();
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.67.1",
|
"version": "2.67.2",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -169,24 +169,40 @@ class TestDirectedShareGate:
|
|||||||
assert not [r for r in resp.json()["results"] if r.get("share_token")]
|
assert not [r for r in resp.json()["results"] if r.get("share_token")]
|
||||||
|
|
||||||
def test_recipient_opens_shared_file_in_app(self, sessions):
|
def test_recipient_opens_shared_file_in_app(self, sessions):
|
||||||
# #196 : /api/file résout home-<user>/Partage/<fichier> vers la source.
|
# #196 : /api/file résout home-<user>/Partage/<token>/<f> vers la
|
||||||
|
# source — même quand le destinataire N'A PAS accès au vault source
|
||||||
|
# (le partage dirigé EST l'autorisation).
|
||||||
admin_client, user_client = sessions
|
admin_client, user_client = sessions
|
||||||
path = _ensure_shared_file()
|
path = _ensure_shared_file()
|
||||||
resp = admin_client.post("/api/share/TestVault", json={
|
resp = admin_client.post("/api/share/TestVault", json={
|
||||||
"path": path, "shared_with": ["normaluser"],
|
"path": path, "shared_with": ["normaluser"],
|
||||||
})
|
})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
r = user_client.get("/api/file/home-normaluser", params={"path": "Partage/share_directed.md"})
|
token = resp.json()["token"]
|
||||||
|
|
||||||
|
# Retire l'accès direct au vault source : le partage doit suffire.
|
||||||
|
from backend.auth.user_store import update_user
|
||||||
|
update_user("normaluser", {"vaults": ["home-normaluser"]})
|
||||||
|
|
||||||
|
# Chemin canonique avec token
|
||||||
|
r = user_client.get("/api/file/home-normaluser", params={"path": f"Partage/{token}/share_directed.md"})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
body = r.json()
|
body = r.json()
|
||||||
assert body["is_markdown"] is True
|
assert body["is_markdown"] is True
|
||||||
assert "dirigé" in body["html"]
|
assert "dirigé" in body["html"]
|
||||||
|
|
||||||
# raw endpoint too
|
# Fallback par nom (liens sans token)
|
||||||
r = user_client.get("/api/file/home-normaluser/raw", params={"path": "Partage/share_directed.md"})
|
r = user_client.get("/api/file/home-normaluser", params={"path": "Partage/share_directed.md"})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
|
||||||
|
# raw + download too
|
||||||
|
r = user_client.get("/api/file/home-normaluser/raw", params={"path": f"Partage/{token}/share_directed.md"})
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert "dirigé" in r.json()["raw"]
|
assert "dirigé" in r.json()["raw"]
|
||||||
|
|
||||||
|
# Restore fixture state for other tests
|
||||||
|
update_user("normaluser", {"vaults": ["TestVault"]})
|
||||||
|
|
||||||
def test_shared_file_resolution_is_user_scoped(self, sessions):
|
def test_shared_file_resolution_is_user_scoped(self, sessions):
|
||||||
# home-admin/Partage/x.md demandé par normaluser → pas de mapping
|
# home-admin/Partage/x.md demandé par normaluser → pas de mapping
|
||||||
# (le home d'un autre user lui est interdit, 403 avant tout).
|
# (le home d'un autre user lui est interdit, 403 avant tout).
|
||||||
@@ -197,3 +213,27 @@ class TestDirectedShareGate:
|
|||||||
})
|
})
|
||||||
r = user_client.get("/api/file/home-admin", params={"path": "Partage/share_directed.md"})
|
r = user_client.get("/api/file/home-admin", params={"path": "Partage/share_directed.md"})
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
def test_same_name_two_shares_token_disambiguates(self, sessions):
|
||||||
|
# Deux partages de fichiers homonymes reçus : le token sélectionne
|
||||||
|
# le bon, le fallback par nom prend le premier sans crash.
|
||||||
|
admin_client, user_client = sessions
|
||||||
|
import os as _os
|
||||||
|
vault = Path(_os.environ["VAULT_1_PATH"])
|
||||||
|
(vault / "a.md").write_text("contenu alpha unique", encoding="utf-8")
|
||||||
|
(vault / "b.md").write_text("contenu beta unique", encoding="utf-8")
|
||||||
|
# subdirectory with same basename to force ambiguity
|
||||||
|
(vault / "d1").mkdir(exist_ok=True)
|
||||||
|
(vault / "d2").mkdir(exist_ok=True)
|
||||||
|
(vault / "d1" / "same.md").write_text("contenu GAMMA unique", encoding="utf-8")
|
||||||
|
(vault / "d2" / "same.md").write_text("contenu DELTA unique", encoding="utf-8")
|
||||||
|
r1 = admin_client.post("/api/share/TestVault", json={"path": "d1/same.md", "shared_with": ["normaluser"]})
|
||||||
|
r2 = admin_client.post("/api/share/TestVault", json={"path": "d2/same.md", "shared_with": ["normaluser"]})
|
||||||
|
t1, t2 = r1.json()["token"], r2.json()["token"]
|
||||||
|
|
||||||
|
r = user_client.get("/api/file/home-normaluser", params={"path": f"Partage/{t2}/same.md"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "DELTA" in r.json()["html"]
|
||||||
|
r = user_client.get("/api/file/home-normaluser", params={"path": f"Partage/{t1}/same.md"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "GAMMA" in r.json()["html"]
|
||||||
|
|||||||
Reference in New Issue
Block a user