Files
kanbante/docs/coffre-reference-obsigate.md
T
2026-10-27 23:52:28 -04:00

8.4 KiB

Kanbante — Références coffre ObsiGate (Phase 4, issue ObsiGate #22)

Patron « même patron, sans variante » de la spec « Coffre de secrets ObsiGate » §12, appliqué à Kanbante sur la branche feat/coffre-reference-obsigate (aucun merge, aucune PR dans ce lot).

1. Inventaire honnête des secrets externes (vérifié dans le code, main 0.37.1)

Secret Stockage actuel Statut dans ce lot
Jeton d'accès forge (forge_connections.token_encrypted, admin + profil) Chiffré Fernet au repos (app/forge_crypto.py, clé KANBANTE_MASTER_KEY ou clé générée en base) ; jamais réaffiché (masque ••••••••) Non migré, à raison : ce n'est pas un secret en clair. Le basculer sur une référence exigerait une distribution à chaque appel forge, car le registre met en cache les clients (ForgeRegistry) : distribuer une fois à la construction puis garder le client reviendrait à conserver la valeur en mémoire pour la durée de vie de l'app, ce qui violerait le « utilise, puis oublie » de la spec. Reste à faire dédié (voir §5).
Secret webhook (forge_connections.webhook_secret, admin) En clair en base (colonne TEXT, saisie via /admin/connections) ; repli environnement KANBANTE_WEBHOOK_SECRET Migré : une référence coffre:<slug>/<nom> saisie dans ce même champ est stockée telle quelle, distribuée à chaque webhook, puis oubliée (§2). Les valeurs historiques en clair restent acceptées (compatibilité, migration opportuniste), sans jamais être réécrites.
GITEA_TOKEN, KANBANTE_NTFY_TOKEN, KANBANTE_OBSIGATE_MCP_TOKEN, KANBANTE_CAPTURE_TOKEN, AGENT_EVENEMENTS_CLE, secrets SSO/session Variables d'environnement d'exploitation uniquement, jamais persistées par l'app, non surchargeables en base (OVERRIDABLE_SETTINGS les exclut explicitement) Hors périmètre applicatif : leur bascule éventuelle vers le coffre relève du déploiement (l'opérateur), pas d'un champ de saisie de Kanbante. Le jeton MCP ObsiGate (KANBANTE_OBSIGATE_MCP_TOKEN) n'est pas réutilisé ici : le consommateur coffre a son propre jeton, conformément à l'issue.
Mots de passe utilisateurs, jetons API Kanbante Hashés (password_hash, token_hash) Hors périmètre (pas des secrets externes).

Conclusion : Kanbante ne stockait qu'un secret externe en clair persisté par l'app — le secret webhook — et c'est celui qui est migré. Le client générique ci-dessous reste disponible pour le premier usage réel suivant (jetons forge, voir §5).

2. Ce qui est implémenté

app/coffre_client.py (nouveau, dépendance : httpx uniquement)

  • est_reference() / parse_reference() : validation du format coffre:<slug>/<nom> (préfixe, un seul /, slug et nom non vides).
  • config_depuis_env() : KANBANTE_OBSIGATE_URL (base ObsiGate, ex. https://og.dracodev.net), KANBANTE_COFFRE_CONSOMMATEUR_TOKEN (jeton du consommateur « kanbante » enregistré dans ObsiGate), KANBANTE_COFFRE_TIMEOUT (défaut 10 s). Absence de l'un des deux premiers → CoffreNonConfigure, échec propre.
  • distribuer() / distribuer_async() : POST /api/coffres/distribuer avec JSON {reference, contexte_type, contexte_id} et Authorization: Bearer <jeton consommateur> — contrat vérifié dans le routeur ObsiGate (backend/routers/coffres.py, main). La valeur retournée n'est jamais journalisée, jamais persistée, jamais mise en cache.
  • usage_distribue() / usage_distribue_async() : gestionnaires de contexte « un seul usage » — la valeur ne doit pas sortir du bloc.
  • Erreurs typées, sans repli, avec messages explicites orientant vers ObsiGate (voir §3). Aucune valeur distribuée n'apparaît dans un message, un log ou une réponse HTTP.

Webhook (app/main.py, _handle_forge_webhook)

  • Si webhook_secret stocké est une référence : distribution asynchrone à chaque webhook (contexte_type="webhook", contexte_id=<slug de la connexion>), HMAC vérifié avec la valeur en mémoire, puis oubli (fin de portée de la fonction).
  • Échec de distribution → aucun repli sur le secret d'environnement ni sur une ancienne valeur (il n'y en a pas de stockée) : CoffreAccesRefuse → HTTP 401 « Accès révoqué ou refusé dans ObsiGate : ré-accorder l'octroi… » ; autre CoffreErreur (ObsiGate indisponible, consommateur non configuré) → HTTP 503 explicite. Le webhook doit être renvoyé après correction côté ObsiGate.
  • Si la valeur stockée n'est pas une référence : comportement historique inchangé (secret en clair, puis repli environnement).

