"""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