Files
kanbante/tests/test_hub_jetons.py
T

146 lines
5.3 KiB
Python

"""HUB-23 — métadonnées des jetons `kbt_` pour le registre du Hub.
Route `GET /api/jetons/metadonnees` : le Hub l'interroge côté
serveur en relayant le cookie `draco_session` de l'administrateur
connecté (patron /api/search, correspondance HUB-16, aucun JIT).
Couvert ici :
- sans cookie draco, ou cookie sans correspondance : 401 ;
- un membre ne reçoit que ses propres jetons ;
- un administrateur Kanbante reçoit tous les jetons (portée
« tous »), propriétaires joints ;
- la réponse ne contient que des métadonnées : ni valeur de jeton,
ni empreinte SHA-256 (`token_hash`), sous aucune clé ;
- l'état distingue actif / révoqué ; le préfixe est `kbt_`.
La base de test est partagée par toute la session (voir conftest) :
les comptes créés ici portent des identifiants uniques « hub23-… »,
liés par `draco_sub` posé directement, et sont supprimés au
teardown de chaque test.
"""
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 accounts, draco, models
CLE_TEST = "hub23-" + "cle-de-test"
def _jeton(sub, nom="Alice Exemple", secret=CLE_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": nom, "iat": int(time.time()),
"exp": int(time.time()) + 3600, "jti": f"jti-{sub}",
}).encode())
sig = hmac.new(secret.encode(), f"{header}.{payload}".encode(),
hashlib.sha256).digest()
return f"{header}.{payload}.{b64(sig)}"
def _creer_lie(username: str, sub: str, role: str = "member") -> dict:
uid = models.create_user(
username, accounts.hash_password("motdepasse-local"),
role=role, display_name=username,
)
models.update_user(uid, draco_sub=sub)
return models.get_user(uid)
def _nettoyer():
with models.get_db() as conn:
ids = [
r[0] for r in conn.execute(
"SELECT id FROM users WHERE username LIKE 'hub23-%'"
).fetchall()
]
for uid in ids:
conn.execute("DELETE FROM api_tokens WHERE user_id = ?", (uid,))
conn.execute(
"DELETE FROM workspace_members WHERE user_id = ?", (uid,)
)
conn.execute("DELETE FROM users WHERE id = ?", (uid,))
@pytest.fixture()
def client_hub23(monkeypatch):
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
monkeypatch.setenv("DRACO_SESSION_SECRET", CLE_TEST)
monkeypatch.setattr(draco, "_refresh_revocations", lambda: None)
draco._revoked_cache.clear()
_nettoyer()
with TestClient(main.app) as client:
yield client
_nettoyer()
draco._revoked_cache.clear()
def test_sans_cookie_ou_sans_correspondance_401(client_hub23):
# Un compte lié existe d'abord : l'amorçage de l'unique compte
# admin (patron /api/search) ne peut pas lier le sub inconnu.
_creer_lie("hub23-temoin", "hub23-temoin")
assert client_hub23.get("/api/jetons/metadonnees").status_code == 401
client_hub23.cookies.set(
"draco_session", _jeton("hub23-inconnu", "Sans Lien")
)
r = client_hub23.get("/api/jetons/metadonnees")
assert r.status_code == 401
def test_membre_ne_voit_que_ses_jetons(client_hub23):
membre = _creer_lie("hub23-membre", "hub23-membre")
autre = _creer_lie("hub23-autre", "hub23-autre")
row, _clair = models.create_api_token(membre["id"], "service-agents")
models.create_api_token(autre["id"], "jeton-d-autrui")
models.revoke_api_token(membre["id"], row["id"])
_row2, _clair2 = models.create_api_token(membre["id"], "mcp-local")
client_hub23.cookies.set(
"draco_session", _jeton("hub23-membre", "Membre Exemple")
)
r = client_hub23.get("/api/jetons/metadonnees")
assert r.status_code == 200
corps = r.json()
assert corps["portee"] == "propres"
noms = {j["nom"]: j for j in corps["jetons"]}
assert set(noms) == {"service-agents", "mcp-local"}
assert noms["service-agents"]["etat"] == "revoque"
assert noms["mcp-local"]["etat"] == "actif"
assert noms["mcp-local"]["proprietaire"] == "hub23-membre"
assert noms["mcp-local"]["prefixe"] == "kbt_"
assert noms["mcp-local"]["cree_le"]
# Aucune valeur, aucune empreinte, sous aucune forme.
texte = json.dumps(corps)
assert "token_hash" not in texte
assert _clair not in texte and _clair2 not in texte
def test_admin_voit_tous_les_jetons(client_hub23):
admin = _creer_lie("hub23-admin", "hub23-admin", role="admin")
membre = _creer_lie("hub23-zoe", "hub23-zoe")
models.create_api_token(admin["id"], "jeton-admin")
models.create_api_token(membre["id"], "jeton-zoe")
client_hub23.cookies.set(
"draco_session", _jeton("hub23-admin", "Admin Exemple")
)
r = client_hub23.get("/api/jetons/metadonnees")
assert r.status_code == 200
corps = r.json()
assert corps["portee"] == "tous"
par_nom = {j["nom"]: j for j in corps["jetons"]}
assert par_nom["jeton-admin"]["proprietaire"] == "hub23-admin"
assert par_nom["jeton-zoe"]["proprietaire"] == "hub23-zoe"
assert "token_hash" not in json.dumps(corps)