Formulaire admin (app/main.py + app/templates/admin_connections.html)

  • Le champ « Secret webhook » accepte désormais une référence coffre:<slug>/<nom> (création et modification). Une saisie commençant par coffre: mais mal formée est refusée (redirection d'erreur), pour ne jamais la stocker comme secret en clair historique par accident.
  • Le champ « Jeton d'accès » est inchangé : une référence saisie là serait aujourd'hui chiffrée Fernet puis rejetée par la forge — c'est volontaire tant que la distribution par appel (§5) n'existe pas. Le bandeau du formulaire le signale.

Aucun changement de schéma n'a été nécessaire : la référence est stockée dans la colonne webhook_secret existante, détectée par son préfixe coffre: (un secret historique ne peut pas porter ce préfixe sans avoir été saisi comme tel, et ce cas est désormais validé).

3. Correspondance des erreurs ObsiGate

Réponse ObsiGate Erreur Kanbante Effet
401 consommateur_invalide, 403 consommateur_revoque CoffreAccesRefuse Webhook 401, message « ré-accorder »
403 octroi_absent / octroi_revoque / octroi_expire, 410 secret_expire CoffreAccesRefuse Webhook 401, message « ré-accorder / renouveler »
400 reference_invalide / contexte_manquant CoffreReferenceInvalide / CoffreContexteManquant Webhook 503 (erreur de configuration)
503 kek_absente / crypto_indisponible, timeout, connexion refusée, 200 sans valeur CoffreIndisponible Webhook 503, renvoyer le webhook plus tard

Dans tous les cas : pas de valeur en cache, pas de continuation.

4. Tests

tests/test_coffre_client.py (faux endpoint httpx.MockTransport, valeurs fictives TEST-VALUE-XXXX uniquement, aucune instance réelle) :

  • validation de références (valides / invalides) ;
  • succès sync + async, avec vérification de la requête (chemin, Bearer, corps reference + contexte) ;
  • refus (octroi révoqué/expiré/absent, consommateur révoqué/invalide, secret expiré) → CoffreAccesRefuse avec message explicite ;
  • indisponibilité (503, erreur réseau, 200 sans valeur) → CoffreIndisponible ;
  • référence invalide / contexte manquant / configuration absente : aucun appel HTTP ;
  • webhook de bout en bout (TestClient) : octroi révoqué → 401 explicite sans repli ; distribution réussie → HMAC accepté et ligne en base toujours porteuse de la référence, jamais de la valeur.

5. Restes à faire (exigent validation par Bruno sur instance réelle)

  1. Validation réelle : enregistrer le consommateur « kanbante » dans ObsiGate, créer un octroi vers un secret webhook de test, configurer KANBANTE_OBSIGATE_URL + KANBANTE_COFFRE_CONSOMMATEUR_TOKEN sur l'instance Kanbante, saisir la référence dans /admin/connections, envoyer un vrai webhook Gitea, vérifier l'acceptation, puis révoquer l'octroi et vérifier le 401 explicite, puis couper ObsiGate et vérifier le 503.
  2. Jetons forge par référence : distribution à chaque appel dans GiteaClient/GitHubClient (ou client jetable par requête), sans passer par le cache ForgeRegistry ; seulement alors, accepter une référence dans le champ « Jeton d'accès » et documenter la migration des connexions existantes (chiffrées Fernet → références).
  3. Migration opportuniste des secrets webhook historiques : proposer en admin la conversion d'un secret en clair existant vers une référence (l'admin crée le secret + l'octroi dans ObsiGate, puis remplace la valeur par la référence) — aucun outil automatique ne copie une valeur réelle vers ObsiGate dans ce lot.
  4. CI complète : la suite n'a été exécutée que localement dans le sandbox (voir rapport) ; le run CI Gitea de la branche reste à observer avant toute PR.