fix(draco): résolution dev/prod par l'hôte de la requête (7.82.4)
Le mode console du panneau restait une page morte sur flowdeck-dev : CSP frame-src/connect-src et DRACO_PANNEAU_CONFIG.agent_url pointaient l'agent prod (pas encore installée). Deux trous de la 7.82.3 : agent_url() sans aucune dérivation, et aucune résolution lisant l'hôte de la requête. Pour tout ce qui est rendu à une requête (barre/DRACO_URLS, iframe du panneau, CSP, boutons et redirections SSO) : explicite > hôte public de la requête (motif fermé ^(og|flowdeck|tiktak|kanbante|agent)(SUFFIXE).dracodev.net$, suffixe appliqué aux sœurs et à l'agent) > dérivation par les adresses de l'agent connues en config (élargie : agent_url() se dérive de l'URL d'événements explicite) > défaut officiel. L'hôte est posé par le middleware CSP dans un ContextVar (patron du nonce CSP) : iframe et CSP de la même réponse consomment la même origine résolue. Contextes sans requête inchangés sinon. Tests : +12 dans test_draco_urls_devprod.py (25/25) — Host dev sans aucun env → tout en -dev ; Host prod → officiels ; Host LAN → repli config/défauts ; explicite gagne sur l'hôte ; motif fermé. Suite complète : 4 failed / 26 errors identiques sur arbre propre (familles préexistantes MCP/realtime/my_tasks/gallery/sync).
This commit is contained in:
@@ -1,5 +1,54 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.82.4 (2026-10-25) — Résolution dev/prod par l'hôte de la requête : le mode console du panneau revit en dev
|
||||
|
||||
- **Bug corrigé (dev)** : sur flowdeck-dev, le mode console du
|
||||
panneau Spark restait une page morte — prouvé en direct avant
|
||||
correctif : la 7.82.3 tournait bien (`GET /api/health` →
|
||||
`7.82.3`), mais sa CSP servait `frame-src 'self'
|
||||
https://agent.dracodev.net` (et `connect-src` idem), pointant
|
||||
l'agent **prod, pas encore installée**, et la source de l'iframe
|
||||
console (`DRACO_PANNEAU_CONFIG.agent_url`, rendue par
|
||||
`base.html`) valait la même origine morte. Deux trous laissés par
|
||||
la v7.82.3 : (1) `agent_url()` n'avait **aucune dérivation** —
|
||||
même avec `AGENT_EVENEMENTS_URL` en `-dev`, l'agent lui-même
|
||||
retombait sur le nom officiel ; (2) **aucune** résolution ne
|
||||
lisait l'hôte de la requête — un `.env` dev sans
|
||||
`DRACO_AGENT_URL` ni `AGENT_EVENEMENTS_URL` retombait
|
||||
entièrement sur la prod.
|
||||
- **Cure** (`app/services/draco_urls.py`, `app/middleware/security.py`) :
|
||||
pour tout ce qui est **rendu à une requête** (liens de la barre /
|
||||
`DRACO_URLS` servi, source de l'iframe du panneau, origine agent
|
||||
des CSP `frame-src`/`connect-src`, boutons et redirections SSO —
|
||||
émettrice via `DRACO_SSO_URL` résolu, callback =
|
||||
`OAUTH_REDIRECT_URI` dont l'origine est sa propre origine
|
||||
résolue), l'ordre de résolution devient : 1) variable explicite
|
||||
de l'installation, prioritaire et inchangée ; 2) **hôte public de
|
||||
la requête** (posé par le middleware CSP dans un `ContextVar`,
|
||||
même patron que le nonce CSP ; `X-Forwarded-Host` sinon `Host`) —
|
||||
motif fermé `^(og|flowdeck|tiktak|kanbante|agent)(<SUFFIXE>)\.dracodev\.net$`,
|
||||
suffixe optionnel (`-dev`, vide en prod) appliqué à toutes les
|
||||
sœurs **et à l'agent** ; jamais de suffixe inventé depuis un hôte
|
||||
arbitraire, hors motif (LAN, IP) on passe à la suite ; 3)
|
||||
dérivation par les adresses de l'agent connues en configuration,
|
||||
**élargie** (URL d'événements **ou** base du service — notamment
|
||||
`agent_url()` se dérive désormais de l'URL d'événements
|
||||
explicite) — aussi la résolution des contextes **sans requête**,
|
||||
inchangée sinon ; 4) défaut officiel. L'iframe du panneau et la
|
||||
CSP de la même réponse consomment la **même** origine résolue :
|
||||
les deux lisent `agent_url()` pendant la même requête.
|
||||
- Tests : `tests/test_draco_urls_devprod.py` +12 (25/25 au total) —
|
||||
requête `Host: flowdeck-dev.dracodev.net` et **aucune variable
|
||||
d'environnement** → barre, iframe, CSP et SSO rendus entièrement
|
||||
en `-dev` ; Host prod → noms officiels ; Host LAN → repli
|
||||
config/défauts ; explicite gagne sur l'hôte ; motif fermé
|
||||
(hôtes arbitraires → aucune dérivation) ; agent dérivé du seul
|
||||
`AGENT_EVENEMENTS_URL` ; contextes sans requête inchangés.
|
||||
Suite complète relue : **4 failed / 26 errors, identiques sur
|
||||
arbre propre** (1307 passed en propre ; familles préexistantes
|
||||
MCP/realtime/my_tasks/gallery/sync, sans rapport avec ce
|
||||
correctif) — 1319 passed avec le correctif. Ruff vert.
|
||||
|
||||
## v7.82.3 (2026-10-25) — URLs sœurs dérivées de l'environnement de l'agent
|
||||
|
||||
- **Bug corrigé (dev)** : les URL sœurs servies par `draco_urls()`
|
||||
|
||||
+1
-1
@@ -216,7 +216,7 @@ def _lire_version() -> str:
|
||||
|
||||
|
||||
# Littéral de version synchronisé avec le fichier VERSION :
|
||||
# version="7.82.3"
|
||||
# version="7.82.4"
|
||||
# Patron historique des bumps (voir scripts/bump_version.py, qui cherche
|
||||
# ce littéral) et contrôle de tests/test_pwa_offline.py. La version
|
||||
# runtime reste lue dynamiquement depuis VERSION par _lire_version()
|
||||
|
||||
@@ -134,8 +134,9 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
|
||||
# service d'agents commun (premier parti, même programme) —
|
||||
# seul hôte externe nommé, rien d'autre ne s'ouvre.
|
||||
# v7.82.0 (dev/prod) : cet hôte n'est plus figé — {agent} est
|
||||
# l'origine résolue par la config (DRACO_AGENT_URL ; défaut =
|
||||
# nom officiel, un dev déclare son agent-dev dans le .env).
|
||||
# l'origine résolue par draco_urls (DRACO_AGENT_URL explicite,
|
||||
# sinon hôte de la requête — v7.82.4 —, sinon dérivation par
|
||||
# l'agent connu en config ; défaut = nom officiel).
|
||||
# Même origine pour frame-src : la console du service est
|
||||
# encadrée en iframe par le panneau (surimpression v3).
|
||||
"connect-src 'self' {agent} ws://{host} wss://{host}; "
|
||||
@@ -152,6 +153,17 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
|
||||
# tâche créée maintenant → le contexte (donc le nonce) y est copié,
|
||||
# exactement ce que les templates liront via `csp_nonce()`.
|
||||
CSP_NONCE.set(nonce)
|
||||
# v7.82.4 : même patron pour l'hôte public de la requête —
|
||||
# `draco_urls` le lit pour résoudre l'environnement (dev/prod)
|
||||
# de tout ce qui est rendu à cette requête (barre, iframe du
|
||||
# panneau, SSO), et la CSP ci-dessous consomme la MÊME origine
|
||||
# agent résolue. X-Forwarded-Host prime, comme
|
||||
# `get_redirect_uri()` (app/routers/auth.py).
|
||||
from app.services import draco_urls
|
||||
|
||||
draco_urls.definir_hote_requete(
|
||||
request.headers.get("x-forwarded-host", "")
|
||||
or request.headers.get("host", ""))
|
||||
response = await call_next(request)
|
||||
# Only set CSP on HTML responses
|
||||
content_type = response.headers.get("content-type", "")
|
||||
@@ -160,8 +172,6 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
|
||||
# on retire quand même tout caractère hors base URL par sécurité.
|
||||
host = re.sub(r"[^0-9A-Za-z.\-:\[\]]", "",
|
||||
request.headers.get("host", ""))
|
||||
from app.services import draco_urls
|
||||
|
||||
agent = re.sub(r"[^0-9A-Za-z.\-:/]", "",
|
||||
draco_urls.origine(draco_urls.agent_url()))
|
||||
response.headers[self.CSP_HEADER] = self.CSP_VALUE.format(
|
||||
|
||||
+7
-5
@@ -33,12 +33,14 @@ def get_redirect_uri(request: Request) -> str:
|
||||
return f"{scheme}://{host}/auth/callback"
|
||||
|
||||
def _sso_login_url() -> str:
|
||||
"""URL du bouton « Login with ObsiGate (SSO) », résolue par la config.
|
||||
"""URL du bouton « Login with ObsiGate (SSO) ».
|
||||
|
||||
Émettrice = DRACO_SSO_URL (défaut : nom officiel) ; retour =
|
||||
URL publique de cette instance (FLOWDECK_URL / OAUTH_REDIRECT_URI /
|
||||
APP_BASE_URL, défaut : nom officiel) + ``/auth/draco``. Aucun nom
|
||||
public figé : un dev déclare ses noms en ``-dev`` dans le .env.
|
||||
Émettrice = ``obsigate_url()`` (DRACO_SSO_URL explicite, sinon
|
||||
l'environnement de la requête — v7.82.4) ; retour = URL publique
|
||||
de cette instance résolue de la même façon (FLOWDECK_URL /
|
||||
OAUTH_REDIRECT_URI explicites, sinon hôte de la requête, le
|
||||
callback = sa propre origine résolue) + ``/auth/draco``. Aucun
|
||||
nom public figé.
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
+149
-7
@@ -32,16 +32,63 @@ qui ne déclare que l'agent pointe ses liens inter-apps en ``-dev``
|
||||
sans quatre variables de plus. Si l'hôte de l'agent ne correspond
|
||||
pas au motif (autre domaine, LAN, IP), **aucune dérivation** : on
|
||||
ne fabrique jamais un lien deviné. L'URL Gitea ne change pas.
|
||||
|
||||
**Résolution par l'hôte de la requête (v7.82.4)** — la v7.82.3
|
||||
laissait deux trous, prouvés en direct sur flowdeck-dev (CSP
|
||||
``frame-src 'self' https://agent.dracodev.net`` alors que la prod
|
||||
n'existe pas encore → console du panneau = page morte) :
|
||||
|
||||
1. ``agent_url()`` n'avait **aucune** dérivation : même avec
|
||||
``AGENT_EVENEMENTS_URL`` en ``-dev``, l'agent lui-même retombait
|
||||
sur le nom officiel ;
|
||||
2. **aucune** résolution ne lisait l'hôte de la requête : un
|
||||
``.env`` dev sans ``DRACO_AGENT_URL`` ni ``AGENT_EVENEMENTS_URL``
|
||||
retombait entièrement sur la prod.
|
||||
|
||||
Pour tout ce qui est **rendu à une requête** (liens de la barre /
|
||||
``DRACO_URLS`` servi, source de l'iframe du panneau, origine agent
|
||||
des CSP ``frame-src``/``connect-src``, boutons et redirections
|
||||
SSO), l'ordre de résolution devient :
|
||||
|
||||
1. variable explicite de l'installation — prioritaire, inchangée ;
|
||||
2. **hôte public de la requête** (posé par le middleware CSP dans
|
||||
``HOTE_REQUETE``, même patron que le nonce CSP) : motif fermé
|
||||
``^(og|flowdeck|tiktak|kanbante|agent)(<SUFFIXE>)\\.dracodev\\.net$``
|
||||
(suffixe optionnel, p. ex. ``-dev``, vide en prod) →
|
||||
l'environnement = ce suffixe, appliqué à toutes les sœurs **et
|
||||
à l'agent**. Jamais de suffixe inventé depuis un hôte arbitraire ;
|
||||
hors motif (LAN, IP) → étape suivante ;
|
||||
3. dérivation par les adresses de l'agent connues en configuration
|
||||
(URL d'événements **ou** base du service) — aussi la résolution
|
||||
des contextes **sans requête**, inchangée sinon ;
|
||||
4. défaut officiel.
|
||||
|
||||
L'iframe du panneau et la CSP de la même réponse consomment la
|
||||
même origine résolue : les deux lisent ``agent_url()`` pendant la
|
||||
même requête, avec le même ``HOTE_REQUETE``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextvars import ContextVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from app.config import settings
|
||||
|
||||
# Hôte public de la requête en cours (``X-Forwarded-Host`` sinon
|
||||
# ``Host``), posé par le middleware CSP avant le traitement — même
|
||||
# patron que ``CSP_NONCE`` : le contexte est copié dans la tâche qui
|
||||
# rend les templates. Vide hors requête (tâches de fond, CLI, tests
|
||||
# unitaires) → la résolution retombe sur la configuration.
|
||||
HOTE_REQUETE: ContextVar[str] = ContextVar("draco_hote_requete", default="")
|
||||
|
||||
|
||||
def definir_hote_requete(hote: str) -> None:
|
||||
"""Mémorise l'hôte public de la requête (premier de la liste)."""
|
||||
HOTE_REQUETE.set((hote or "").split(",")[0].strip().lower())
|
||||
|
||||
OFFICIELLES = {
|
||||
"obsigate": "https://og.dracodev.net",
|
||||
"agent": "https://agent.dracodev.net",
|
||||
@@ -77,6 +124,16 @@ _SLUGS_SOEURS = {
|
||||
# Hôte de l'agent : agent<SUFFIXE>.dracodev.net (SUFFIXE = ``-dev``…).
|
||||
_MOTIF_HOTE_AGENT = re.compile(r"^agent(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
|
||||
# Hôte public d'une requête : motif FERMÉ des cinq applications.
|
||||
# Le suffixe capturé (groupe 2, p. ex. ``-dev``) est l'environnement
|
||||
# de TOUTE l'installation — jamais un suffixe inventé depuis un
|
||||
# hôte arbitraire : hors motif (autre domaine, LAN, IP), None.
|
||||
_MOTIF_HOTE_PUBLIC = re.compile(
|
||||
r"^(og|flowdeck|tiktak|kanbante|agent)(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
|
||||
# Slugs pour la résolution par l'hôte de la requête (l'agent inclus).
|
||||
_SLUGS_TOUTES = {**_SLUGS_SOEURS, "agent": "agent"}
|
||||
|
||||
|
||||
def _suffixe_environnement() -> str | None:
|
||||
"""Suffixe d'environnement porté par l'hôte de l'agent, ou None.
|
||||
@@ -105,13 +162,60 @@ def _url_derivee(app: str) -> str:
|
||||
return f"https://{slug}{suffixe}.dracodev.net"
|
||||
|
||||
|
||||
def _suffixe_requete() -> str | None:
|
||||
"""Suffixe d'environnement porté par l'hôte de la requête, ou None.
|
||||
|
||||
Motif fermé ``_MOTIF_HOTE_PUBLIC`` sur l'hôte public mémorisé
|
||||
(port retiré) : ``""`` pour un nom officiel, ``"-dev"`` pour un
|
||||
nom en ``-dev``, None hors motif ou hors requête.
|
||||
"""
|
||||
hote_req = HOTE_REQUETE.get().split(":")[0]
|
||||
if not hote_req:
|
||||
return None
|
||||
m = _MOTIF_HOTE_PUBLIC.match(hote_req)
|
||||
if not m:
|
||||
return None
|
||||
return m.group(2) or ""
|
||||
|
||||
|
||||
def _url_depuis_requete(app: str) -> str:
|
||||
"""URL d'``app`` dérivée de l'hôte de la requête ("" si aucune)."""
|
||||
slug = _SLUGS_TOUTES.get(app)
|
||||
if not slug:
|
||||
return ""
|
||||
suffixe = _suffixe_requete()
|
||||
if suffixe is None:
|
||||
return ""
|
||||
return f"https://{slug}{suffixe}.dracodev.net"
|
||||
|
||||
|
||||
def _suffixe_evenements_explicite() -> str | None:
|
||||
"""Suffixe porté par l'URL d'événements **explicite**, ou None.
|
||||
|
||||
Lit ``AGENT_EVENEMENTS_URL`` (environnement puis settings) sans
|
||||
passer par ``agent_evenements_url()`` : pas de récursion avec
|
||||
``agent_url()``, dont c'est une source de dérivation.
|
||||
"""
|
||||
for candidate in (os.environ.get("AGENT_EVENEMENTS_URL", ""),
|
||||
getattr(settings, "agent_evenements_url", "") or ""):
|
||||
candidate = _nettoyer(candidate)
|
||||
if candidate:
|
||||
hote_agent = (urlparse(candidate).hostname or "").lower()
|
||||
m = _MOTIF_HOTE_AGENT.match(hote_agent)
|
||||
return (m.group(1) or "") if m else None
|
||||
return None
|
||||
|
||||
|
||||
def _resoudre_soeur(env_name: str, settings_attr: str, app: str) -> str:
|
||||
"""Résolution d'une URL sœur : explicite > dérivation > officiel.
|
||||
"""Résolution d'une URL sœur, dans l'ordre du chantier :
|
||||
|
||||
explicite > **hôte de la requête** > dérivation par l'agent
|
||||
connu en configuration > officiel.
|
||||
|
||||
La valeur ``settings`` égale au nom officiel est le *défaut* de
|
||||
``app/config.py``, pas un choix explicite : elle ne doit pas
|
||||
bloquer la dérivation d'environnement. Une valeur settings
|
||||
différente (dev, LAN, domaine propre) reste prioritaire.
|
||||
bloquer les dérivations. Une valeur settings différente (dev,
|
||||
LAN, domaine propre) reste prioritaire.
|
||||
"""
|
||||
env = _nettoyer(os.environ.get(env_name, ""))
|
||||
if env:
|
||||
@@ -119,6 +223,9 @@ def _resoudre_soeur(env_name: str, settings_attr: str, app: str) -> str:
|
||||
cfg = _nettoyer(getattr(settings, settings_attr, "") or "")
|
||||
if cfg and cfg != OFFICIELLES[app]:
|
||||
return cfg
|
||||
depuis_requete = _url_depuis_requete(app)
|
||||
if depuis_requete:
|
||||
return depuis_requete
|
||||
derivee = _url_derivee(app)
|
||||
if derivee:
|
||||
return derivee
|
||||
@@ -133,8 +240,30 @@ def obsigate_url() -> str:
|
||||
|
||||
|
||||
def agent_url() -> str:
|
||||
"""Service d'agents (Spark) — base sans chemin."""
|
||||
return _resoudre("DRACO_AGENT_URL", "draco_agent_url", "agent")
|
||||
"""Service d'agents (Spark) — base sans chemin.
|
||||
|
||||
Même ordre que les sœurs (la v7.82.3 l'avait laissé sans aucune
|
||||
dérivation — trou n° 1 du mode console mort en dev) : explicite
|
||||
(``DRACO_AGENT_URL`` / settings non officiel) > hôte de la
|
||||
requête > dérivation par l'URL d'événements explicite
|
||||
(``AGENT_EVENEMENTS_URL`` : ``agent-dev…`` → base ``agent-dev…``)
|
||||
> défaut officiel.
|
||||
"""
|
||||
env = _nettoyer(os.environ.get("DRACO_AGENT_URL", ""))
|
||||
if env:
|
||||
return env
|
||||
cfg = _nettoyer(getattr(settings, "draco_agent_url", "") or "")
|
||||
if cfg and cfg != OFFICIELLES["agent"]:
|
||||
return cfg
|
||||
depuis_requete = _url_depuis_requete("agent")
|
||||
if depuis_requete:
|
||||
return depuis_requete
|
||||
suffixe = _suffixe_evenements_explicite()
|
||||
if suffixe is not None:
|
||||
return f"https://agent{suffixe}.dracodev.net"
|
||||
if cfg:
|
||||
return cfg
|
||||
return OFFICIELLES["agent"]
|
||||
|
||||
|
||||
def tiktak_url() -> str:
|
||||
@@ -154,7 +283,8 @@ def flowdeck_url() -> str:
|
||||
|
||||
FLOWDECK_URL > origine de OAUTH_REDIRECT_URI (déjà déclarée en
|
||||
dev : ``https://flowdeck-dev.dracodev.net/auth/callback``) >
|
||||
APP_BASE_URL si elle n'est pas locale > nom officiel.
|
||||
hôte de la requête (motif fermé) > APP_BASE_URL si elle n'est
|
||||
pas locale > dérivation par l'agent > nom officiel.
|
||||
"""
|
||||
env = _nettoyer(os.environ.get("FLOWDECK_URL", ""))
|
||||
if env:
|
||||
@@ -171,6 +301,13 @@ def flowdeck_url() -> str:
|
||||
p = urlparse(candidate)
|
||||
if p.scheme and p.netloc:
|
||||
return f"{p.scheme}://{p.netloc}"
|
||||
# Étape 2 du chantier : l'hôte de la requête — c'est l'origine
|
||||
# que le navigateur utilise déjà pour cette instance, donc
|
||||
# aussi celle du callback SSO (``/auth/draco``) quand rien
|
||||
# d'explicite n'est déclaré.
|
||||
depuis_requete = _url_depuis_requete("flowdeck")
|
||||
if depuis_requete:
|
||||
return depuis_requete
|
||||
base = _nettoyer(getattr(settings, "app_base_url", "") or "")
|
||||
if base and "localhost" not in base and "127.0.0.1" not in base:
|
||||
return base
|
||||
@@ -208,7 +345,12 @@ def hote(url: str) -> str:
|
||||
|
||||
|
||||
def toutes() -> dict[str, str]:
|
||||
"""Toutes les URL résolues — injectées aux templates (hôte = config)."""
|
||||
"""Toutes les URL résolues — injectées aux templates.
|
||||
|
||||
Résolution à l'appel : dans une requête, l'hôte public de la
|
||||
requête fait foi après les variables explicites ; hors requête,
|
||||
la configuration seule (voir le docstring du module).
|
||||
"""
|
||||
return {
|
||||
"obsigate": obsigate_url(),
|
||||
"agent": agent_url(),
|
||||
|
||||
@@ -188,3 +188,185 @@ def test_bouton_sso_login_depuis_config(monkeypatch):
|
||||
assert _sso_login_url() == (
|
||||
"https://og-dev.dracodev.net/sso/authorize?return_to="
|
||||
"https%3A%2F%2Fflowdeck-dev.dracodev.net%2Fauth%2Fdraco")
|
||||
|
||||
|
||||
# ── v7.82.4 — résolution par l'hôte de la requête ─────────────────────
|
||||
# Correctif du mode console mort sur flowdeck-dev : l'agent n'avait
|
||||
# aucune dérivation (v7.82.3) et rien ne lisait l'hôte de la requête.
|
||||
# Ordre : explicite > hôte public de la requête (motif fermé) >
|
||||
# dérivation par l'agent connu en configuration > officiel.
|
||||
|
||||
@pytest.fixture
|
||||
def avec_hote():
|
||||
"""Pose l'hôte de requête comme le middleware CSP le fait."""
|
||||
jetons = []
|
||||
|
||||
def poser(hote):
|
||||
jetons.append(draco_urls.HOTE_REQUETE.set(hote))
|
||||
|
||||
yield poser
|
||||
for jeton in reversed(jetons):
|
||||
draco_urls.HOTE_REQUETE.reset(jeton)
|
||||
|
||||
|
||||
def test_agent_url_derivee_depuis_evenements_seul(monkeypatch):
|
||||
# Trou n° 1 de la v7.82.3 : seul AGENT_EVENEMENTS_URL posé (cas
|
||||
# réel de la dev) → l'agent lui-même suit en ``-dev``, pas
|
||||
# seulement les sœurs.
|
||||
monkeypatch.setenv(
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements")
|
||||
assert draco_urls.agent_url() == "https://agent-dev.dracodev.net"
|
||||
assert draco_urls.agent_evenements_url() == \
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements"
|
||||
|
||||
|
||||
def test_hote_requete_dev_toutes_urls_sans_env(avec_hote):
|
||||
# Aucune variable d'environnement (fixture _env_propre) : l'hôte
|
||||
# public suffit — sœurs ET agent en ``-dev``, Gitea inchangée.
|
||||
avec_hote("flowdeck-dev.dracodev.net")
|
||||
urls = draco_urls.toutes()
|
||||
assert urls["obsigate"] == "https://og-dev.dracodev.net"
|
||||
assert urls["agent"] == "https://agent-dev.dracodev.net"
|
||||
assert urls["tiktak"] == "https://tiktak-dev.dracodev.net"
|
||||
assert urls["kanbante"] == "https://kanbante-dev.dracodev.net"
|
||||
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
|
||||
assert urls["gitea"] == "https://git.dracodev.net"
|
||||
|
||||
|
||||
def test_hote_requete_quelconque_des_cinq_slugs(avec_hote):
|
||||
# L'environnement est celui de l'installation, pas de l'app
|
||||
# demandée : un hôte ``agent-dev`` ou ``og-dev`` vaut ``-dev``
|
||||
# pour toutes, port éventuel ignoré.
|
||||
for hote in ("agent-dev.dracodev.net", "og-dev.dracodev.net:443",
|
||||
"tiktak-dev.dracodev.net", "kanbante-dev.dracodev.net"):
|
||||
avec_hote(hote)
|
||||
assert draco_urls.agent_url() == "https://agent-dev.dracodev.net", hote
|
||||
assert draco_urls.obsigate_url() == "https://og-dev.dracodev.net", hote
|
||||
|
||||
|
||||
def test_hote_requete_prod_noms_officiels(avec_hote):
|
||||
avec_hote("flowdeck.dracodev.net")
|
||||
urls = draco_urls.toutes()
|
||||
assert urls["obsigate"] == "https://og.dracodev.net"
|
||||
assert urls["agent"] == "https://agent.dracodev.net"
|
||||
assert urls["flowdeck"] == "https://flowdeck.dracodev.net"
|
||||
assert all("-dev" not in u for u in urls.values())
|
||||
|
||||
|
||||
def test_hote_requete_motif_ferme_aucune_derivation(avec_hote):
|
||||
# Jamais de suffixe inventé depuis un hôte arbitraire.
|
||||
for hote in ("evil-dev.dracodev.net",
|
||||
"flowdeck-dev.dracodev.net.evil.com",
|
||||
"monflowdeck-dev.dracodev.net",
|
||||
"flowdeckdev.dracodev.net",
|
||||
"flowdeck-dev.example.net"):
|
||||
avec_hote(hote)
|
||||
urls = draco_urls.toutes()
|
||||
assert urls["agent"] == "https://agent.dracodev.net", hote
|
||||
assert urls["obsigate"] == "https://og.dracodev.net", hote
|
||||
assert urls["flowdeck"] == "https://flowdeck.dracodev.net", hote
|
||||
|
||||
|
||||
def test_hote_requete_lan_repli_configuration(avec_hote, monkeypatch):
|
||||
# Hors motif (LAN, IP) : repli configuration/défauts, comme
|
||||
# hors requête — ici la dérivation par AGENT_EVENEMENTS_URL.
|
||||
avec_hote("192.168.30.101:8080")
|
||||
urls = draco_urls.toutes()
|
||||
assert urls["agent"] == "https://agent.dracodev.net"
|
||||
assert urls["obsigate"] == "https://og.dracodev.net"
|
||||
monkeypatch.setenv(
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements")
|
||||
urls = draco_urls.toutes()
|
||||
assert urls["agent"] == "https://agent-dev.dracodev.net"
|
||||
assert urls["obsigate"] == "https://og-dev.dracodev.net"
|
||||
assert urls["tiktak"] == "https://tiktak-dev.dracodev.net"
|
||||
|
||||
|
||||
def test_hote_requete_explicite_gagne_sur_hote(avec_hote, monkeypatch):
|
||||
avec_hote("flowdeck-dev.dracodev.net")
|
||||
# Explicites prioritaires, y compris quand elles pointent le
|
||||
# nom officiel ou un domaine propre.
|
||||
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent.dracodev.net")
|
||||
monkeypatch.setenv("DRACO_SSO_URL", "https://og.example.net")
|
||||
urls = draco_urls.toutes()
|
||||
assert urls["agent"] == "https://agent.dracodev.net"
|
||||
assert urls["obsigate"] == "https://og.example.net"
|
||||
# Non déclarées : l'hôte de la requête fait foi.
|
||||
assert urls["tiktak"] == "https://tiktak-dev.dracodev.net"
|
||||
assert urls["kanbante"] == "https://kanbante-dev.dracodev.net"
|
||||
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
|
||||
|
||||
|
||||
def test_hote_requete_bouton_sso_tout_dev(avec_hote):
|
||||
from app.routers.auth import _sso_login_url
|
||||
|
||||
avec_hote("flowdeck-dev.dracodev.net")
|
||||
# Émettrice via DRACO_SSO_URL résolu par l'hôte + callback =
|
||||
# propre origine résolue de l'instance.
|
||||
assert _sso_login_url() == (
|
||||
"https://og-dev.dracodev.net/sso/authorize?return_to="
|
||||
"https%3A%2F%2Fflowdeck-dev.dracodev.net%2Fauth%2Fdraco")
|
||||
|
||||
|
||||
def test_http_dev_sans_env_tout_en_dev(client):
|
||||
# Requête complète, aucune variable d'environnement : barre,
|
||||
# panneau (config de l'iframe console), CSP et SSO en ``-dev``.
|
||||
r = client.get("/workspaces",
|
||||
headers={"Host": "flowdeck-dev.dracodev.net"})
|
||||
assert r.status_code == 200
|
||||
html = r.text
|
||||
# Barre inter-apps (les quatre sœurs).
|
||||
for soeur in ("og-dev", "flowdeck-dev", "tiktak-dev", "kanbante-dev"):
|
||||
assert f'href="https://{soeur}.dracodev.net/"' in html
|
||||
# Panneau : la source de l'iframe console vient de cette config.
|
||||
assert 'agent_url: "https://agent-dev.dracodev.net"' in html
|
||||
# CSP de la MÊME réponse : même origine agent résolue.
|
||||
csp = r.headers.get("content-security-policy", "")
|
||||
assert "frame-src 'self' https://agent-dev.dracodev.net" in csp
|
||||
assert "connect-src 'self' https://agent-dev.dracodev.net" in csp
|
||||
# Aucun nom officiel nu ne subsiste dans la réponse.
|
||||
for officiel in ("https://og.dracodev.net", "https://agent.dracodev.net",
|
||||
"https://tiktak.dracodev.net",
|
||||
"https://kanbante.dracodev.net",
|
||||
"https://flowdeck.dracodev.net"):
|
||||
assert officiel not in html.replace("-dev.dracodev.net", ""), officiel
|
||||
assert officiel not in csp.replace("-dev.dracodev.net", ""), officiel
|
||||
# Bouton SSO de la page de connexion, même requête type.
|
||||
r = client.get("/auth/login?provider=local",
|
||||
headers={"Host": "flowdeck-dev.dracodev.net"})
|
||||
assert ("https://og-dev.dracodev.net/sso/authorize?return_to="
|
||||
"https%3A%2F%2Fflowdeck-dev.dracodev.net%2Fauth%2Fdraco") in r.text
|
||||
|
||||
|
||||
def test_http_prod_noms_officiels(client):
|
||||
r = client.get("/workspaces",
|
||||
headers={"Host": "flowdeck.dracodev.net"})
|
||||
assert r.status_code == 200
|
||||
assert 'agent_url: "https://agent.dracodev.net"' in r.text
|
||||
assert 'href="https://og.dracodev.net/"' in r.text
|
||||
csp = r.headers.get("content-security-policy", "")
|
||||
assert "frame-src 'self' https://agent.dracodev.net" in csp
|
||||
assert "-dev.dracodev.net" not in r.text
|
||||
assert "-dev.dracodev.net" not in csp
|
||||
|
||||
|
||||
def test_http_lan_repli_defauts(client):
|
||||
r = client.get("/workspaces",
|
||||
headers={"Host": "192.168.30.101:8080"})
|
||||
assert r.status_code == 200
|
||||
assert 'agent_url: "https://agent.dracodev.net"' in r.text
|
||||
csp = r.headers.get("content-security-policy", "")
|
||||
assert "frame-src 'self' https://agent.dracodev.net" in csp
|
||||
|
||||
|
||||
def test_http_explicite_gagne_sur_hote(client, monkeypatch):
|
||||
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent.dracodev.net")
|
||||
r = client.get("/workspaces",
|
||||
headers={"Host": "flowdeck-dev.dracodev.net"})
|
||||
assert 'agent_url: "https://agent.dracodev.net"' in r.text
|
||||
csp = r.headers.get("content-security-policy", "")
|
||||
assert "frame-src 'self' https://agent.dracodev.net;" in csp
|
||||
# Le reste suit l'hôte de la requête.
|
||||
assert 'href="https://og-dev.dracodev.net/"' in r.text
|
||||
|
||||
Reference in New Issue
Block a user