Kanbante 0.46.2 : pont draco — consultation du Hub avant résolution/JIT, refus « aucun » sans session ni compte, marqueur de session draco signé, /admin gardé par le registre pour les sessions draco (app/main.py)
This commit is contained in:
+107
-9
@@ -263,23 +263,48 @@ def _session_secret() -> bytes:
|
||||
|
||||
|
||||
|
||||
def _make_session(user: str) -> str:
|
||||
"""Signed session cookie (0.22.0): user.expiry.hmac."""
|
||||
def _make_session(user: str, via_draco: bool = False) -> str:
|
||||
"""Signed session cookie (0.22.0): user.expiry.hmac.
|
||||
|
||||
HUB-19 (0.46.2) : une session **établie par le pont draco**
|
||||
porte un segment ``draco`` couvert par la signature
|
||||
(``user.expiry.draco.hmac``) — c'est ce marqueur infalsifiable
|
||||
qui soumet ensuite ``/admin`` au registre des rôles du Hub
|
||||
(voir ``_require_admin``). Les sessions natives gardent le
|
||||
format historique, et les deux formats restent lisibles.
|
||||
"""
|
||||
exp = int(time.time()) + SESSION_DAYS * 86400
|
||||
charge = f"{user}.{exp}.draco" if via_draco else f"{user}.{exp}"
|
||||
sig = hmac.new(
|
||||
_session_secret(), f"{user}.{exp}".encode("utf-8"), hashlib.sha256
|
||||
_session_secret(), charge.encode("utf-8"), hashlib.sha256
|
||||
).hexdigest()
|
||||
return f"{user}.{exp}.{sig}"
|
||||
return f"{charge}.{sig}"
|
||||
|
||||
|
||||
def _session_user(request: Request) -> str | None:
|
||||
def _session_info(request: Request) -> tuple[str, bool] | None:
|
||||
"""(utilisateur, établie par draco) de la session, ou None.
|
||||
|
||||
Vérifie signature et expiration, dans les deux formats
|
||||
(natif historique et marqué draco de la 0.46.2).
|
||||
"""
|
||||
raw = request.cookies.get(SESSION_COOKIE, "")
|
||||
user, _, rest = raw.partition(".")
|
||||
exp, _, sig = rest.partition(".")
|
||||
morceaux = raw.split(".")
|
||||
if len(morceaux) < 3:
|
||||
return None
|
||||
sig = morceaux[-1]
|
||||
via_draco = len(morceaux) >= 4 and morceaux[-2] == "draco"
|
||||
if via_draco:
|
||||
exp = morceaux[-3]
|
||||
user = ".".join(morceaux[:-3])
|
||||
charge = f"{user}.{exp}.draco"
|
||||
else:
|
||||
exp = morceaux[-2]
|
||||
user = ".".join(morceaux[:-2])
|
||||
charge = f"{user}.{exp}"
|
||||
if not user or not exp or not sig:
|
||||
return None
|
||||
expected = hmac.new(
|
||||
_session_secret(), f"{user}.{exp}".encode("utf-8"), hashlib.sha256
|
||||
_session_secret(), charge.encode("utf-8"), hashlib.sha256
|
||||
).hexdigest()
|
||||
if not secrets.compare_digest(sig, expected):
|
||||
return None
|
||||
@@ -288,6 +313,14 @@ def _session_user(request: Request) -> str | None:
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
return user, via_draco
|
||||
|
||||
|
||||
def _session_user(request: Request) -> str | None:
|
||||
info = _session_info(request)
|
||||
if info is None:
|
||||
return None
|
||||
user = info[0]
|
||||
# 0.24.0 : la session peut appartenir à n'importe quel
|
||||
# compte de la base (résolu ensuite par _current_user) ou,
|
||||
# en mode env pur, à l'opérateur d'environnement.
|
||||
@@ -299,6 +332,12 @@ def _session_user(request: Request) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _session_via_draco(request: Request) -> bool:
|
||||
"""Vrai si la session présentée a été établie par le pont draco."""
|
||||
info = _session_info(request)
|
||||
return bool(info and info[1] and _session_user(request) is not None)
|
||||
|
||||
|
||||
def _basic_ok(request: Request) -> bool:
|
||||
settings = get_settings()
|
||||
header = request.headers.get("authorization", "")
|
||||
@@ -597,9 +636,21 @@ def draco_bridge(request: Request, next: str = "/"):
|
||||
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``.
|
||||
|
||||
HUB-19 (0.46.2, contrat D3(a)) : le registre des rôles du Hub
|
||||
est consulté **en premier** — avant la liaison à une session
|
||||
native, avant la résolution de la correspondance et avant
|
||||
toute création JIT. Rôle connu ``aucun`` → le pont refuse :
|
||||
aucune session Kanbante n'est ouverte par draco pour ce
|
||||
``sub`` et aucun compte n'est créé. Rôle inconnu (Hub
|
||||
injoignable) → comportement actuel, inchangé. Les autres
|
||||
accès ne passent pas par ce pont et ne sont pas touchés :
|
||||
session native préexistante, connexion native (mot de
|
||||
passe), Basic d'environnement, jetons ``kbt_``.
|
||||
"""
|
||||
from app import accounts as accounts_service
|
||||
from app import draco
|
||||
from app import hub_roles
|
||||
|
||||
settings = get_settings()
|
||||
claims = draco.claims_for(request)
|
||||
@@ -608,6 +659,28 @@ def draco_bridge(request: Request, next: str = "/"):
|
||||
sub = str(claims.get("sub") or "")
|
||||
if not sub:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
# HUB-19 : consultation du registre AVANT toute résolution
|
||||
# ou création (resolve_draco_user peut créer le compte JIT :
|
||||
# un sub refusé ne doit jamais en recevoir un).
|
||||
role_hub = hub_roles.role_pour(
|
||||
request.headers.get("cookie", ""), sub,
|
||||
hote=request.headers.get("host"),
|
||||
)
|
||||
if role_hub == "aucun":
|
||||
return render(
|
||||
request, "error.html", status_code=403,
|
||||
message=(
|
||||
"Accès refusé : le registre des rôles de la suite "
|
||||
"(Hub Dracodev) attribue le niveau « aucun » à "
|
||||
"votre compte pour Kanbante — l'ouverture de "
|
||||
"session par draco est refusée et aucun compte "
|
||||
"Kanbante n'a été créé. L'administrateur de la "
|
||||
"suite peut modifier ce niveau dans la matrice "
|
||||
"des rôles du Hub. Vos autres accès à Kanbante "
|
||||
"(connexion par mot de passe, session déjà "
|
||||
"ouverte) ne sont pas touchés."
|
||||
),
|
||||
)
|
||||
# Déjà connecté nativement : lier le sub à ce compte.
|
||||
current = _current_user(request)
|
||||
if current is not None and current.get("id") is not None:
|
||||
@@ -618,12 +691,15 @@ def draco_bridge(request: Request, next: str = "/"):
|
||||
return RedirectResponse(_safe_next(next), status_code=303)
|
||||
# Correspondance, amorçage ou création JIT : ouvrir la
|
||||
# session du compte que ce sub désigne — et seulement lui.
|
||||
# La session est marquée « établie par draco » (segment signé,
|
||||
# voir _make_session) : c'est elle, et elle seule, que le
|
||||
# garde de /admin soumettra au registre du Hub.
|
||||
user = accounts_service.resolve_draco_user(claims, provisionner=True)
|
||||
if user is not None:
|
||||
response = RedirectResponse(_safe_next(next), status_code=303)
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE,
|
||||
_make_session(user["username"]),
|
||||
_make_session(user["username"], via_draco=True),
|
||||
max_age=SESSION_DAYS * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
@@ -1006,6 +1082,28 @@ def _require_admin(request: Request) -> dict | None:
|
||||
user = _current_user(request)
|
||||
if user is None or user.get("role") != "admin":
|
||||
return None
|
||||
# HUB-19 (0.46.2) : pour une session **établie par le pont
|
||||
# draco** (marqueur signé, voir _make_session), si le rôle du
|
||||
# registre du Hub est connu, il faut AUSSI « administrateur »
|
||||
# dans le registre — en plus du rôle local ci-dessus. Rôle
|
||||
# inconnu (Hub injoignable) ou session native (mot de passe,
|
||||
# Basic, jeton) : règles locales actuelles, inchangées. Ce
|
||||
# garde couvre /admin et toutes ses actions (même fonction).
|
||||
if _session_via_draco(request):
|
||||
from app import draco
|
||||
from app import hub_roles
|
||||
|
||||
sub = str(user.get("draco_sub") or "")
|
||||
if not sub:
|
||||
claims = draco.claims_for(request)
|
||||
sub = str((claims or {}).get("sub") or "")
|
||||
if sub:
|
||||
role_hub = hub_roles.role_pour(
|
||||
request.headers.get("cookie", ""), sub,
|
||||
hote=request.headers.get("host"),
|
||||
)
|
||||
if role_hub is not None and role_hub != "administrateur":
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user