feat: partage dirigé entre utilisateurs #196
- create_share/shared_with + validation des destinataires
- gate auth sur /s/{token} (+pdf, raw) : 404 opaque hors créateur/admin/destinataires
- GET /api/shares scopé (non-admin = créés + reçus), révocation créateur/admin
- UI : dialogue dirigé/public + destinataires, dashboard « partagé par X »
- i18n FR/EN, tests test_directed_shares.py (9), fiche feature
This commit is contained in:
+20
-1
@@ -1,6 +1,6 @@
|
||||
# ObsiGate — Roadmap
|
||||
|
||||
> **Version :** 2.65.2 | **Dernière mise à jour :** 2026-10-10
|
||||
> **Version :** 2.66.0 | **Dernière mise à jour :** 2026-10-10
|
||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||
> vers les fonctionnalités livrées.
|
||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||
@@ -30,6 +30,25 @@
|
||||
|
||||
## 🔵 En cours
|
||||
|
||||
### 196. Partage dirigé entre utilisateurs (document → utilisateur)
|
||||
|
||||
- **Effort :** 0,5-1 jour | **Impact :** 🟡 | **Ouvert :** 2026-10-10 | **Statut :** ✅ livré le 2026-10-10
|
||||
- **Fiche :** [features/directed-shares-196.md](./features/directed-shares-196.md)
|
||||
- **Description :** un utilisateur peut partager un document à un ou plusieurs
|
||||
autres comptes (pas un lien public) : les pages `/s/{token}` exigent alors
|
||||
une session et n'acceptent que créateur + admin + destinataires (404 sinon,
|
||||
pas de fuite d'existence). Vue « partagé avec moi » dans le dashboard,
|
||||
destinataires listés dans le dialogue de partage, révocabilité réservée au
|
||||
créateur/admin, `GET /api/shares` scopé (un non-admin ne voit que ses
|
||||
partages créés/reçus — corrige une fuite préexistante). Rétrocompatible :
|
||||
les partages publics existants restent anonymes.
|
||||
- **Sous-tâches :**
|
||||
- [x] Store `shared_with` + validation des destinataires à la création
|
||||
- [x] Gate auth+appartenance sur `/s/{token}` (+`/pdf`, `/raw`) — 404 opaque
|
||||
- [x] Scope de `GET /api/shares` ; révocation créateur/admin uniquement
|
||||
- [x] UI : dialogue dirigé/public + destinataires, dashboard « partagé par X »
|
||||
- [x] i18n FR/EN, tests `test_directed_shares.py` (9), fiche feature
|
||||
|
||||
### 194. Dossier personnel par utilisateur (`<racine>/Home/<user>`)
|
||||
|
||||
- **Effort :** 1-2 jours | **Impact :** 🟡 | **Ouvert :** 2026-10-09 | **Statut :** 🔵 en cours
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# #196 — Partage dirigé entre utilisateurs
|
||||
|
||||
> **Statut :** livré | **Ouvert :** 2026-10-10 | **Type :** feature (sharing + auth)
|
||||
|
||||
## Problème
|
||||
|
||||
Le seul partage existant (#85) était un lien **public** : quiconque possède
|
||||
l'URL lit le document, sans compte ni session. Il n'existait aucun moyen de
|
||||
partager un document *à un utilisateur précis* de la plateforme — un
|
||||
utilisateur ne pouvait ni adresser un document à un autre, ni consulter les
|
||||
documents qu'on lui adresse, ni (côté créateur) retirer ce partage.
|
||||
|
||||
## Décision d'architecture
|
||||
|
||||
Deux formes étaient possibles :
|
||||
|
||||
1. **Réutiliser le partage par token existant, dirigé** (retenu) — le record
|
||||
`data/shares.json` gagne `shared_with: [usernames]`. Si la liste est non
|
||||
vide, les pages `/s/{token}` (et `/pdf`, `/raw`) exigent une session et
|
||||
n'acceptent que créateur + admin + destinataires. Vide/absent : le
|
||||
comportement public historique est inchangé (rétrocompatibilité totale,
|
||||
zéro migration — les shares existants n'ont pas le champ).
|
||||
2. Octroyer le vault entier au destinataire (`update_user {"vaults": …}`)
|
||||
(écartée) — accès lecture-**écriture** au vault complet au lieu d'un
|
||||
document en lecture seule ; c'est le modèle « collaborateur de vault »,
|
||||
pas « partager un document ». Reste la bonne réponse pour ce besoin-là.
|
||||
|
||||
Points de sécurité délibérés :
|
||||
|
||||
- **404, jamais 403/401, sur la gate** — un visiteur sans session (ou un
|
||||
utilisateur non destinataire) apprend le même « Share not found or
|
||||
expired » qu'un token inventé : l'existence d'un token dirigé n'est pas
|
||||
confirmable de l'extérieur.
|
||||
- **`GET /api/shares` scopé** (correctif au passage) — un non-admin ne voit
|
||||
que les partages qu'il a créés **ou reçus`** (avant : tout le monde voyait
|
||||
tous les partages, fuite préexistante devenue intenable avec du user-to-user).
|
||||
Un admin voit tout.
|
||||
- **Révocation réservée au créateur/admin** — un destinataire ne peut pas
|
||||
casser l'accès des autres.
|
||||
- Les **destinataires sont validés** à la création (`get_user`) : pas de lien
|
||||
mort vers un compte inexistant.
|
||||
|
||||
## Mise en place
|
||||
|
||||
| Élément | Où |
|
||||
|---|---|
|
||||
| Store : champ `shared_with`, param de `create_share` | `backend/share.py` |
|
||||
| Gate `_gate_share()` + branchement `/s/{token}`, `/s/{token}/pdf`, `/s/{token}/raw` | `backend/routers/sharing.py` |
|
||||
| Scope de `GET /api/shares` (non-admin = créés + reçus) | `backend/routers/sharing.py`, `backend/share.py::list_shares(user=…)` |
|
||||
| Révocabilité créateur/admin uniquement | `backend/routers/sharing.py::api_share_revoke` |
|
||||
| Validation des destinataires à la création | `backend/routers/sharing.py::api_share_create` |
|
||||
| `ShareModel.shared_with` | `backend/schemas.py` |
|
||||
| Dialogue : radio public/dirigé + champ destinataires (virgules) | `frontend/js/config.js::openShareDialog` |
|
||||
| Destinataires affichés dans la vue « déjà partagé » | `frontend/js/config.js` |
|
||||
| Dashboard : cartes reçues (« Partagé par X », ouverture `/s/`, pas de bouton révoquer) | `frontend/js/dashboard.js::DashboardSharedWidget` |
|
||||
| i18n FR/EN (6 clés config.* + dashboard.shared_by) | `frontend/locales/{fr,en}.json` |
|
||||
|
||||
Ce qui est gratuit par construction : lecture seule (le destinataire ne
|
||||
passe jamais par l'éditeur), expiration, compteur d'accès, suivi des
|
||||
renommages (`update_shares_after_rename`), masquage des secrets
|
||||
(`redact_file_content` déjà dans les 3 routes `/s/*`), isolation des
|
||||
dossiers perso #194 (le destinataire ne voit jamais l'arbre du vault de
|
||||
l'auteur, seulement le document rendu).
|
||||
|
||||
## Tests
|
||||
|
||||
`tests/test_directed_shares.py` (9) : création dirigée, destinataire inconnu
|
||||
→ 400, anonyme → 404 / destinataire + créateur → 200, non-destinataire →
|
||||
404, lien public toujours anonyme, scope de `/api/shares`, destinataire ne
|
||||
peut pas révoquer (403), créateur révoque, la révocation coupe l'accès.
|
||||
|
||||
## Limites connues
|
||||
|
||||
- `shared_with` est une liste plate d'usernames — pas de groupes. Ajouter
|
||||
des groupes quand le besoin sera réel.
|
||||
- Pas de notification push : le destinataire découvre les partages reçus au
|
||||
rechargement du dashboard (badge/rafraîchissement suffisent ; SSE existe
|
||||
si un vrai push est demandé).
|
||||
- La date d'« expiration » reste globale au partage (pas par destinataire).
|
||||
Reference in New Issue
Block a user