CORS recherche/alertes : allowlist soeurs prod+dev explicites (fusion #68/#69) #32

Merged
bruno merged 4 commits from cors-recherche-alertes-68-69 into main 2026-10-28 14:25:23 -04:00
4 changed files with 53 additions and 18 deletions
+11
View File
@@ -251,6 +251,17 @@ _CORS_ORIGINS = sorted(set(_CORS_ORIGINS) | {
_draco_urls69.origine(_toutes69[k]) _draco_urls69.origine(_toutes69[k])
for k in ("obsigate", "flowdeck", "tiktak", "kanbante") 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 — # Hors prod : dev local + origines d'extension (Web Clipper, Bearer uniquement —
# pas de cookie → `allow_credentials` ne s'applique pas à ces origines). # pas de cookie → `allow_credentials` ne s'applique pas à ces origines).
app.add_middleware( app.add_middleware(
+18 -15
View File
@@ -8,15 +8,18 @@ Ce module n'autorise donc que :
- le chemin ``/api/search`` (jamais les autres routes, jamais les - le chemin ``/api/search`` (jamais les autres routes, jamais les
écritures) ; écritures) ;
- les origines sœurs : celles résolues par - les origines sœurs : les origines officielles **prod ET dev,
``app.services.draco_urls.toutes()`` (explicite > hôte > dérivation listées explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus**
> défaut officiel), **plus** les origines prod ou ``-dev`` celles résolues par ``app.services.draco_urls.toutes()``
officielles correspondant à l'hôte de la requête — ce second (explicite > hôte > dérivation > défaut officiel) — jamais ``*``.
ensemble est nécessaire car ce middleware s'exécute avant le Correctif 2026-10-28 (fusion #68/#69, matrice constatée par
middleware CSP qui pose le contexte d'hôte de la requête pour Bruno) : l'ancienne allowlist ne contenait que l'environnement
``draco_urls`` ; sans lui, une page servie par ``flowdeck-dev`` résolu (configuration seule, ce middleware s'exécutant avant le
verrait ses sœurs ``-dev`` refusées quand la configuration seule middleware CSP qui pose le contexte d'hôte, plus l'environnement
ne les déclare pas ; 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 - avec credentials (le cookie SSO ``draco_session``, accepté par
``/api/search`` depuis cette livraison, ou la session native). ``/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)) origin = origin_of((resolved or {}).get(key))
if origin: if origin:
origins.add(origin) origins.add(origin)
if is_dev_host(request_host): # Correctif 2026-10-28 : prod ET dev toujours — ``request_host``
origins |= DEV_ORIGINS # ne restreint plus l'allowlist (il ne servait qu'à ajouter
elif is_prod_host(request_host): # l'environnement de la requête, désormais inclus d'office).
origins |= PROD_ORIGINS # ``resolved`` n'ajoute que des origines de configuration sans
if not origins: # jamais retirer une origine officielle.
return PROD_ORIGINS | DEV_ORIGINS origins |= PROD_ORIGINS | DEV_ORIGINS
return frozenset(origins) return frozenset(origins)
+6
View File
@@ -61,3 +61,9 @@ def test_notifications_api_et_cors_soeur(client):
headers={"Origin": "https://evil.example", headers={"Origin": "https://evil.example",
"Access-Control-Request-Method": "GET"}) "Access-Control-Request-Method": "GET"})
assert "access-control-allow-origin" not in r4.headers 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"
+18 -3
View File
@@ -38,7 +38,13 @@ def test_origin_of():
def test_sister_origins_dev(): 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) == { 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://og-dev.dracodev.net",
"https://flowdeck-dev.dracodev.net", "https://flowdeck-dev.dracodev.net",
"https://tiktak-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(): 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( assert "https://og-dev.dracodev.net" in sister_origins(
{}, request_host="flowdeck-dev.dracodev.net" {}, request_host="flowdeck-dev.dracodev.net"
) )
assert "https://og.dracodev.net" in sister_origins( assert "https://og.dracodev.net" in sister_origins(
{}, request_host="flowdeck.dracodev.net" {}, 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" {}, request_host="flowdeck.dracodev.net"
) )
assert "https://og.dracodev.net" in sister_origins(
{}, request_host="flowdeck-dev.dracodev.net"
)
def test_cors_headers(): 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-Origin"] == "https://og-dev.dracodev.net"
assert headers["Access-Control-Allow-Credentials"] == "true" assert headers["Access-Control-Allow-Credentials"] == "true"
assert cors_headers_for("https://evil.example", RESOLVED_DEV) is None 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(): def test_preflight_headers():