feat: Fusion — barre d'apps commune (#28) et pont SSO draco_session (#27)
CI / test (push) Successful in 1m37s

This commit is contained in:
Spark
2026-10-21 10:56:22 -04:00
parent 6066890bae
commit f087b41b4b
13 changed files with 635 additions and 3 deletions
+39
View File
@@ -0,0 +1,39 @@
"""Fusion ACC-1 — barre d'apps commune (carte kanbante#28).
Le snippet figé de dracodev-commons est vendangé (CSS/JS statiques +
partiel _draco_bar.html) et inclus par le cadre partagé _header.html :
toutes les pages applicatives l'affichent avec Kanbante comme app
courante ; l'écran de connexion, autonome, ne l'affiche pas.
"""
def test_barre_presente_sur_le_dashboard(client):
r = client.get("/")
assert r.status_code == 200
assert 'class="draco-bar"' in r.text
assert 'data-current="kanbante"' in r.text
assert "draco-barre.css" in r.text
assert "draco-barre.js" in r.text
assert 'window.DRACO_BAR_CONFIG' in r.text
# Les quatre applications de la barre, aux URL publiques.
for url in ("https://og.dracodev.net/", "https://flowdeck.dracodev.net/",
"https://tiktak.dracodev.net/", "https://kanbante.dracodev.net/"):
assert url in r.text
def test_statiques_de_la_barre_servis(client):
assert client.get("/static/css/draco-barre.css").status_code == 200
assert client.get("/static/js/draco-barre.js").status_code == 200
def test_pas_de_barre_sur_le_login(monkeypatch, fake_gitea):
from fastapi.testclient import TestClient
import app.main as main
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
with TestClient(main.app) as client:
r = client.get("/login")
assert r.status_code == 200
assert "draco-bar" not in r.text
+103
View File
@@ -0,0 +1,103 @@
"""Fusion ACC-3.2 — pont SSO draco_session (carte kanbante#27).
Le cookie émis par ObsiGate (ACC-3.1) est vérifié localement
(secret partagé, HS256) et converti en session Kanbante native par
GET /auth/draco. Kanbante est mono-utilisateur : un jeton valide
ouvre la session de l'opérateur configuré. Sans
DRACO_SESSION_SECRET, le pont renvoie simplement au login.
"""
import base64
import hashlib
import hmac
import json
import time
import pytest
from fastapi.testclient import TestClient
import app.main as main
from app import draco
SECRET = "secret-partage-de-test"
def _jeton(sub="admin-1", secret=SECRET, exp=None, jti="jti-test"):
def b64(d: bytes) -> str:
return base64.urlsafe_b64encode(d).rstrip(b"=").decode()
header = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
payload = b64(json.dumps({
"sub": sub, "name": "Bruno", "iat": int(time.time()),
"exp": exp if exp is not None else int(time.time()) + 3600,
"jti": jti,
}).encode())
sig = hmac.new(secret.encode(), f"{header}.{payload}".encode(),
hashlib.sha256).digest()
return f"{header}.{payload}.{b64(sig)}"
@pytest.fixture()
def client_sso(monkeypatch, fake_gitea):
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
monkeypatch.setenv("DRACO_SESSION_SECRET", SECRET)
monkeypatch.setattr(draco, "_refresh_revocations", lambda: None)
draco._revoked_cache.clear()
with TestClient(main.app) as client:
yield client
draco._revoked_cache.clear()
def test_verify_token(monkeypatch):
monkeypatch.setenv("DRACO_SESSION_SECRET", SECRET)
assert draco.verify_token(_jeton())["sub"] == "admin-1"
assert draco.verify_token(_jeton(secret="mauvais")) is None
assert draco.verify_token(_jeton(exp=int(time.time()) - 5)) is None
assert draco.verify_token("pas.un.jeton") is None
def test_pont_sans_cookie_vers_login(client_sso):
r = client_sso.get("/auth/draco", follow_redirects=False)
assert r.status_code == 303
assert r.headers["location"] == "/login"
def test_pont_cookie_valide_ouvre_la_session(client_sso):
client_sso.cookies.set("draco_session", _jeton())
r = client_sso.get("/auth/draco", follow_redirects=False)
assert r.status_code == 303
assert r.headers["location"] == "/"
assert "kanbante_session" in r.headers.get("set-cookie", "")
# La session native créée par le pont donne accès à l'app.
assert client_sso.get("/").status_code == 200
def test_pont_cookie_falsifie_vers_login(client_sso):
client_sso.cookies.set("draco_session", _jeton(secret="mauvais"))
r = client_sso.get("/auth/draco", follow_redirects=False)
assert r.headers["location"] == "/login"
def test_pont_jeton_revoque_refuse(client_sso):
draco._revoked_cache["jti-revoque"] = int(time.time()) + 3600
client_sso.cookies.set("draco_session", _jeton(jti="jti-revoque"))
r = client_sso.get("/auth/draco", follow_redirects=False)
assert r.headers["location"] == "/login"
def test_pont_inerte_sans_secret(monkeypatch, fake_gitea):
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
monkeypatch.delenv("DRACO_SESSION_SECRET", raising=False)
with TestClient(main.app) as client:
client.cookies.set("draco_session", _jeton())
r = client.get("/auth/draco", follow_redirects=False)
assert r.headers["location"] == "/login"
def test_bouton_sso_sur_la_page_login(client_sso):
r = client_sso.get("/login")
assert r.status_code == 200
assert "og.dracodev.net/sso/authorize" in r.text
assert "kanbante.dracodev.net%2Fauth%2Fdraco" in r.text