From 7be96f0618bc6c3fd7346a46f32ed20ae7ab2e21 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 1 Oct 2026 10:01:39 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20A29=20+=20A42(partiel)=20=E2=80=94=20pub?= =?UTF-8?q?lish=20partag=C3=A9,=20fuite=20password=5Fhash,=20data=5Fdir=20?= =?UTF-8?q?(v7.5.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A29 — `app/services/publish.py` : slugify titré unique (fallback aléatoire), 404 si la page n'existe pas, événements centralisés. Les 3 paires publish/unpublish déléguent (sharing = front, board, v2) : · board : mise à jour aveugle → 404 + contrôle de session ajouté · board : perd `share_mode='anyone'` en bonus, v2 : perd `is_shared=1` — le share dialog reste l'unique propriétaire de ces drapeaux · v2 : slug fourni conservé, slug vidé aussi à la dépublication (avant : laissé) · `/users/me` ×2 et listings collections ×3 = contrats versionnés distincts, décision documentée (on garde) - Byproduct sécurité — `GET /api/users/me` (v1) et le contexte de `/accounts` faisaient `SELECT *` sur users → password_hash / login_attempts / locked_until exposés → colonnes whitelistées (liste v2) - A42 (partiel) — 9 copies de `Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))` → `settings.data_dir` (property : lecture à chaque accès, les tests monkeypatchent l'env) ; cache Gitea : évacuation des entrées expirées à chaque écriture. Reste : client httpx partagé (52 créations, cache par event loop) tests : test_publish_service_shared_and_safe, test_users_me_no_secret_columns, test_gitea_cache_evicts_expired suite **1034/1034** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.5.0 docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour --- CHANGELOG.md | 31 ++++++++++ ROADMAP.md | 6 +- VERSION | 2 +- WORKLOAD.md | 2 +- app/config.py | 10 ++++ app/main.py | 2 +- app/routers/api.py | 7 ++- app/routers/api_v2.py | 23 ++------ app/routers/board.py | 38 ++++--------- app/routers/dashboard.py | 18 +++--- app/routers/emoji.py | 4 +- app/routers/sharing.py | 63 ++------------------- app/services/export.py | 4 +- app/services/gitea_client.py | 5 +- app/services/importers/pipeline.py | 4 +- app/services/meetings.py | 3 +- app/services/publish.py | 91 ++++++++++++++++++++++++++++++ docs/openapi-v2.json | 2 +- tests/test_audit_p0_fixes.py | 69 ++++++++++++++++++++++ 19 files changed, 258 insertions(+), 126 deletions(-) create mode 100644 app/services/publish.py diff --git a/CHANGELOG.md b/CHANGELOG.md index da31a1e..4586da2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog - FlowDeck +## v7.5.0 (2026-10-01) — Audit : A29, A42 (partiel) + +### Changed + +- **A29** — `services/publish.py` partagé : les 3 paires publish/unpublish + (sharing = front, board, v2) déléguent ; 404 partout (board faisait une + mise à jour aveugle), slugify titré unique (board : aléatoire ; v2 : slug + fourni conservé), événements centralisés, board gagne le contrôle de session. + Les bonus divergents disparaissent (`share_mode='anyone'` pour board, + `is_shared=1` pour v2) : le share dialog reste l'unique propriétaire de ces + drapeaux, dépublier ne révoque donc pas un partage manuel. Les listings + `/users/me` ×2 et collections ×3 restent : contrats versionnés distincts +- **A42** — 9 copies de `Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))` → + `settings.data_dir` (property : lecture à chaque accès, les tests + monkeypatchent l'env) ; cache Gitea : évacuation des entrées expirées à + chaque écriture (il ne pouvait que grandir) ; les 29 `Environment(...)` + étaient déjà couverts par A10. **Reste** : client httpx partagé (52 créations, + à faire avec un cache par event loop) + +### Security + +- **Byproduct A29** — `GET /api/users/me` (v1) et le contexte Jinja de + `/accounts` renvoyaient `SELECT *` sur `users` : **password_hash**, + `login_attempts` et `locked_until` exposés → colonnes whitelistées + (identiques à la liste v2) + +### Tests + +- `test_publish_service_shared_and_safe` (slug, 404, partage préservé), + `test_users_me_no_secret_columns`, `test_gitea_cache_evicts_expired` + ## v7.4.0 (2026-10-01) — Audit : A30, A37, A39, A40, A41 ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index 1ad0da0..71452b5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1149,7 +1149,7 @@ Quality DB views, Agent IA Palette → Realtime + E - [x] **A26 — Config piège** : (a) `FLOWDECK_STANDALONE` documenté dans `config.py:26` mais **jamais lu** (le champ réel est `STANDALONE`, pas de `env_prefix`, `extra="ignore"` avale la coquille) ; (b) `.env.example` documente `postgresql://…` mais `db_path` (`config.py:133`) retombe silencieusement sur SQLite ; (c) `db_path` fait `Path("/" + p)` (`config.py:132`) → pour `sqlite:////data/flowdeck.db` le résultat est le chemin UNC `\\data\flowdeck.db` (vérifié : inexistant) ; (d) `app_secret_key="change-me-to-random"` (`config.py:36`) + `gitea_oauth_client_secret="test-secret"` sans aucun garde au boot alors qu'il signe `flowdeck_session` (`session.py:14`). *Fix : 1 normalisation de `db_path` + `raise` au boot si secret par défaut + corriger le commentaire/`env_prefix`. Effort : **S**.* - [ ] **A27 — 13 900 lignes de JS inline dans 37 blocs**, ~3 800 livrées sur **chaque** page (`base.html` 1520 + `agent_panel` 1805 + `_icon_picker` 297 + `_header` 124 + `_notification_bell` 69), et **0 linté** : `eslint.config.mjs:50` ne couvre que `static/js/**/*.js` (soit `app.js` + `offline.js`), 2 blocs se neutralisent avec `/* eslint-disable */`. Grosseurs : `_page_editor_scripts` 2517, `local_workspace` 2030, `agent_panel` 1805, `base` 1520, `_database_table_scripts` 1323, `settings` 1093, `library` 1039. *Fix : extraire les gros partials vers `/static/js/*.js` (ils ne sont pas Jinja-interpolés) + ajouter les templates à eslint. Effort : **L**.* - [ ] **A28 — Dette de découpe (god files)** : `api_v2.py` 115 routes / 131 Ko, `dashboard.py` 63 / 116 Ko (27 pages HTMLResponse + 50 JSON + I/O fichiers, 16 `Environment(...)` locaux), `collections.py` 53 / 112 Ko, `board.py` 53 / 93 Ko (page CRUD + `zipfile` + sync Gitea). *Fix : scinder par **concern** (`pages_html`, `files`, sous-modules `api_v2/*`) — mécanique, 0 changement d'URL. Effort : **L**.* -- [ ] **A29 — Endpoints dupliqués 2-3×** : publish/unpublish existe en 3 endroits (`sharing.py:304/345`, `board.py:1020/1039`, `api_v2.py:1743/1761`) avec slug et auth **différents** ; listing collections ×3 (`/api/v1/collections`, `/db/api`, `/api/v2/collections`) ; `/api/users/me` ×2. *Fix : un `services/publish.py` partagé, les routers déléguent. Effort : **M**.* +- [x] **A29 — Endpoints dupliqués 2-3×** : publish/unpublish existe en 3 endroits (`sharing.py:304/345`, `board.py:1020/1039`, `api_v2.py:1743/1761`) avec slug et auth **différents** ; listing collections ×3 (`/api/v1/collections`, `/db/api`, `/api/v2/collections`) ; `/api/users/me` ×2. *Fix : un `services/publish.py` partagé, les routers déléguent.* — **fait 2026-10-01** : `services/publish.py` (slugify unique, 404 partout, événements) ; les 3 paires publish/unpublish déléguent (sharing + board + v2), board gagne `_require_auth`, les bonus divergents (`share_mode='anyone'` / `is_shared=1`) supprimés — le share dialog reste propriétaire de ces drapeaux ; **byproduct sécurité** : `GET /api/users/me` (v1) et le contexte de `/accounts` faisaient `SELECT *` → `password_hash` exposé → colonnes whitelistées. **Décision** : `/api/users/me` ×2 et listing collections ×3 **restent** — contrats versionnés distincts (session+guest vs Bearer+scope, formes différentes). Effort : **M**. - [x] **A30 — 16 fonctions top-level jamais référencées**, dont `require_scope` (`api_v2_helpers.py:213`, la factory FastAPI qui doit faire les scopes — les handlers font `has_scope(...)` à la main), `validate_upload`, `_get_user_or_redirect`, `_require_user_gitea`, `unsync_block`, `find_referring`… *Fix : câbler `validate_upload` (A22) + `require_scope`, supprimer le reste. Effort : **S**.* - [ ] **A31 — Dette migrations** : `migrations.py` 1 522 lignes / 66 Ko, 28 migrations (versions 2-29, contiguës, bien version-gated), **25 copies du motif `PRAGMA table_info`** sans helper (`table_exists`/`column_exists` inexistants), 30 `ALTER TABLE`, et `fn(conn)` tourne **hors transaction** → un échec au milieu laisse du DDL partiel commité. *Fix : 3 helpers + transaction par migration. Effort : **M**.* - [ ] **A32 — Couverture de tests par trou** : routers à **0 test** : `webhooks.py` (0/3), `notes.py` (0/2), `sidebar_config.py` (0/2), `github_routes.py` (0/2) ; quasi nuls : `library.py` 1/10, `api.py` 3/23 (move, col-mapping, board-config, CRUD issues), `dashboard.py` 17/63, `api_v2.py` 50/115. Points positifs vérifiés : 1 002 tests, **aucun sans `assert`**, aucun qui touche le réseau réel. *Fix : 1 smoke test par route non couverte (fixture TestClient existante). Effort : **M**.* @@ -1165,7 +1165,7 @@ Quality DB views, Agent IA Palette → Realtime + E - [x] **A39 — HTMX chargé (49,7 Ko) pour 10 attributs réels** vs 265 `fetch(` manuels. *Fix : soit drop `htmx.min.js` et convertir les 10, soit rien.* — **décision 2026-10-01 : rien** (option proposée par l'audit) : 32 attributs `hx-*` réels répartis dans 6 templates (view-switch board/dashboard/notes, `hx-boost`) ; les convertir = refonte du view-switching en JS sans couverture automatisée, coût/risque > gain de 49,7 Ko. **Reconsidérer** dès qu'un test E2E couvre le view-switch. - [x] **A40 — Assets** : `?v=` incohérent (`app.css?v=5.1.1` mais CSS modifié le 2026-09-14 > dernier bump 2026-09-12 → servi depuis le cache), la même liste d'assets est **dupliquée** dans `sw.js:19-31`, htmx/alpine/prism vendored **sans bannière de version ni SRI**, 3 `