Flowdeck 7.85.0 — HUB-15 : provisionnement JIT des comptes draco (fusion #129)
FlowDeck CI / test (push) Failing after 3s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 13s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (pull_request) Failing after 12s
FlowDeck CI / test (push) Failing after 3s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 13s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (pull_request) Failing after 12s
Généralisation de maybe_link : à la première connexion draco, si aucun compte local ne correspond au sub (ni lien draco_links, ni repli mono-utilisateur), le pendant local est créé depuis les claims (sub, name) et lié dans la même transaction. Rôle par défaut sûr : utilisateur standard (is_admin=0), auth_method 'draco', password_hash NULL (aucune connexion locale par mot de passe pour ce compte ; inchangée pour les autres). Garde pierre tombale : une ligne draco_links existante interdit toute création. Connexion locale et OAuth Gitea/GitHub intacts. Tests : test_fusion_draco.py étendu (création JIT, pierre tombale) ; 9/9 sur le fichier, balayage auth 226 réussis (3 échecs préexistants sur main vierge, réseau).
This commit is contained in:
+1
-1
@@ -216,7 +216,7 @@ def _lire_version() -> str:
|
||||
|
||||
|
||||
# Littéral de version synchronisé avec le fichier VERSION :
|
||||
# version="7.84.3"
|
||||
# version="7.85.0"
|
||||
# Patron historique des bumps (voir scripts/bump_version.py, qui cherche
|
||||
# ce littéral) et contrôle de tests/test_pwa_offline.py. La version
|
||||
# runtime reste lue dynamiquement depuis VERSION par _lire_version()
|
||||
|
||||
+85
-1
@@ -15,6 +15,17 @@ local : table ``draco_links`` (migration 51), créée automatiquement
|
||||
coexistent (association 1:1, dissociation = pierre tombale) ; à
|
||||
défaut de lien, une instance à un seul utilisateur actif associe ce
|
||||
sub à cet unique compte. Sans secret configuré, module inerte.
|
||||
|
||||
HUB-15 (plan Hub v2.1, §2.2) — provisionnement JIT : si aucun compte
|
||||
local ne correspond au ``sub`` (ni lien, ni repli mono-utilisateur),
|
||||
le pendant local est **créé automatiquement** à la première
|
||||
connexion draco, depuis les claims (``sub``, ``name``), avec le rôle
|
||||
par défaut sûr : utilisateur standard (``is_admin = 0``, jamais
|
||||
admin), aucun mot de passe local (``password_hash`` NULL,
|
||||
``auth_method = 'draco'`` — l'authentification passe par draco, la
|
||||
connexion locale par mot de passe reste impossible pour ce compte
|
||||
et inchangée pour les autres). Une pierre tombale (dissociation
|
||||
explicite) interdit toute re-création automatique pour ce ``sub``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -138,8 +149,79 @@ def _linked_user(conn, sub: str) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _jit_login(sub: str) -> str:
|
||||
"""Login local déterministe dérivé du ``sub`` draco.
|
||||
|
||||
Préfixe ``draco-`` (aucune collision possible avec un login
|
||||
choisi par un humain ou par l'OAuth, préfixés par fournisseur),
|
||||
caractères restreints à ``[a-z0-9._-]``.
|
||||
"""
|
||||
nettoye = "".join(
|
||||
c if (c.isascii() and (c.isalnum() or c in "._-")) else "-"
|
||||
for c in sub.strip().lower()
|
||||
).strip("-")
|
||||
return f"draco-{nettoye}" if nettoye else "draco-compte"
|
||||
|
||||
|
||||
def _provision_jit(conn, claims: dict) -> dict | None:
|
||||
"""Crée le pendant local d'un ``sub`` draco inconnu (HUB-15).
|
||||
|
||||
Création + liaison dans la même transaction. Retourne le nouvel
|
||||
utilisateur, ou None si la création ne doit pas avoir lieu :
|
||||
une ligne ``draco_links`` existe déjà pour ce ``sub`` (lien actif
|
||||
— déjà résolu par ``_linked_user`` —, compte lié inactif, ou
|
||||
pierre tombale d'une dissociation explicite : on ne recrée
|
||||
jamais automatiquement derrière une dissociation).
|
||||
"""
|
||||
sub = str(claims.get("sub", ""))
|
||||
if not sub:
|
||||
return None
|
||||
deja = conn.execute(
|
||||
"SELECT user_id FROM draco_links WHERE sub = ?", (sub,)
|
||||
).fetchone()
|
||||
if deja is not None:
|
||||
return None
|
||||
nom = str(claims.get("name", "") or "").strip()
|
||||
login = _jit_login(sub)
|
||||
# Collision résiduelle : un login identique pris par un autre
|
||||
# compte (non lié) — on suffixe par une empreinte courte du sub
|
||||
# plutôt que de lier ou d'écraser quoi que ce soit.
|
||||
if conn.execute(
|
||||
"SELECT id FROM users WHERE login = ?", (login,)
|
||||
).fetchone() is not None:
|
||||
suffixe = hashlib.sha256(sub.encode("utf-8")).hexdigest()[:8]
|
||||
login = f"{login}-{suffixe}"
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users (login, full_name, email, password_hash, "
|
||||
"is_admin, is_active, auth_method) "
|
||||
"VALUES (?, ?, '', NULL, 0, 1, 'draco')",
|
||||
(login, nom or login),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO draco_links (sub, user_id) VALUES (?, ?)",
|
||||
(sub, cur.lastrowid),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as exc: # la création ne casse jamais la requête
|
||||
logger.debug("Provisionnement JIT draco refusé: %s", exc)
|
||||
conn.rollback()
|
||||
return None
|
||||
row = conn.execute(
|
||||
"SELECT * FROM users WHERE id = ?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
logger.info("Compte local créé en JIT pour le sub draco %s (login %s)",
|
||||
sub, login)
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
def user_for_draco(request) -> dict | None:
|
||||
"""Utilisateur Flowdeck prouvé par le cookie draco_session, ou None."""
|
||||
"""Utilisateur Flowdeck prouvé par le cookie draco_session, ou None.
|
||||
|
||||
Résolution : lien ``draco_links`` existant, sinon repli
|
||||
mono-utilisateur, sinon provisionnement JIT (HUB-15) — le
|
||||
pendant local est créé depuis les claims et lié au ``sub``.
|
||||
"""
|
||||
if not enabled():
|
||||
return None
|
||||
claims = verify_token(request.cookies.get(COOKIE_NAME, ""))
|
||||
@@ -153,6 +235,8 @@ def user_for_draco(request) -> dict | None:
|
||||
return None
|
||||
with get_conn() as conn:
|
||||
user = _linked_user(conn, sub)
|
||||
if user is None:
|
||||
user = _provision_jit(conn, claims)
|
||||
if user is not None and not user.get("is_active"):
|
||||
return None
|
||||
return user
|
||||
|
||||
Reference in New Issue
Block a user