CORS recherche/alertes : allowlist soeurs prod+dev explicites (fusion #68/#69) #32
+11
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user