coffre(obsigate#22): webhook distribue le secret reference a chaque appel, sans repli; validation reference admin
CI / test (push) Failing after 0s
CI / test (pull_request) Failing after 1s

This commit is contained in:
2026-10-27 23:52:37 -04:00
parent 4184ba0272
commit c46d6dc519
+57 -1
View File
@@ -52,6 +52,7 @@ from app import forecast as forecast_service
from app import rules as rules_service
from app import sprint as sprint_service
from app import connections as connections_service
from app import coffre_client
from app import board_columns as bcols
from app.briefing import (
format_briefing_text,
@@ -1149,6 +1150,12 @@ async def admin_create_connection(
return RedirectResponse(
"/admin/connections?err=Nom+et+URL+requis", status_code=303
)
if hook_secret.startswith("coffre:") and not coffre_client.est_reference(hook_secret):
return RedirectResponse(
"/admin/connections?err=R%C3%A9f%C3%A9rence+coffre+invalide"
"+%28format+coffre%3A%3Cslug%3E%2F%3Cnom%3E%29",
status_code=303,
)
slug = _unique_slug(_slugify(name), "connections")
conn_id = models.create_connection(
slug=slug,
@@ -1193,6 +1200,12 @@ async def admin_update_connection(
if token:
fields["token_encrypted"] = forge_crypto.encrypt_token(token)
if hook_secret:
if hook_secret.startswith("coffre:") and not coffre_client.est_reference(hook_secret):
return RedirectResponse(
"/admin/connections?err=R%C3%A9f%C3%A9rence+coffre+invalide"
"+%28format+coffre%3A%3Cslug%3E%2F%3Cnom%3E%29",
status_code=303,
)
fields["webhook_secret"] = hook_secret
if fields:
models.update_connection(conn_id, **fields)
@@ -4879,7 +4892,50 @@ async def _handle_forge_webhook(request: Request, conn_row):
settings = get_settings()
raw = await request.body()
kind = conn_row["kind"] if conn_row else "gitea"
secret = (conn_row or {}).get("webhook_secret") or settings.webhook_secret
# Secret webhook : une référence coffre:<slug>/<nom> stockée dans
# forge_connections.webhook_secret est distribuée à chaque webhook
# (Phase 4, issue ObsiGate #22), utilisée en mémoire pour ce seul
# HMAC, puis oubliée. Aucun repli : si la distribution échoue
# (octroi révoqué, ObsiGate indisponible ou consommateur non
# configuré), le webhook échoue proprement avec un message explicite
# orientant vers ObsiGate, sans retomber sur le secret d'environnement
# ni sur une ancienne valeur (il n'y en a pas de stockée).
# Les valeurs historiques en clair et le repli environnement restent
# inchangés (compatibilité) tant qu'aucune référence n'est saisie.
stocke = (conn_row or {}).get("webhook_secret") or ""
if coffre_client.est_reference(stocke):
try:
secret = await coffre_client.distribuer_async(
stocke,
contexte_type="webhook",
contexte_id=(conn_row or {}).get("slug") or "gitea",
)
except coffre_client.CoffreAccesRefuse as exc:
return JSONResponse(
{
"ok": False,
"error": "Accès révoqué ou refusé dans ObsiGate : "
"ré-accorder l'octroi du secret webhook pour le "
"consommateur Kanbante dans ObsiGate, puis renvoyer "
"le webhook.",
"detail": str(exc),
},
status_code=401,
)
except coffre_client.CoffreErreur as exc:
return JSONResponse(
{
"ok": False,
"error": "ObsiGate indisponible ou consommateur Kanbante "
"non configuré : impossible de vérifier ce webhook "
"sans distribuer son secret. Vérifier ObsiGate, puis "
"renvoyer le webhook.",
"detail": str(exc),
},
status_code=503,
)
else:
secret = stocke or settings.webhook_secret
if secret:
if kind == "github":
signature = request.headers.get("x-hub-signature-256", "")