128 lines
8.4 KiB
Markdown
128 lines
8.4 KiB
Markdown
# 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.
|