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).
373 lines
17 KiB
Python
373 lines
17 KiB
Python
"""URLs publiques dracodev par configuration — chantier dev/prod (v7.82.0).
|
|
|
|
Règle vérifiée : aucun nom public codé en dur dans le code vivant ;
|
|
les défauts sont les noms OFFICIELS (jamais de ``-dev`` en défaut) et
|
|
chaque environnement déclare ses noms en ``-dev`` par l'environnement
|
|
(``DRACO_SSO_URL``, ``DRACO_AGENT_URL``, ``TIKTAK_URL``,
|
|
``KANBANTE_URL``, ``FLOWDECK_URL``/``OAUTH_REDIRECT_URI``…).
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.services import draco_urls
|
|
|
|
VARS = ("DRACO_SSO_URL", "DRACO_AGENT_URL", "TIKTAK_URL", "KANBANTE_URL",
|
|
"FLOWDECK_URL", "OAUTH_REDIRECT_URI", "AGENT_EVENEMENTS_URL")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _env_propre(monkeypatch):
|
|
for v in VARS:
|
|
monkeypatch.delenv(v, raising=False)
|
|
|
|
|
|
def test_defauts_officiels_sans_dev():
|
|
urls = draco_urls.toutes()
|
|
assert urls["obsigate"] == "https://og.dracodev.net"
|
|
assert urls["agent"] == "https://agent.dracodev.net"
|
|
assert urls["tiktak"] == "https://tiktak.dracodev.net"
|
|
assert urls["kanbante"] == "https://kanbante.dracodev.net"
|
|
assert urls["flowdeck"] == "https://flowdeck.dracodev.net"
|
|
assert all("-dev" not in u for u in urls.values())
|
|
|
|
|
|
def test_surcharge_dev_par_env(monkeypatch):
|
|
monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net")
|
|
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
|
|
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
|
|
monkeypatch.setenv("KANBANTE_URL", "https://kanbante-dev.dracodev.net")
|
|
monkeypatch.setenv("FLOWDECK_URL", "https://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"
|
|
|
|
|
|
def test_flowdeck_url_derivée_oauth_redirect(monkeypatch):
|
|
monkeypatch.setenv(
|
|
"OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback")
|
|
assert draco_urls.flowdeck_url() == "https://flowdeck-dev.dracodev.net"
|
|
|
|
|
|
def test_evenements_url_derivée_agent(monkeypatch):
|
|
from app.services import agent_evenements
|
|
|
|
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
|
|
assert agent_evenements._url() == \
|
|
"https://agent-dev.dracodev.net/api/v1/evenements"
|
|
monkeypatch.setenv("AGENT_EVENEMENTS_URL", "http://127.0.0.1:9/api/v1/evenements")
|
|
assert agent_evenements._url() == "http://127.0.0.1:9/api/v1/evenements"
|
|
|
|
|
|
def test_csp_agent_depuis_config(client, monkeypatch):
|
|
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
|
|
r = client.get("/")
|
|
csp = r.headers.get("content-security-policy", "")
|
|
assert "https://agent-dev.dracodev.net" in csp
|
|
assert "connect-src 'self' https://agent-dev.dracodev.net" in csp
|
|
assert "frame-src 'self' https://agent-dev.dracodev.net" in csp
|
|
assert "https://agent.dracodev.net " not in csp.replace(
|
|
"https://agent-dev.dracodev.net", "")
|
|
|
|
|
|
def test_parse_deep_link_dev_et_officiel(monkeypatch):
|
|
from app.services.external_refs import parse_deep_link
|
|
|
|
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
|
|
monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net")
|
|
# Hôte configuré (dev)…
|
|
assert parse_deep_link("https://tiktak-dev.dracodev.net/?task=7")[:3] == \
|
|
("tiktak", "task", "7")
|
|
assert parse_deep_link("https://og-dev.dracodev.net/#file=P:A.md")[:3] == \
|
|
("obsigate", "note", "P:A.md")
|
|
# …et hôte officiel toujours reconnu (références déjà stockées).
|
|
assert parse_deep_link("https://tiktak.dracodev.net/?task=7")[:3] == \
|
|
("tiktak", "task", "7")
|
|
assert parse_deep_link("https://og.dracodev.net/#file=P:A.md")[:3] == \
|
|
("obsigate", "note", "P:A.md")
|
|
|
|
|
|
def test_url_affichage_reecrit_hote_sans_toucher_stockage(monkeypatch):
|
|
from app.services.external_refs import url_affichage
|
|
|
|
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
|
|
stockee = "https://tiktak.dracodev.net/?task=42"
|
|
assert url_affichage(stockee) == "https://tiktak-dev.dracodev.net/?task=42"
|
|
# La fonction est pure : la valeur d'entrée (celle en base) ne bouge pas.
|
|
assert stockee == "https://tiktak.dracodev.net/?task=42"
|
|
assert url_affichage("https://example.com/x") == "https://example.com/x"
|
|
|
|
|
|
def test_preset_tiktak_depuis_config(monkeypatch):
|
|
from app.services import connectors
|
|
|
|
assert connectors.PRESETS["tiktak"]["url"] == \
|
|
"https://tiktak.dracodev.net/mcp"
|
|
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
|
|
assert connectors.PRESETS["tiktak"]["url"] == \
|
|
"https://tiktak-dev.dracodev.net/mcp"
|
|
|
|
|
|
def test_derivation_dev_depuis_agent_evenements_seul(monkeypatch):
|
|
# Cas réel de la dev : seul AGENT_EVENEMENTS_URL est posé —
|
|
# les quatre URLs sœurs servies suivent en ``-dev``, sans
|
|
# déclarer quatre variables de plus. Gitea ne change pas.
|
|
monkeypatch.setenv(
|
|
"AGENT_EVENEMENTS_URL",
|
|
"https://agent-dev.dracodev.net/api/v1/evenements")
|
|
urls = draco_urls.toutes()
|
|
assert urls["obsigate"] == "https://og-dev.dracodev.net"
|
|
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
|
|
assert urls["tiktak"] == "https://tiktak-dev.dracodev.net"
|
|
assert urls["kanbante"] == "https://kanbante-dev.dracodev.net"
|
|
assert urls["gitea"] == "https://git.dracodev.net"
|
|
# Les accesseurs individuels partagent la même résolution.
|
|
assert draco_urls.obsigate_url() == "https://og-dev.dracodev.net"
|
|
assert draco_urls.flowdeck_url() == "https://flowdeck-dev.dracodev.net"
|
|
assert draco_urls.tiktak_url() == "https://tiktak-dev.dracodev.net"
|
|
assert draco_urls.kanbante_url() == "https://kanbante-dev.dracodev.net"
|
|
|
|
|
|
def test_derivation_explicite_gagne_sur_derivation(monkeypatch):
|
|
monkeypatch.setenv(
|
|
"AGENT_EVENEMENTS_URL",
|
|
"https://agent-dev.dracodev.net/api/v1/evenements")
|
|
# Explicites (contrat inchangé) : elles priment sur la dérivation,
|
|
# y compris quand elles pointent le nom officiel.
|
|
monkeypatch.setenv("DRACO_SSO_URL", "https://og.dracodev.net")
|
|
monkeypatch.setenv("TIKTAK_URL", "https://tiktak.example.net")
|
|
urls = draco_urls.toutes()
|
|
assert urls["obsigate"] == "https://og.dracodev.net"
|
|
assert urls["tiktak"] == "https://tiktak.example.net"
|
|
# Non déclarées : dérivées.
|
|
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
|
|
assert urls["kanbante"] == "https://kanbante-dev.dracodev.net"
|
|
|
|
|
|
def test_derivation_hote_agent_hors_motif_defauts_officiels(monkeypatch):
|
|
# Autre domaine, LAN, IP : aucune dérivation, jamais un lien deviné.
|
|
for url_agent in (
|
|
"https://agent.example.com/api/v1/evenements",
|
|
"https://agent-dev.example.net/api/v1/evenements",
|
|
"https://monagent-dev.dracodev.net/api/v1/evenements",
|
|
"https://agent-dev.dracodev.net.evil.com/api/v1/evenements",
|
|
"http://192.168.30.101:8790/api/v1/evenements",
|
|
"http://127.0.0.1:8790/api/v1/evenements",
|
|
"http://agent-dev.local:8790/api/v1/evenements",
|
|
):
|
|
monkeypatch.setenv("AGENT_EVENEMENTS_URL", url_agent)
|
|
urls = draco_urls.toutes()
|
|
assert urls["obsigate"] == "https://og.dracodev.net", url_agent
|
|
assert urls["flowdeck"] == "https://flowdeck.dracodev.net", url_agent
|
|
assert urls["tiktak"] == "https://tiktak.dracodev.net", url_agent
|
|
assert urls["kanbante"] == "https://kanbante.dracodev.net", url_agent
|
|
|
|
|
|
def test_derivation_agent_officiel_suffixe_vide(monkeypatch):
|
|
# Hôte agent officiel : suffixe vide, les sœurs restent officielles.
|
|
monkeypatch.setenv(
|
|
"AGENT_EVENEMENTS_URL", "https://agent.dracodev.net/api/v1/evenements")
|
|
urls = draco_urls.toutes()
|
|
assert urls["obsigate"] == "https://og.dracodev.net"
|
|
assert urls["flowdeck"] == "https://flowdeck.dracodev.net"
|
|
assert urls["tiktak"] == "https://tiktak.dracodev.net"
|
|
assert urls["kanbante"] == "https://kanbante.dracodev.net"
|
|
|
|
|
|
def test_bouton_sso_login_depuis_config(monkeypatch):
|
|
from app.routers.auth import _sso_login_url
|
|
|
|
assert _sso_login_url() == (
|
|
"https://og.dracodev.net/sso/authorize?return_to="
|
|
"https%3A%2F%2Fflowdeck.dracodev.net%2Fauth%2Fdraco")
|
|
monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net")
|
|
monkeypatch.setenv(
|
|
"OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback")
|
|
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
|