diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e2fe4..1026dc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.85.1**. +> [Unreleased](#unreleased). La dernière version livrée est **2.86.0**. --- @@ -14,6 +14,12 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.86.0] — 2026-11-06 + +- **BUG-122 — Déconnexion globale navigable : `GET /sso/logout`** (fusion #141) : le bouton « Déconnexion » du menu compte de la barre commune ne faisait rien dans les applications sœurs — la barre relaie l'action à l'hôte, et la déconnexion globale d'ObsiGate (ACC-3.1, #216) n'était appelable qu'en same-origin depuis l'interface d'ObsiGate (`POST /api/auth/logout`). Nouvelle route `GET /sso/logout?return_to=`, symétrique de `/sso/authorize` : elle exécute la déconnexion globale — logique extraite dans `apply_global_logout()` (`backend/auth/router.py`), désormais partagée par les deux entrées (jetons natifs refresh/access révoqués, `jti` du cookie `draco_session` révoqué, cookie effacé sur le domaine parent) — puis redirige vers le `return_to` validé par la liste blanche existante (`validate_return_to` : https + `*.dracodev.net`, repli « / »). Idempotente sans session. Les applications sœurs branchent leur action `deconnexion` sur cette route (leur session locale est détruite d'abord chez celles qui en ont une). Tests : `tests/test_draco_session.py` (+3 : révocation et redirection, liste blanche, idempotence). Fiche : `docs/features/deconnexion-globale-bug-122.md`. + +--- + ## [2.85.1] — 2026-11-06 - **HUB-19 — Propagation des rôles du Hub à ObsiGate (suite du pilote ShaarIt, fusion Hub v2 §2.2, décision D3(a), #228)** : ObsiGate, à la fois émettrice et consommatrice du SSO, consomme à son tour le registre des rôles du Hub. Nouveau client `backend/hub_roles.py` : à l'ouverture de session draco (validation locale `draco_session.user_for_draco`), consultation de `GET {hub}/api/roles/obsigate` (en-tête Cookie complet relayé, `Accept: application/json`, délai 2 s), cache mémoire 60 s par `sub` sur le patron du pilote (horodatage posé avant l'appel : un échec conserve l'ancien cache, pas de rafale) ; URL du Hub résolue comme la barre commune (`draco_config.app_url("hub")` : `DRACO_HUB_URL` explicite, dérivation par l'hôte sœur, défaut officiel — jamais d'URL dev figée). **Effets, sessions établies par draco seulement** : niveau connu `aucun` → session draco refusée, `require_auth` répond 403 avec un message explicite en français (l'authentification native d'ObsiGate — l'émettrice, son accès propre est le secours — n'est jamais touchée) ; routes d'administration `/api/auth/admin/*` : nouvelle garde `require_admin_hub` — canal draco + rôle connu ⇒ niveau `administrateur` du registre exigé, sessions natives et rôle inconnu (Hub injoignable ou en erreur) ⇒ règles actuelles inchangées, aucun verrouillage nouveau, et le registre n'élève aucun privilège local. Validation locale du jeton, émission, révocations et renouvellement inchangés ; aucun rôle dans le jeton. Aucun impact frontend (pas d'i18n). Fiche : `docs/features/hub-roles-obsigate-228.md`. Tests : `tests/test_hub_roles.py` (client : parsing/cache/repli/résolution d'URL ; intégration : refus draco, garde admin). **Hypothèse de version** : branche depuis le main 2.84.1, cible 2.85.1 — rebasée sur la 2.85.0 (PR « API sessions HUB-22 » fusionnée d'abord) avant fusion. diff --git a/README.fr.md b/README.fr.md index b735f89..ed82e32 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.85.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.86.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.85.1). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.86.0). --- -*Projet : ObsiGate | Version : 2.85.1 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.86.0 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 4bbc524..146d194 100644 --- a/README.md +++ b/README.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.85.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.86.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.85.1). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.86.0). --- -*Project: ObsiGate | Version: 2.85.1 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.86.0 | Last updated: September 2026* diff --git a/VERSION b/VERSION index d7441bf..c994171 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.85.1 +2.86.0 diff --git a/backend/auth/router.py b/backend/auth/router.py index 339d56e..61af12f 100644 --- a/backend/auth/router.py +++ b/backend/auth/router.py @@ -375,12 +375,15 @@ async def refresh_token_endpoint(request: Request, response: Response): } -@router.post("/logout") -async def logout( - request: Request, - response: Response, -): - """Logout: revoke refresh and access tokens, then delete cookies.""" +def apply_global_logout(request: Request, response: Response) -> None: + """Déconnexion globale (Fusion ACC-3.1, #216) appliquée à ``response``. + + Révoque les jetons natifs (refresh + access, BUG-027), révoque le + ``jti`` du cookie partagé ``draco_session`` et efface les cookies. + Partagée entre ``POST /api/auth/logout`` (réponse JSON) et + ``GET /sso/logout`` (redirection, BUG-122) : les deux entrées + terminent exactement la même session, celle de la suite entière. + """ refresh_tok = request.cookies.get("refresh_token") if refresh_tok: payload = decode_token(refresh_tok) @@ -421,6 +424,15 @@ async def logout( response.delete_cookie("refresh_token", path="/api/auth/refresh") response.delete_cookie("access_token", path="/") response.delete_cookie("access_token", path="/api") # just in case + + +@router.post("/logout") +async def logout( + request: Request, + response: Response, +): + """Logout: revoke refresh and access tokens, then delete cookies.""" + apply_global_logout(request, response) return {"message": "Deconnecte avec succes"} diff --git a/backend/routers/sso.py b/backend/routers/sso.py index 7e9cfc1..cf2ff7f 100644 --- a/backend/routers/sso.py +++ b/backend/routers/sso.py @@ -11,6 +11,14 @@ # GET /api/sso/revocations : liste des jti révoqués, réservée aux # autres applications (clé machine DRACO_SSO_MACHINE_KEY, en-tête # X-Draco-Machine-Key) qui la mettent en cache 60 s (design §5.2). +# +# GET /sso/logout?return_to= : symétrique de /sso/authorize — +# point d'entrée navigable de la déconnexion globale (BUG-122, +# fusion #141). Les autres applications y naviguent depuis leur menu +# compte ; la déconnexion exécutée est celle de POST /api/auth/logout +# (logique partagée : jetons natifs révoqués, jti draco révoqué, +# cookie effacé sur le domaine parent), puis redirection vers le +# return_to validé (même liste blanche ; repli « / »). from __future__ import annotations @@ -21,6 +29,7 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import RedirectResponse from backend import draco_session +from backend.auth.router import apply_global_logout router = APIRouter(tags=["SSO"]) @@ -34,6 +43,14 @@ async def sso_authorize(return_to: str = ""): return RedirectResponse(url="/", status_code=302) +@router.get("/sso/logout") +async def sso_logout(request: Request, return_to: str = ""): + target = draco_session.validate_return_to(return_to) + response = RedirectResponse(url=target or "/", status_code=302) + apply_global_logout(request, response) + return response + + @router.get("/api/sso/revocations") async def sso_revocations(request: Request): machine_key = os.environ.get("DRACO_SSO_MACHINE_KEY", "") diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 666d12f..529be6c 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.85.1" +version = "2.86.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 329b2c1..683d432 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.85.1" +version = "2.86.0" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 12ae52f..e40f5a5 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.85.1", + "version": "2.86.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 4e0fb2d..4d47a4a 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -227,6 +227,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-117* | Drag & drop des fichiers dans la sidebar : le déplacement d'un répertoire à un autre ne fait plus rien, sans message | 🟢 corrigé | P1 | 📱 frontend / sidebar | IA | `frontend/js/dragdrop.js` (`onDrop`) | Glisser un fichier sur un dossier : aucune requête `/api/move`, aucun toast | La détection `isExternalFiles` dans `onDrop` (`items.length > 0`, ajoutée par #89) comptait aussi les formats string du drag interne (`tree:vault/path/type` posés par `setData` au `dragstart`) : tout déplacement partait dans la branche upload où rien ne passe le filtre `kind === 'file'` → drop avalé silencieusement. Corrigé à la racine : `dragState` (interne) prioritaire + items réellement fichiers. Livré avec #201 : menu contextuel Déplacer/Dupliquer réutilisant `promptTargetFolder`, emplacement actuel pré-sélectionné | Vérifié Playwright instance E2E : glisser `note1.md` → `d1` = toast « Moved file to d1 » + chemin déplacé ; 4 tests `TestDuplicateApi` | | *BUG-119* | Panneau Spark : le « mode console » (surimpression iframe de la console du service d'agents) est bloqué par le navigateur (« This content is blocked ») | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/main.py` (`SecurityHeadersMiddleware`) | Panneau Spark → « Console ⤢ » : l'iframe reste sur la page d'erreur du navigateur | `frame-src 'self' blob: https://agent.dracodev.net` dans la CSP servie ; test `tests/test_security_headers.py::test_csp_frame_src_allows_agent_console` | Cause : `frame-src` n'autorisait pas l'origine du service d'agents, pourtant déjà présente dans `connect-src` (le panneau texte fonctionnait). Aucune autre directive modifiée. Redéploiement Docker requis | | *BUG-120* | Menu inter-apps : sur l'environnement dev, les URL sœurs servies (`window.DRACO_URLS` : ObsiGate, Flowdeck, TikTak, Kanbante) restent en noms **prod** alors que l'URL de l'agent est bien en `-dev` | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/draco_config.py` (`app_url`, `_suffixe_environnement`) | Sur og-dev : `window.DRACO_URLS` donne `agent: https://agent-dev.dracodev.net` mais `tiktak: https://tiktak.dracodev.net` (prod) — le `.env` dev ne déclare pas les quatre `DRACO__URL` | `backend/draco_config.py` : résolution sœur en 3 temps — explicite, puis **dérivation d'environnement** depuis l'hôte de `agent_url()` (`agent.dracodev.net` → `https://.dracodev.net`, slugs `og`/`flowdeck`/`tiktak`/`kanbante` ; hors motif → rien), puis défaut officiel ; Gitea exclue. Tests `tests/test_draco_config.py` (+9 : dérivation `-dev` via `AGENT_EVENEMENTS_URL` et via `DRACO_AGENT_URL`, priorité de l'explicite, 5 hôtes hors motif, suffixe vide, injection) | Suite de #223 : seule l'URL de l'agent avait une dérivation de repli (`AGENT_EVENEMENTS_URL`) ; les sœurs retombaient sur les défauts officiels. Correctif de code demandé par Bruno : l'environnement se déduit de l'agent déjà configuré, sans variable supplémentaire. Redéploiement Docker requis | +| *BUG-122* | Déconnexion du menu compte inerte dans les applications sœurs : la barre commune relaie `deconnexion` à l'hôte, mais la déconnexion globale d'ObsiGate (ACC-3.1) n'est appelable qu'en same-origin (`POST /api/auth/logout`) — aucun point d'entrée navigable pour les autres hôtes | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/routers/sso.py` (`GET /sso/logout`), `backend/auth/router.py` (`apply_global_logout`) | Depuis la console Spark (ou Flowdeck/TikTak/ShaarIt/Hub) : menu compte → « Déconnexion » → rien ne se passe, la session draco reste vivante (vérifié en navigateur réel : cookie présent, `/api/v1/me` toujours 200) | Nouvelle route `GET /sso/logout?return_to=` : exécute la déconnexion globale (logique partagée `apply_global_logout` : jetons natifs refresh/access révoqués, `jti` draco révoqué, cookie effacé sur le domaine parent) puis redirige vers le `return_to` validé (liste blanche https + *.dracodev.net, repli `/`). Tests `tests/test_draco_session.py` (+3) | Signalé par Bruno le 2026-11-06 (capture console), carte fusion #141 ; les hôtes branchent leur action `deconnexion` sur cette route (PR sœurs). BUG-121 reste réservé au bug de couleur « Secrets » du vault Spark (proposé, non ordonné). Redéploiement Docker requis | | # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes | |---|---|---|---|---|---|---|---|---|---| @@ -361,6 +362,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 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-` 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) | | 2026-10-23 | BUG-119 | Correction | `backend/main.py`, `tests/test_security_headers.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md` | **BUG-119 — le « mode console » du panneau Spark V3 (#222) était bloqué par le navigateur** (« This content is blocked ») : la console du service d'agents s'affiche dans une iframe `https://agent.dracodev.net`, mais la CSP servie par `SecurityHeadersMiddleware` limitait `frame-src` à `'self' blob:` — seul le panneau texte passait (`connect-src` autorisait déjà l'origine). Correctif strict : `frame-src 'self' blob: https://agent.dracodev.net`, toutes les autres directives inchangées à l'identique. Test de non-régression `test_csp_frame_src_allows_agent_console` (la CSP servie est analysée et la liste exacte des sources `frame-src` verrouillée). Vérifié : 100 passed (security_headers + csp_nonce + security_hardening + draco_barre + version), ruff propre sur les fichiers touchés (51 erreurs préexistantes ailleurs, inchangées), mypy 0 (120 fichiers) | 🟢 corrigé (en attente vérif utilisateur) | | 2026-10-25 | BUG-120 (suite #223) | Correction | `backend/draco_config.py`, `tests/test_draco_config.py`, `.env.example`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md`, `docs/features/devprod-urls-config-223.md` | **BUG-120 — le menu inter-apps de la dev pointait la prod** : `window.DRACO_URLS` servait l'agent en `-dev` mais les quatre sœurs en noms officiels, le `.env` dev ne déclarant pas les `DRACO__URL`. Correctif dans `draco_config.app_url()` : après l'explicite, **dérivation d'environnement** — hôte de `agent_url()` de la forme `agent.dracodev.net` sans port → sœur en `.dracodev.net` ; hors motif (autre domaine, LAN, IP, port) aucune dérivation ; Gitea jamais dérivée. Vérifié : `test_draco_config.py` 16 passed, suite complète **1668 passed / 9 skipped**, ruff 0 et mypy 0 sur les fichiers touchés (51 erreurs ruff préexistantes ailleurs, inchangées) | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-11-06 | BUG-122 (fusion #141) | Correction | `backend/routers/sso.py`, `backend/auth/router.py`, `tests/test_draco_session.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/features/deconnexion-globale-bug-122.md`, `VERSION` (+ dérivés) | **BUG-122 — la déconnexion du menu compte ne terminait rien hors ObsiGate** : ajout de `GET /sso/logout`, point d'entrée navigable de la déconnexion globale (logique partagée avec `POST /api/auth/logout`). Vérifié : `test_draco_session.py` 34 passed, ruff 0 et mypy 0 sur les fichiers touchés | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3ab53a0..2fa3bf8 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.85.1 | **Dernière mise à jour :** 2026-11-06 +> **Version :** 2.86.0 | **Dernière mise à jour :** 2026-11-06 > **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)** diff --git a/docs/features/deconnexion-globale-bug-122.md b/docs/features/deconnexion-globale-bug-122.md new file mode 100644 index 0000000..4e384e6 --- /dev/null +++ b/docs/features/deconnexion-globale-bug-122.md @@ -0,0 +1,56 @@ +# BUG-122 — Déconnexion globale navigable : `GET /sso/logout` + +**Carte :** fusion #141 (signalement de Bruno, 2026-11-06, capture +depuis la console Spark). **Version :** 2.86.0. **Prérequis :** ACC-3.1 +(#216) — la déconnexion globale existe déjà dans +`POST /api/auth/logout`. + +## Constat + +Le bouton « Déconnexion » du menu compte de la barre commune +(dracodev-commons) est une **action relayée à l'hôte** (`cfg.onAction`, +sinon événement `draco:bar`) : la barre ne déconnecte jamais +elle-même. Or : + +- console Spark, ObsiGate, Flowdeck, TikTak, ShaarIt et Hub ne + branchaient **aucune** action `deconnexion` (clic sans effet, + reproduit en navigateur réel sur la console : session draco intacte + après le clic) ; +- Kanbante, seul branché, ne détruisait que sa session locale — la + session SSO survivait ; +- et de toute façon, aucun hôte n'aurait pu terminer la session + globale : `POST /api/auth/logout` n'est appelable qu'en same-origin + depuis l'interface d'ObsiGate. Il manquait le point d'entrée + navigable, symétrique de `GET /sso/authorize`. + +## Livraison (cette PR, côté émettrice) + +| Méthode | Chemin | Effet | +|---|---|---| +| GET | `/sso/logout?return_to=` | Déconnexion globale, puis redirection 302 vers le `return_to` validé (repli « / ») | + +- La logique de `POST /api/auth/logout` est extraite dans + `apply_global_logout(request, response)` (`backend/auth/router.py`) + et partagée par les deux entrées : révocation des jetons natifs + refresh/access, révocation du `jti` du cookie `draco_session`, + effacement des cookies (dont `draco_session` sur le domaine parent). +- `return_to` passe par la liste blanche existante + (`draco_session.validate_return_to` : https + `dracodev.net` ou + sous-domaine) — aucun relais ouvert. +- Idempotente : sans session, la route redirige quand même (les hôtes + y naviguent même si leur session a déjà expiré). + +## Côté hôtes (PR sœurs, carte fusion #141) + +Chaque hôte branche son action `deconnexion` : destruction de sa +session locale d'abord (Flowdeck, TikTak, Kanbante, ShaarIt), puis +navigation vers `<émettrice>/sso/logout?return_to=`. La +console et le Hub (session draco seule) naviguent directement. +Après le parcours complet, « Déconnexion » termine **la session de la +suite entière** et la session locale de l'hôte courant. + +## Tests + +`tests/test_draco_session.py` (+3) : révocation du `jti` + redirection +vers le `return_to` + cookie effacé (`Max-Age=0`), liste blanche +(relais refusé, repli « / »), idempotence sans session. diff --git a/package.json b/package.json index 6e28085..7d90d49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.85.1", + "version": "2.86.0", "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", "directories": { diff --git a/tests/test_draco_session.py b/tests/test_draco_session.py index 7eeca91..813ed35 100644 --- a/tests/test_draco_session.py +++ b/tests/test_draco_session.py @@ -189,6 +189,44 @@ def test_sso_authorize_liste_blanche(client): assert r.headers["location"] == "/" # refusé, pas de relais +# ── Déconnexion globale navigable (BUG-122, fusion #141) ──────────────── + +def test_sso_logout_revoque_et_redirige(admin_client): + token = _token_depuis_login(admin_client) + assert draco_session.verify_token(token) + r = admin_client.get( + "/sso/logout", + params={"return_to": "https://tiktak.dracodev.net/"}, + headers={"Cookie": f"draco_session={token}"}, + follow_redirects=False) + assert r.status_code == 302 + assert r.headers["location"] == "https://tiktak.dracodev.net/" + assert draco_session.verify_token(token) is None # jti révoqué + cookie = _draco_cookie(r) # cookie effacé + assert cookie is not None and "Max-Age=0" in cookie + + +def test_sso_logout_liste_blanche(admin_client): + r = admin_client.get("/sso/logout", + params={"return_to": "https://evil.com/phish"}, + follow_redirects=False) + assert r.status_code == 302 + assert r.headers["location"] == "/" # refusé, pas de relais + r = admin_client.get("/sso/logout", follow_redirects=False) + assert r.status_code == 302 + assert r.headers["location"] == "/" # sans return_to + + +def test_sso_logout_sans_session_idempotent(client): + # Aucun cookie : la route reste une redirection propre (les autres + # applications y naviguent même si la session a déjà expiré). + r = client.get("/sso/logout", + params={"return_to": "https://hub.dracodev.net/"}, + follow_redirects=False) + assert r.status_code == 302 + assert r.headers["location"] == "https://hub.dracodev.net/" + + # ── Validation locale (ACC-3.2, #9) ────────────────────────────────────── def test_me_avec_cookie_draco_seul(admin_client):