Kanbante 0.47.0 — HUB-23 : métadonnées des jetons kbt_ pour le registre du Hub (GET /api/jetons/metadonnees, cookie draco relayé, métadonnées seulement)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user