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:
|
def _make_session(user: str, via_draco: bool = False) -> str:
|
||||||
"""Signed session cookie (0.22.0): user.expiry.hmac."""
|
"""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
|
exp = int(time.time()) + SESSION_DAYS * 86400
|
||||||
|
charge = f"{user}.{exp}.draco" if via_draco else f"{user}.{exp}"
|
||||||
sig = hmac.new(
|
sig = hmac.new(
|
||||||
_session_secret(), f"{user}.{exp}".encode("utf-8"), hashlib.sha256
|
_session_secret(), charge.encode("utf-8"), hashlib.sha256
|
||||||
).hexdigest()
|
).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, "")
|
raw = request.cookies.get(SESSION_COOKIE, "")
|
||||||
user, _, rest = raw.partition(".")
|
morceaux = raw.split(".")
|
||||||
exp, _, sig = rest.partition(".")
|
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:
|
if not user or not exp or not sig:
|
||||||
return None
|
return None
|
||||||
expected = hmac.new(
|
expected = hmac.new(
|
||||||
_session_secret(), f"{user}.{exp}".encode("utf-8"), hashlib.sha256
|
_session_secret(), charge.encode("utf-8"), hashlib.sha256
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
if not secrets.compare_digest(sig, expected):
|
if not secrets.compare_digest(sig, expected):
|
||||||
return None
|
return None
|
||||||
@@ -288,6 +313,14 @@ def _session_user(request: Request) -> str | None:
|
|||||||
return None
|
return None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
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
|
# 0.24.0 : la session peut appartenir à n'importe quel
|
||||||
# compte de la base (résolu ensuite par _current_user) ou,
|
# compte de la base (résolu ensuite par _current_user) ou,
|
||||||
# en mode env pur, à l'opérateur d'environnement.
|
# en mode env pur, à l'opérateur d'environnement.
|
||||||
@@ -299,6 +332,12 @@ def _session_user(request: Request) -> str | None:
|
|||||||
return 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:
|
def _basic_ok(request: Request) -> bool:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
header = request.headers.get("authorization", "")
|
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
|
Un ``sub`` inconnu n'ouvre plus jamais la session de
|
||||||
l'opérateur d'environnement ni celle d'un compte non lié.
|
l'opérateur d'environnement ni celle d'un compte non lié.
|
||||||
Inerte sans ``DRACO_SESSION_SECRET``.
|
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 accounts as accounts_service
|
||||||
from app import draco
|
from app import draco
|
||||||
|
from app import hub_roles
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
claims = draco.claims_for(request)
|
claims = draco.claims_for(request)
|
||||||
@@ -608,6 +659,28 @@ def draco_bridge(request: Request, next: str = "/"):
|
|||||||
sub = str(claims.get("sub") or "")
|
sub = str(claims.get("sub") or "")
|
||||||
if not sub:
|
if not sub:
|
||||||
return RedirectResponse("/login", status_code=303)
|
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.
|
# 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:
|
||||||
@@ -618,12 +691,15 @@ def draco_bridge(request: Request, next: str = "/"):
|
|||||||
return RedirectResponse(_safe_next(next), status_code=303)
|
return RedirectResponse(_safe_next(next), status_code=303)
|
||||||
# Correspondance, amorçage ou création JIT : ouvrir la
|
# Correspondance, amorçage ou création JIT : ouvrir la
|
||||||
# session du compte que ce sub désigne — et seulement lui.
|
# 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)
|
user = accounts_service.resolve_draco_user(claims, provisionner=True)
|
||||||
if user is not None:
|
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(user["username"]),
|
_make_session(user["username"], via_draco=True),
|
||||||
max_age=SESSION_DAYS * 86400,
|
max_age=SESSION_DAYS * 86400,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
samesite="lax",
|
samesite="lax",
|
||||||
@@ -1006,6 +1082,28 @@ def _require_admin(request: Request) -> dict | None:
|
|||||||
user = _current_user(request)
|
user = _current_user(request)
|
||||||
if user is None or user.get("role") != "admin":
|
if user is None or user.get("role") != "admin":
|
||||||
return None
|
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
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user