Carte : fusion #130 — HUB-16 — Kanbante : correspondance par utilisateur (plan Hub v2.1, §2.2 mécanisme 3 et §4 ; décision D5 de Bruno). Version visée : 0.46.0 (mineur : c'est une fonction — patron du dépôt : fonctions en mineur comme 0.24.0/0.25.0, les 0.45.x étaient vendanges et correctifs). Base revérifiée : production et main distant en 0.45.12 (37aa83ee, aucune collision de numéro).
Le problème
Le pont SSO draco_session (ACC-3.2) était mono-utilisateur par hypothèse : sans correspondance, un jeton draco valide ouvrait la session du compte unique — ou, sans aucun compte, celle de l'opérateur d'environnement — et /api/search acceptait tout jeton valide « en tant qu'opérateur ». Le critère « un compte créé fonctionne dans toutes les applications » ne pouvait pas tenir dès le deuxième utilisateur.
Modèle de correspondance retenu
La correspondance vit dans le modèle existant : la colonne users.draco_sub (texte, UNIQUE, présente depuis la 0.24.0) porte le sub draco de chaque compte ; models.get_user_by_draco(sub) était déjà la résolution canonique. Aucune nouvelle table : le patron était déjà là, il manquait la décision et le provisionnement. La règle complète est centralisée dans app.accounts.resolve_draco_user(), seul point d'entrée du pont /auth/draco et du contrôle draco de /api/search :
session native ouverte + sub libre → liaison à ce compte (première visite doublement connectée, inchangé) ;
sub lié → session de son compte (refus si désactivé, sans contournement) ;
amorçage (ci-dessous) ;
sinon création JIT.
JIT : nom d'utilisateur = slug du nom affiché des claims, suffixé en cas de collision (jamais d'adoption d'un compte non lié, même homonyme — le JIT crée toujours un compte nouveau) ; rôle membre (utilisateur standard, jamais admin) ; mot de passe local = secret aléatoire haché, jamais divulgué (l'accès normal passe par draco ; un admin peut poser un mot de passe local via « Réinitialiser ») ; membre de l'espace Principal ; journalisé draco.jit.
Liaison par l'administrateur (comptes préexistants) : colonne « Draco (sub) » de l'écran d'administration existant /admin — saisir le sub, « Lier » ; « Délier » pour retirer ; refus si le sub est déjà lié à un autre compte ; journalisé draco.link.admin / draco.unlink.admin. Choix du patron existant plutôt qu'un nouvel écran : /admin gérait déjà les utilisateurs (rôle, activation, réinitialisation) et affichait déjà l'état de liaison — c'est son prolongement naturel, un seul endroit pour administrer les comptes. Procédure et modèle documentés dans docs/correspondance-draco.md. Le registre matriciel côté Hub viendra avec HUB-19 : il lira cette correspondance, dont Kanbante reste la source de vérité locale.
Traitement du compte d'amorçage existant
S'il n'existe qu'un seul compte, que c'est un admin actif sans liaison, le premier sub valide s'y lie au premier passage (journalisé draco.link.amorcage) et ouvre sa session : le compte actuel de l'opérateur (créé depuis l'environnement) garde son accès, sans verrouillage. Restriction délibérée par rapport à l'ancien code, qui liait le compte unique quel que soit son rôle : un unique compte membre n'est plus « adopté » par la première identité draco venue — elle reçoit son propre compte JIT. Voies de secours inchangées dans tous les cas : mot de passe du compte, repli environnement (formulaire et Basic), et liaison manuelle par l'administrateur sur son propre compte (permise explicitement, contrairement au rôle/à l'activation).
Changement corrélé : sans aucun compte en base, un jeton draco n'ouvre plus la session de l'opérateur d'environnement — il crée un compte membre JIT (en pratique ce cas n'existe pas : l'amorçage crée l'admin au démarrage dès que l'authentification est configurée).
/api/search
Le cookie draco n'y vaut plus « opérateur » : le sub doit désigner un compte (correspondance ou amorçage lié au passage). Aucun JIT sur cette surface — la création a lieu à la première vraie connexion, par le pont.
Non-régressions vérifiées
Jetons personnels kbt_ : création, usage /mcp et /api/, révocation, refus sur l'interface web — inchangés (tests existants + un test ciblé sous authentification active).
Sessions propres Kanbante et connexion native, liaison à la première visite doublement connectée incluse : inchangées.
Basic (environnement) : inchangé. Pont inerte sans DRACO_SESSION_SECRET : inchangé (tests existants).
Compte désactivé : refusé par le pont comme par les jetons ; la correspondance existante interdit tout contournement JIT.
Réconciliation Kanbante/Gitea par étiquettes : non touchée.
Tests
Nouveaux : tests/test_draco_correspondance.py — 8 tests : amorçage unique admin ; deux identités → deux comptes distincts (+ retour d'une identité sur le même compte) ; JIT sans adoption d'un homonyme non lié ; compte désactivé refusé sans contournement ; liaison/déliaison admin + conflit de sub refusé ; liaison refusée aux non-admins ; /api/search par utilisateur ; jeton kbt_ toujours accepté.
Comparaison branche vs main vierge, même environnement, même interpréteur (venv neuf, proxies de la VM retirés — no_proxy casse httpx) : main vierge 387 réussis / 1 échec ; branche 395 réussis / 1 échec. L'écart est exactement les 8 nouveaux tests. L'échec unique est identique des deux côtés et préexistant : tests/test_mobile.py::test_menu_compte_draco_fonctionnel (il attend la branche profil du menu compte, retirée en 0.45.10 par la barre v0.3.0 — hors périmètre de cette carte).
python -m py_compile app/*.py : OK (étape CI).
Reste à faire (hors de cette PR)
Fusion sur l'ordre de Bruno, puis release v0.46.0 et redéploiement (règle dev : la publication Gitea déploie).
Mise à jour de la section d'aide du Hub (guide Kanbante) pour la liaison administrateur — chantier Hub séparé, à grouper avec les autres cartes de la vague B.
🤖 Carte fusion #130 — aucune fusion effectuée par l'agent, la carte avance en Révision seulement.
**Carte : fusion #130 — HUB-16 — Kanbante : correspondance par utilisateur** (plan Hub v2.1, §2.2 mécanisme 3 et §4 ; décision D5 de Bruno). Version visée : **0.46.0** (mineur : c'est une fonction — patron du dépôt : fonctions en mineur comme 0.24.0/0.25.0, les 0.45.x étaient vendanges et correctifs). Base revérifiée : production et `main` distant en **0.45.12** (`37aa83ee`, aucune collision de numéro).
## Le problème
Le pont SSO `draco_session` (ACC-3.2) était mono-utilisateur par hypothèse : sans correspondance, un jeton draco valide ouvrait la session du compte unique — ou, sans aucun compte, celle de l'opérateur d'environnement — et `/api/search` acceptait tout jeton valide « en tant qu'opérateur ». Le critère « un compte créé fonctionne dans toutes les applications » ne pouvait pas tenir dès le deuxième utilisateur.
## Modèle de correspondance retenu
La correspondance vit **dans le modèle existant** : la colonne **`users.draco_sub`** (texte, `UNIQUE`, présente depuis la 0.24.0) porte le `sub` draco de chaque compte ; `models.get_user_by_draco(sub)` était déjà la résolution canonique. Aucune nouvelle table : le patron était déjà là, il manquait la décision et le provisionnement. La règle complète est centralisée dans **`app.accounts.resolve_draco_user()`**, seul point d'entrée du pont `/auth/draco` et du contrôle draco de `/api/search` :
1. session native ouverte + `sub` libre → liaison à ce compte (première visite doublement connectée, **inchangé**) ;
2. `sub` lié → session de **son** compte (refus si désactivé, sans contournement) ;
3. amorçage (ci-dessous) ;
4. sinon **création JIT**.
**JIT** : nom d'utilisateur = slug du nom affiché des claims, suffixé en cas de collision (**jamais d'adoption** d'un compte non lié, même homonyme — le JIT crée toujours un compte nouveau) ; rôle **membre** (utilisateur standard, jamais admin) ; mot de passe local = secret aléatoire haché, **jamais divulgué** (l'accès normal passe par draco ; un admin peut poser un mot de passe local via « Réinitialiser ») ; membre de l'espace Principal ; journalisé `draco.jit`.
**Liaison par l'administrateur** (comptes préexistants) : colonne **« Draco (sub) » de l'écran d'administration existant `/admin`** — saisir le `sub`, « Lier » ; « Délier » pour retirer ; refus si le `sub` est déjà lié à un autre compte ; journalisé `draco.link.admin` / `draco.unlink.admin`. Choix du patron existant plutôt qu'un nouvel écran : `/admin` gérait déjà les utilisateurs (rôle, activation, réinitialisation) et affichait déjà l'état de liaison — c'est son prolongement naturel, un seul endroit pour administrer les comptes. Procédure et modèle documentés dans **`docs/correspondance-draco.md`**. Le registre matriciel côté Hub viendra avec HUB-19 : il lira cette correspondance, dont Kanbante reste la source de vérité locale.
## Traitement du compte d'amorçage existant
S'il n'existe qu'**un seul compte**, que c'est un **admin actif sans liaison**, le premier `sub` valide s'y lie au premier passage (journalisé `draco.link.amorcage`) et ouvre sa session : le compte actuel de l'opérateur (créé depuis l'environnement) **garde son accès, sans verrouillage**. Restriction délibérée par rapport à l'ancien code, qui liait le compte unique **quel que soit son rôle** : un unique compte *membre* n'est plus « adopté » par la première identité draco venue — elle reçoit son propre compte JIT. Voies de secours inchangées dans tous les cas : mot de passe du compte, repli environnement (formulaire et Basic), et liaison manuelle par l'administrateur sur son propre compte (permise explicitement, contrairement au rôle/à l'activation).
Changement corrélé : sans aucun compte en base, un jeton draco n'ouvre plus la session de l'opérateur d'environnement — il crée un compte membre JIT (en pratique ce cas n'existe pas : l'amorçage crée l'admin au démarrage dès que l'authentification est configurée).
## /api/search
Le cookie draco n'y vaut plus « opérateur » : le `sub` doit désigner un compte (correspondance ou amorçage lié au passage). **Aucun JIT** sur cette surface — la création a lieu à la première vraie connexion, par le pont.
## Non-régressions vérifiées
- Jetons personnels **`kbt_`** : création, usage `/mcp` et `/api/`, révocation, refus sur l'interface web — inchangés (tests existants + un test ciblé sous authentification active).
- **Sessions propres Kanbante** et connexion native, liaison à la première visite doublement connectée incluse : inchangées.
- **Basic** (environnement) : inchangé. Pont **inerte sans `DRACO_SESSION_SECRET`** : inchangé (tests existants).
- Compte **désactivé** : refusé par le pont comme par les jetons ; la correspondance existante interdit tout contournement JIT.
- Réconciliation Kanbante/Gitea par étiquettes : **non touchée**.
## Tests
- Nouveaux : `tests/test_draco_correspondance.py` — **8 tests** : amorçage unique admin ; deux identités → deux comptes distincts (+ retour d'une identité sur le même compte) ; JIT sans adoption d'un homonyme non lié ; compte désactivé refusé sans contournement ; liaison/déliaison admin + conflit de `sub` refusé ; liaison refusée aux non-admins ; `/api/search` par utilisateur ; jeton `kbt_` toujours accepté.
- Comparaison **branche vs main vierge, même environnement, même interpréteur** (venv neuf, proxies de la VM retirés — `no_proxy` casse httpx) : main vierge **387 réussis / 1 échec** ; branche **395 réussis / 1 échec**. L'écart est exactement les 8 nouveaux tests. L'échec unique est identique des deux côtés et préexistant : `tests/test_mobile.py::test_menu_compte_draco_fonctionnel` (il attend la branche `profil` du menu compte, retirée en 0.45.10 par la barre v0.3.0 — hors périmètre de cette carte).
- `python -m py_compile app/*.py` : OK (étape CI).
## Reste à faire (hors de cette PR)
- Fusion sur l'ordre de Bruno, puis release `v0.46.0` et redéploiement (règle dev : la publication Gitea déploie).
- Mise à jour de la section d'aide du Hub (guide Kanbante) pour la liaison administrateur — chantier Hub séparé, à grouper avec les autres cartes de la vague B.
🤖 Carte fusion #130 — aucune fusion effectuée par l'agent, la carte avance en Révision seulement.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Carte : fusion #130 — HUB-16 — Kanbante : correspondance par utilisateur (plan Hub v2.1, §2.2 mécanisme 3 et §4 ; décision D5 de Bruno). Version visée : 0.46.0 (mineur : c'est une fonction — patron du dépôt : fonctions en mineur comme 0.24.0/0.25.0, les 0.45.x étaient vendanges et correctifs). Base revérifiée : production et
maindistant en 0.45.12 (37aa83ee, aucune collision de numéro).Le problème
Le pont SSO
draco_session(ACC-3.2) était mono-utilisateur par hypothèse : sans correspondance, un jeton draco valide ouvrait la session du compte unique — ou, sans aucun compte, celle de l'opérateur d'environnement — et/api/searchacceptait tout jeton valide « en tant qu'opérateur ». Le critère « un compte créé fonctionne dans toutes les applications » ne pouvait pas tenir dès le deuxième utilisateur.Modèle de correspondance retenu
La correspondance vit dans le modèle existant : la colonne
users.draco_sub(texte,UNIQUE, présente depuis la 0.24.0) porte lesubdraco de chaque compte ;models.get_user_by_draco(sub)était déjà la résolution canonique. Aucune nouvelle table : le patron était déjà là, il manquait la décision et le provisionnement. La règle complète est centralisée dansapp.accounts.resolve_draco_user(), seul point d'entrée du pont/auth/dracoet du contrôle draco de/api/search:sublibre → liaison à ce compte (première visite doublement connectée, inchangé) ;sublié → session de son compte (refus si désactivé, sans contournement) ;JIT : nom d'utilisateur = slug du nom affiché des claims, suffixé en cas de collision (jamais d'adoption d'un compte non lié, même homonyme — le JIT crée toujours un compte nouveau) ; rôle membre (utilisateur standard, jamais admin) ; mot de passe local = secret aléatoire haché, jamais divulgué (l'accès normal passe par draco ; un admin peut poser un mot de passe local via « Réinitialiser ») ; membre de l'espace Principal ; journalisé
draco.jit.Liaison par l'administrateur (comptes préexistants) : colonne « Draco (sub) » de l'écran d'administration existant
/admin— saisir lesub, « Lier » ; « Délier » pour retirer ; refus si lesubest déjà lié à un autre compte ; journalisédraco.link.admin/draco.unlink.admin. Choix du patron existant plutôt qu'un nouvel écran :/admingérait déjà les utilisateurs (rôle, activation, réinitialisation) et affichait déjà l'état de liaison — c'est son prolongement naturel, un seul endroit pour administrer les comptes. Procédure et modèle documentés dansdocs/correspondance-draco.md. Le registre matriciel côté Hub viendra avec HUB-19 : il lira cette correspondance, dont Kanbante reste la source de vérité locale.Traitement du compte d'amorçage existant
S'il n'existe qu'un seul compte, que c'est un admin actif sans liaison, le premier
subvalide s'y lie au premier passage (journalisédraco.link.amorcage) et ouvre sa session : le compte actuel de l'opérateur (créé depuis l'environnement) garde son accès, sans verrouillage. Restriction délibérée par rapport à l'ancien code, qui liait le compte unique quel que soit son rôle : un unique compte membre n'est plus « adopté » par la première identité draco venue — elle reçoit son propre compte JIT. Voies de secours inchangées dans tous les cas : mot de passe du compte, repli environnement (formulaire et Basic), et liaison manuelle par l'administrateur sur son propre compte (permise explicitement, contrairement au rôle/à l'activation).Changement corrélé : sans aucun compte en base, un jeton draco n'ouvre plus la session de l'opérateur d'environnement — il crée un compte membre JIT (en pratique ce cas n'existe pas : l'amorçage crée l'admin au démarrage dès que l'authentification est configurée).
/api/search
Le cookie draco n'y vaut plus « opérateur » : le
subdoit désigner un compte (correspondance ou amorçage lié au passage). Aucun JIT sur cette surface — la création a lieu à la première vraie connexion, par le pont.Non-régressions vérifiées
kbt_: création, usage/mcpet/api/, révocation, refus sur l'interface web — inchangés (tests existants + un test ciblé sous authentification active).DRACO_SESSION_SECRET: inchangé (tests existants).Tests
tests/test_draco_correspondance.py— 8 tests : amorçage unique admin ; deux identités → deux comptes distincts (+ retour d'une identité sur le même compte) ; JIT sans adoption d'un homonyme non lié ; compte désactivé refusé sans contournement ; liaison/déliaison admin + conflit desubrefusé ; liaison refusée aux non-admins ;/api/searchpar utilisateur ; jetonkbt_toujours accepté.no_proxycasse httpx) : main vierge 387 réussis / 1 échec ; branche 395 réussis / 1 échec. L'écart est exactement les 8 nouveaux tests. L'échec unique est identique des deux côtés et préexistant :tests/test_mobile.py::test_menu_compte_draco_fonctionnel(il attend la brancheprofildu menu compte, retirée en 0.45.10 par la barre v0.3.0 — hors périmètre de cette carte).python -m py_compile app/*.py: OK (étape CI).Reste à faire (hors de cette PR)
v0.46.0et redéploiement (règle dev : la publication Gitea déploie).🤖 Carte fusion #130 — aucune fusion effectuée par l'agent, la carte avance en Révision seulement.