From dd979f81a798d323cd531b81ba99fb529a50908c Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 13:45:42 -0400 Subject: [PATCH 1/4] CORS recherche/alertes (fusion #68/#69) : allowlist soeurs prod+dev explicites --- app/middleware/recherche_cors.py | 33 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/app/middleware/recherche_cors.py b/app/middleware/recherche_cors.py index ed2f323..0c6d029 100644 --- a/app/middleware/recherche_cors.py +++ b/app/middleware/recherche_cors.py @@ -8,15 +8,18 @@ Ce module n'autorise donc que : - le chemin ``/api/search`` (jamais les autres routes, jamais les écritures) ; -- les origines sœurs : celles résolues par - ``app.services.draco_urls.toutes()`` (explicite > hôte > dérivation - > défaut officiel), **plus** les origines prod ou ``-dev`` - officielles correspondant à l'hôte de la requête — ce second - ensemble est nécessaire car ce middleware s'exécute avant le - middleware CSP qui pose le contexte d'hôte de la requête pour - ``draco_urls`` ; sans lui, une page servie par ``flowdeck-dev`` - verrait ses sœurs ``-dev`` refusées quand la configuration seule - ne les déclare pas ; +- les origines sœurs : les origines officielles **prod ET dev, + listées explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus** + celles résolues par ``app.services.draco_urls.toutes()`` + (explicite > hôte > dérivation > défaut officiel) — jamais ``*``. + Correctif 2026-10-28 (fusion #68/#69, matrice constatée par + Bruno) : l'ancienne allowlist ne contenait que l'environnement + résolu (configuration seule, ce middleware s'exécutant avant le + middleware CSP qui pose le contexte d'hôte, plus l'environnement + de l'hôte de la requête) — jamais les deux à la fois. Une page + servie dans l'autre environnement voyait donc sa section + afficher « Injoignable depuis cet hôte (CORS ou réseau) », et + c'est Flowdeck qui échouait depuis tous les hôtes ; - avec credentials (le cookie SSO ``draco_session``, accepté par ``/api/search`` depuis cette livraison, ou la session native). @@ -105,12 +108,12 @@ def sister_origins(resolved: dict | None = None, request_host: str | None = None origin = origin_of((resolved or {}).get(key)) if origin: origins.add(origin) - if is_dev_host(request_host): - origins |= DEV_ORIGINS - elif is_prod_host(request_host): - origins |= PROD_ORIGINS - if not origins: - return PROD_ORIGINS | DEV_ORIGINS + # Correctif 2026-10-28 : prod ET dev toujours — ``request_host`` + # ne restreint plus l'allowlist (il ne servait qu'à ajouter + # l'environnement de la requête, désormais inclus d'office). + # ``resolved`` n'ajoute que des origines de configuration sans + # jamais retirer une origine officielle. + origins |= PROD_ORIGINS | DEV_ORIGINS return frozenset(origins) -- 2.54.0 From 2d3a93511b1f5617cf2d9e3660966c0c6066e0bf Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 13:45:45 -0400 Subject: [PATCH 2/4] CORS alertes (fusion #69) : allow_origins /api/notifications prod+dev --- app/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/main.py b/app/main.py index 2519335..c09f227 100644 --- a/app/main.py +++ b/app/main.py @@ -251,6 +251,17 @@ _CORS_ORIGINS = sorted(set(_CORS_ORIGINS) | { _draco_urls69.origine(_toutes69[k]) for k in ("obsigate", "flowdeck", "tiktak", "kanbante") } - {""}) +# Correctif CORS alertes (fusion #69, 2026-10-28) : allow_origins +# est figé à l'import (configuration seule, sans hôte de requête) +# — il ne doit donc pas dépendre de l'environnement résolu. Les +# origines sœurs prod ET dev sont listées explicitement (module +# CORS de la recherche, jamais "*") : c'est ce qui manquait à +# /api/notifications lu depuis les autres hôtes. +from app.middleware.recherche_cors import DEV_ORIGINS as _DEV_ORIGINS +from app.middleware.recherche_cors import PROD_ORIGINS as _PROD_ORIGINS + +_CORS_ORIGINS = sorted(set(_CORS_ORIGINS) | set(_DEV_ORIGINS) + | set(_PROD_ORIGINS)) # Hors prod : dev local + origines d'extension (Web Clipper, Bearer uniquement — # pas de cookie → `allow_credentials` ne s'applique pas à ces origines). app.add_middleware( -- 2.54.0 From c09a0ac51d58f9ea04bc21881dd77277e1a604c9 Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 13:45:48 -0400 Subject: [PATCH 3/4] Tests CORS recherche : prod+dev autorises, tiers refuse --- tests/test_recherche.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/test_recherche.py b/tests/test_recherche.py index 9b58017..bcebe93 100644 --- a/tests/test_recherche.py +++ b/tests/test_recherche.py @@ -38,7 +38,13 @@ def test_origin_of(): def test_sister_origins_dev(): + # Correctif 2026-10-28 : 8 origines officielles, prod ET dev, + # quelle que soit la résolution. assert sister_origins(RESOLVED_DEV) == { + "https://og.dracodev.net", + "https://flowdeck.dracodev.net", + "https://tiktak.dracodev.net", + "https://kanbante.dracodev.net", "https://og-dev.dracodev.net", "https://flowdeck-dev.dracodev.net", "https://tiktak-dev.dracodev.net", @@ -47,16 +53,21 @@ def test_sister_origins_dev(): def test_sister_origins_ajoute_env_de_la_requete(): - # Configuration muette (hors requête) + hôte dev : les -dev passent. + # Correctif 2026-10-28 : configuration muette, quel que soit + # l'hôte de la requête, prod ET dev passent — l'hôte ne + # restreint plus l'allowlist. assert "https://og-dev.dracodev.net" in sister_origins( {}, request_host="flowdeck-dev.dracodev.net" ) assert "https://og.dracodev.net" in sister_origins( {}, request_host="flowdeck.dracodev.net" ) - assert "https://og-dev.dracodev.net" not in sister_origins( + assert "https://og-dev.dracodev.net" in sister_origins( {}, request_host="flowdeck.dracodev.net" ) + assert "https://og.dracodev.net" in sister_origins( + {}, request_host="flowdeck-dev.dracodev.net" + ) def test_cors_headers(): @@ -64,7 +75,11 @@ def test_cors_headers(): assert headers["Access-Control-Allow-Origin"] == "https://og-dev.dracodev.net" assert headers["Access-Control-Allow-Credentials"] == "true" assert cors_headers_for("https://evil.example", RESOLVED_DEV) is None - assert cors_headers_for("https://og.dracodev.net", RESOLVED_DEV) is None + assert cors_headers_for("https://og-dev.dracodev.net.evil.example", + RESOLVED_DEV) is None + # Autre environnement autorisé (correctif 2026-10-28). + assert cors_headers_for("https://og.dracodev.net", + RESOLVED_DEV) is not None def test_preflight_headers(): -- 2.54.0 From 9ca831f816da8fbb1fa675691311a6cd1e6f3cf5 Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 13:45:50 -0400 Subject: [PATCH 4/4] Tests CORS alertes : origine dev autorisee en preflight --- tests/test_draco_alertes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_draco_alertes.py b/tests/test_draco_alertes.py index 86c08f6..eafebbe 100644 --- a/tests/test_draco_alertes.py +++ b/tests/test_draco_alertes.py @@ -61,3 +61,9 @@ def test_notifications_api_et_cors_soeur(client): headers={"Origin": "https://evil.example", "Access-Control-Request-Method": "GET"}) assert "access-control-allow-origin" not in r4.headers + # Correctif 2026-10-28 : une origine sœur dev passe aussi. + r5 = client.options("/api/notifications", + headers={"Origin": "https://tiktak-dev.dracodev.net", + "Access-Control-Request-Method": "GET"}) + assert r5.headers.get("access-control-allow-origin") == "https://tiktak-dev.dracodev.net" + assert r5.headers.get("access-control-allow-credentials") == "true" -- 2.54.0