Kanbante 0.46.0 : pont SSO par utilisateur (fin du mono-utilisateur), /api/search par utilisateur, liaison draco par l'administrateur (app/main.py, app/draco.py)
This commit is contained in:
+6
-3
@@ -9,9 +9,12 @@ est présenté). Vérification hmac/hashlib — mêmes sémantiques que
|
|||||||
les validateurs de TikTak (0.52.0) et Flowdeck (7.74.0).
|
les validateurs de TikTak (0.52.0) et Flowdeck (7.74.0).
|
||||||
|
|
||||||
Le pont ``GET /auth/draco`` (app/main.py) convertit un cookie valide
|
Le pont ``GET /auth/draco`` (app/main.py) convertit un cookie valide
|
||||||
en session Kanbante native. Kanbante est mono-utilisateur
|
en session Kanbante native. Depuis la 0.46.0 (HUB-16), la
|
||||||
(identifiants configurés par l'environnement) : un jeton valide
|
correspondance est PAR UTILISATEUR : le ``sub`` des claims désigne
|
||||||
désigne l'opérateur configuré — pas de table de liens. Sans secret
|
le compte Kanbante lié dans ``users.draco_sub`` (résolution et
|
||||||
|
création JIT : app.accounts.resolve_draco_user) — un jeton valide
|
||||||
|
ne désigne plus « l'opérateur » par hypothèse. Ce module ne fait
|
||||||
|
que valider le jeton ; il ne connaît aucun compte. Sans secret
|
||||||
configuré, module inerte.
|
configuré, module inerte.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
+97
-49
@@ -391,15 +391,24 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
and _bearer_user(request) is not None:
|
and _bearer_user(request) is not None:
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
# Recherche partout (#68) : /api/search accepte aussi le jeton
|
# Recherche partout (#68) : /api/search accepte aussi le jeton
|
||||||
# SSO draco_session (mono-utilisateur : un jeton valide désigne
|
# SSO draco_session — depuis la 0.46.0 (HUB-16), par
|
||||||
# l'opérateur), comme les API de recherche des sœurs. Les
|
# utilisateur : le sub doit désigner un compte Kanbante
|
||||||
# preflights OPTIONS ne portent jamais de cookies : ils sont
|
# (correspondance existante ou compte d'amorçage lié au
|
||||||
# répondus par RechercheCorsMiddleware avant d'arriver ici.
|
# passage, voir app.accounts.resolve_draco_user) ; un jeton
|
||||||
|
# valide sans correspondance ne désigne plus l'opérateur et
|
||||||
|
# ne suffit plus. Aucun JIT ici : la création du compte a
|
||||||
|
# lieu à la première connexion par le pont /auth/draco.
|
||||||
|
# Les preflights OPTIONS ne portent jamais de cookies : ils
|
||||||
|
# sont répondus par RechercheCorsMiddleware avant d'arriver ici.
|
||||||
if path == "/api/search":
|
if path == "/api/search":
|
||||||
try:
|
try:
|
||||||
|
from app import accounts as accounts_service
|
||||||
from app import draco
|
from app import draco
|
||||||
|
|
||||||
if draco.claims_for(request) is not None:
|
claims = draco.claims_for(request)
|
||||||
|
if claims is not None and (
|
||||||
|
accounts_service.resolve_draco_user(claims) is not None
|
||||||
|
):
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -571,67 +580,50 @@ def draco_bridge(request: Request, next: str = "/"):
|
|||||||
"""Fusion ACC-3.2 — pont SSO : cookie ``draco_session`` → session.
|
"""Fusion ACC-3.2 — pont SSO : cookie ``draco_session`` → session.
|
||||||
|
|
||||||
Point de retour de ``GET /sso/authorize`` de l'émettrice
|
Point de retour de ``GET /sso/authorize`` de l'émettrice
|
||||||
(ObsiGate). Depuis 0.24.0 la liaison est PAR UTILISATEUR :
|
(ObsiGate). Depuis la 0.46.0 (HUB-16), la correspondance est
|
||||||
si le ``sub`` draco est lié à un compte, sa session est
|
PAR UTILISATEUR et l'hypothèse mono-utilisateur a disparu —
|
||||||
créée ; si une session native est déjà ouverte, le ``sub``
|
résolution centralisée dans app.accounts.resolve_draco_user :
|
||||||
est lié à ce compte (première visite doublement connectée) ;
|
|
||||||
sans compte lié, retour au login — la connexion native qui
|
- session native déjà ouverte : le ``sub`` libre est lié à
|
||||||
suit crée le lien (voir login_submit). Sans utilisateurs en
|
ce compte (première visite doublement connectée, inchangé) ;
|
||||||
base, comportement historique : session de l'opérateur.
|
- ``sub`` lié à un compte : sa session est ouverte ;
|
||||||
|
- compte d'amorçage (unique compte, admin, sans liaison) :
|
||||||
|
liaison au premier passage, puis sa session — le compte
|
||||||
|
existant de l'opérateur garde son accès ;
|
||||||
|
- sinon : création JIT d'un compte propre à cette identité
|
||||||
|
(rôle membre, mot de passe local aléatoire non divulgué)
|
||||||
|
et ouverture de sa session.
|
||||||
|
|
||||||
|
Un ``sub`` inconnu n'ouvre plus jamais la session de
|
||||||
|
l'opérateur d'environnement ni celle d'un compte non lié.
|
||||||
Inerte sans ``DRACO_SESSION_SECRET``.
|
Inerte sans ``DRACO_SESSION_SECRET``.
|
||||||
"""
|
"""
|
||||||
|
from app import accounts as accounts_service
|
||||||
from app import draco
|
from app import draco
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
claims = draco.claims_for(request)
|
claims = draco.claims_for(request)
|
||||||
if not settings.auth_enabled or claims is None:
|
if not settings.auth_enabled or claims is None:
|
||||||
return RedirectResponse("/login", status_code=303)
|
return RedirectResponse("/login", status_code=303)
|
||||||
sub = claims.get("sub")
|
sub = str(claims.get("sub") or "")
|
||||||
|
if not sub:
|
||||||
|
return RedirectResponse("/login", status_code=303)
|
||||||
# Déjà connecté nativement : lier le sub à ce compte.
|
# Déjà connecté nativement : lier le sub à ce compte.
|
||||||
current = _current_user(request)
|
current = _current_user(request)
|
||||||
if current is not None and current.get("id") is not None:
|
if current is not None and current.get("id") is not None:
|
||||||
if sub and not current.get("draco_sub"):
|
if not current.get("draco_sub"):
|
||||||
if models.get_user_by_draco(sub) is None:
|
if models.get_user_by_draco(sub) is None:
|
||||||
models.update_user(current["id"], draco_sub=sub)
|
models.update_user(current["id"], draco_sub=sub)
|
||||||
models.log_admin(current["id"], "draco.link", str(sub))
|
models.log_admin(current["id"], "draco.link", sub)
|
||||||
return RedirectResponse(_safe_next(next), status_code=303)
|
return RedirectResponse(_safe_next(next), status_code=303)
|
||||||
# Compte lié à ce sub : ouvrir sa session.
|
# Correspondance, amorçage ou création JIT : ouvrir la
|
||||||
linked = models.get_user_by_draco(sub) if sub else None
|
# session du compte que ce sub désigne — et seulement lui.
|
||||||
if linked is not None and linked["active"]:
|
user = accounts_service.resolve_draco_user(claims, provisionner=True)
|
||||||
|
if user is not None:
|
||||||
response = RedirectResponse(_safe_next(next), status_code=303)
|
response = RedirectResponse(_safe_next(next), status_code=303)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
SESSION_COOKIE,
|
SESSION_COOKIE,
|
||||||
_make_session(linked["username"]),
|
_make_session(user["username"]),
|
||||||
max_age=SESSION_DAYS * 86400,
|
|
||||||
httponly=True,
|
|
||||||
samesite="lax",
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
if models.count_users() == 1:
|
|
||||||
# Mono-utilisateur (cas historique) : le premier sub
|
|
||||||
# valide se lie au compte unique et ouvre sa session —
|
|
||||||
# c'est la sémantique livrée en 0.23.0, conservée tant
|
|
||||||
# qu'il n'y a qu'un seul compte.
|
|
||||||
sole = models.list_users()[0]
|
|
||||||
if sole["active"]:
|
|
||||||
if sub and not sole.get("draco_sub"):
|
|
||||||
if models.get_user_by_draco(sub) is None:
|
|
||||||
models.update_user(sole["id"], draco_sub=sub)
|
|
||||||
response = RedirectResponse(_safe_next(next), status_code=303)
|
|
||||||
response.set_cookie(
|
|
||||||
SESSION_COOKIE,
|
|
||||||
_make_session(sole["username"]),
|
|
||||||
max_age=SESSION_DAYS * 86400,
|
|
||||||
httponly=True,
|
|
||||||
samesite="lax",
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
if models.count_users() == 0:
|
|
||||||
# Mode env pur (aucun compte) : session de l'opérateur.
|
|
||||||
response = RedirectResponse(_safe_next(next), status_code=303)
|
|
||||||
response.set_cookie(
|
|
||||||
SESSION_COOKIE,
|
|
||||||
_make_session(settings.auth_user),
|
|
||||||
max_age=SESSION_DAYS * 86400,
|
max_age=SESSION_DAYS * 86400,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
samesite="lax",
|
samesite="lax",
|
||||||
@@ -1164,6 +1156,62 @@ async def admin_reset_user(
|
|||||||
return RedirectResponse("/admin?msg=Mot+de+passe+r%C3%A9initialis%C3%A9", status_code=303)
|
return RedirectResponse("/admin?msg=Mot+de+passe+r%C3%A9initialis%C3%A9", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/users/{user_id}/draco")
|
||||||
|
async def admin_draco_user(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
draco_sub: str = Form(""),
|
||||||
|
csrf_token: str = Form(""),
|
||||||
|
):
|
||||||
|
"""Liaison draco par l'administrateur (HUB-16, 0.46.0).
|
||||||
|
|
||||||
|
C'est le mécanisme de liaison des comptes Kanbante
|
||||||
|
préexistants : l'administrateur saisit le ``sub`` draco du
|
||||||
|
compte (colonne Draco de /admin) ; un champ vide délie.
|
||||||
|
Un ``sub`` déjà lié à un autre compte est refusé (la
|
||||||
|
correspondance est biunivoque : users.draco_sub est UNIQUE).
|
||||||
|
Contrairement au rôle et à l'activation, le geste est permis
|
||||||
|
sur son propre compte : c'est la voie manuelle qui établit la
|
||||||
|
correspondance du compte d'amorçage si le premier passage ne
|
||||||
|
l'a pas fait. Le registre matriciel côté Hub (HUB-19) lira
|
||||||
|
cette même correspondance ; ici, seul le côté Kanbante est
|
||||||
|
requis. Procédure : docs/correspondance-draco.md.
|
||||||
|
"""
|
||||||
|
from urllib.parse import quote as _q
|
||||||
|
|
||||||
|
admin = _require_admin(request)
|
||||||
|
if admin is None:
|
||||||
|
return _admin_denied(request)
|
||||||
|
if not _csrf_valid(request, csrf_token):
|
||||||
|
return _csrf_error()
|
||||||
|
target = models.get_user(user_id)
|
||||||
|
if target is None:
|
||||||
|
return RedirectResponse("/admin", status_code=303)
|
||||||
|
sub = draco_sub.strip()
|
||||||
|
if sub:
|
||||||
|
autre = models.get_user_by_draco(sub)
|
||||||
|
if autre is not None and autre["id"] != user_id:
|
||||||
|
return RedirectResponse(
|
||||||
|
"/admin?err="
|
||||||
|
+ _q("Identité draco déjà liée à un autre compte"),
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
models.update_user(user_id, draco_sub=sub)
|
||||||
|
models.log_admin(
|
||||||
|
admin.get("id"), "draco.link.admin",
|
||||||
|
f"{target['username']} : {sub}",
|
||||||
|
)
|
||||||
|
return RedirectResponse(
|
||||||
|
"/admin?msg=" + _q("Liaison draco enregistrée"),
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
models.update_user(user_id, draco_sub=None)
|
||||||
|
models.log_admin(admin.get("id"), "draco.unlink.admin", target["username"])
|
||||||
|
return RedirectResponse(
|
||||||
|
"/admin?msg=" + _q("Liaison draco retirée"), status_code=303
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/admin/connections")
|
@app.get("/admin/connections")
|
||||||
async def admin_connections(request: Request, msg: str = "", err: str = ""):
|
async def admin_connections(request: Request, msg: str = "", err: str = ""):
|
||||||
admin = _require_admin(request)
|
admin = _require_admin(request)
|
||||||
|
|||||||
Reference in New Issue
Block a user