coffre(obsigate#22): webhook distribue le secret reference a chaque appel, sans repli; validation reference admin
This commit is contained in:
+57
-1
@@ -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", "")
|
||||
|
||||
Reference in New Issue
Block a user