diff --git a/app/main.py b/app/main.py index 216e0bf..4764288 100644 --- a/app/main.py +++ b/app/main.py @@ -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:/ 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", "")