CORS recherche/alertes : allowlist soeurs prod+dev explicites (fusion #68/#69) #37
+8
-2
@@ -438,10 +438,16 @@ class DracoAlertesCorsMiddleware(BaseHTTPMiddleware):
|
|||||||
if request.url.path != "/api/events":
|
if request.url.path != "/api/events":
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
from app.config import resoudre_urls_draco
|
from app.config import resoudre_urls_draco
|
||||||
|
from app.recherche import DEV_ORIGINS, PROD_ORIGINS, origin_of
|
||||||
|
|
||||||
urls = resoudre_urls_draco(request.headers.get("host"))
|
urls = resoudre_urls_draco(request.headers.get("host"))
|
||||||
soeurs = {urls.sso_url, urls.flowdeck_url, urls.tiktak_url,
|
# Correctif CORS alertes (fusion #69, 2026-10-28) : comme
|
||||||
urls.public_url}
|
# pour /api/search, les origines sœurs prod ET dev sont
|
||||||
|
# listées explicitement — l'ancienne allowlist (environnement
|
||||||
|
# résolu seul) refusait les pages de l'autre environnement.
|
||||||
|
soeurs = ({origin_of(u) for u in (urls.sso_url, urls.flowdeck_url,
|
||||||
|
urls.tiktak_url, urls.public_url)}
|
||||||
|
| set(PROD_ORIGINS) | set(DEV_ORIGINS)) - {None}
|
||||||
origine = request.headers.get("origin", "")
|
origine = request.headers.get("origin", "")
|
||||||
if origine not in soeurs:
|
if origine not in soeurs:
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|||||||
+12
-4
@@ -9,9 +9,14 @@ testables sans base ni serveur.
|
|||||||
recherche Gitea de la page ``/search`` (titres + corps, tous états,
|
recherche Gitea de la page ``/search`` (titres + corps, tous états,
|
||||||
tous projets du workspace) et la regroupe par dépôt.
|
tous projets du workspace) et la regroupe par dépôt.
|
||||||
- La CORS bornée n'autorise que ``GET /api/search``, et seulement
|
- La CORS bornée n'autorise que ``GET /api/search``, et seulement
|
||||||
pour les origines sœurs résolues par ``app.config`` (explicite >
|
pour les origines sœurs officielles **prod ET dev, listées
|
||||||
hôte de la requête > dérivation > défaut officiel — donc ``-dev``
|
explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus** les
|
||||||
reste ``-dev``), jamais ``*``. Le middleware est un ASGI pur :
|
origines résolues par ``app.config`` — jamais ``*``. Correctif
|
||||||
|
2026-10-28 (fusion #68/#69, matrice constatée par Bruno) :
|
||||||
|
l'ancienne allowlist ne contenait que l'environnement résolu
|
||||||
|
(prod OU dev), donc une page de l'autre environnement voyait sa
|
||||||
|
section afficher « Injoignable depuis cet hôte (CORS ou réseau) ».
|
||||||
|
Le middleware est un ASGI pur :
|
||||||
il répond lui-même aux preflights OPTIONS (qui ne portent jamais
|
il répond lui-même aux preflights OPTIONS (qui ne portent jamais
|
||||||
de cookies, donc qu'``AuthMiddleware`` refuserait sinon) et ajoute
|
de cookies, donc qu'``AuthMiddleware`` refuserait sinon) et ajoute
|
||||||
les en-têtes aux réponses de recherche, sans toucher aux autres
|
les en-têtes aux réponses de recherche, sans toucher aux autres
|
||||||
@@ -97,7 +102,10 @@ def sister_origins(
|
|||||||
origin = origin_of((resolved or {}).get(key))
|
origin = origin_of((resolved or {}).get(key))
|
||||||
if origin:
|
if origin:
|
||||||
origins.add(origin)
|
origins.add(origin)
|
||||||
return frozenset(origins)
|
# Correctif 2026-10-28 : prod ET dev toujours, quelle que soit
|
||||||
|
# la résolution — ``resolved`` n'ajoute que des origines de
|
||||||
|
# configuration sans jamais retirer une origine officielle.
|
||||||
|
return frozenset(origins) | PROD_ORIGINS | DEV_ORIGINS
|
||||||
|
|
||||||
|
|
||||||
def cors_headers_for(
|
def cors_headers_for(
|
||||||
|
|||||||
@@ -44,3 +44,10 @@ def test_cors_events_soeur_preflight_get_et_tiers(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 r3.headers
|
assert "access-control-allow-origin" not in r3.headers
|
||||||
|
# Correctif 2026-10-28 : une origine sœur dev passe aussi.
|
||||||
|
r4 = client.options("/api/events",
|
||||||
|
headers={"Origin": "https://tiktak-dev.dracodev.net",
|
||||||
|
"Access-Control-Request-Method": "GET"})
|
||||||
|
assert r4.status_code == 200
|
||||||
|
assert r4.headers.get("access-control-allow-origin") == "https://tiktak-dev.dracodev.net"
|
||||||
|
assert r4.headers.get("access-control-allow-credentials") == "true"
|
||||||
|
|||||||
+19
-2
@@ -34,7 +34,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,7 +53,11 @@ def test_cors_headers():
|
|||||||
assert headers["Access-Control-Allow-Origin"] == "https://tiktak-dev.dracodev.net"
|
assert headers["Access-Control-Allow-Origin"] == "https://tiktak-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():
|
||||||
@@ -86,7 +96,14 @@ def test_groupes_vers_json_vide():
|
|||||||
|
|
||||||
|
|
||||||
def test_sister_origins_hote_requete():
|
def test_sister_origins_hote_requete():
|
||||||
# Configuration muette + hôte dev : le repli inclut les -dev.
|
# Correctif 2026-10-28 : configuration muette, quel que soit
|
||||||
|
# l'hôte de la requête, prod ET dev passent.
|
||||||
assert "https://og-dev.dracodev.net" in sister_origins(
|
assert "https://og-dev.dracodev.net" in sister_origins(
|
||||||
{}, request_host="kanbante-dev.dracodev.net"
|
{}, request_host="kanbante-dev.dracodev.net"
|
||||||
)
|
)
|
||||||
|
assert "https://og.dracodev.net" in sister_origins(
|
||||||
|
{}, request_host="kanbante-dev.dracodev.net"
|
||||||
|
)
|
||||||
|
assert "https://og-dev.dracodev.net" in sister_origins(
|
||||||
|
{}, request_host="kanbante.dracodev.net"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user