diff --git a/backend/routers/coffres.py b/backend/routers/coffres.py index 34d0ab4..7aeb8fc 100644 --- a/backend/routers/coffres.py +++ b/backend/routers/coffres.py @@ -12,6 +12,14 @@ Règles d'or, appliquées côté serveur à chaque requête : - Seul ``POST /api/coffres/distribuer`` rend une valeur, et uniquement à un consommateur authentifié par jeton (hash + jti, jamais la session utilisateur), avec un octroi actif et un contexte obligatoire. +- Seule exception, voulue : ``POST /api/coffres/{slug}/secrets/{nom}/reveler`` + rend la valeur au gestionnaire du coffre authentifié par sa session + (propriétaire, ou admin sur coffre d'instance — jamais un simple + lecteur par octroi utilisateur), pour l'affichage et la copie manuelle + dans le panneau détail. Chaque révélation est auditée + (``revelation_manuelle``) et met à jour la dernière utilisation ; la + valeur n'est jamais journalisée et le frontend l'efface dès qu'elle + est masquée ou que le détail change. - Autorisations (spec §6) : le propriétaire seul sur son coffre personnel ; l'admin gère les coffres d'instance, les consommateurs et la KEK, jamais le coffre personnel d'autrui ; l'ACL « * » des vaults @@ -622,6 +630,50 @@ async def api_rotate_secret( return _metadata_secret(coffre, secret) +@router.post("/api/coffres/{slug}/secrets/{nom}/reveler") +async def api_reveler_secret( + slug: str, nom: str, current_user=Depends(require_auth) +): + """Révélation manuelle pour le panneau détail (afficher / copier). + + Session utilisateur + gestion du coffre exigées : un lecteur par + octroi utilisateur voit les métadonnées mais ne révèle jamais la + valeur. Chaque appel est audité et touche la dernière utilisation. + """ + _exiger_actives() + coffre = _coffre_or_404(slug) + _exiger_gerer(current_user, coffre) + secret = _secret_or_404(coffre, nom) + try: + valeur = distribuer_valeur(coffre, secret) + except SecretExpireError: + _audit(_username(current_user), "revelation_refusee", + coffre=coffre["slug"], + secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}", + resultat="secret_expire") + raise HTTPException(status_code=410, detail="secret_expire") from None + except KEKAbsenteError: + _audit(_username(current_user), "revelation_refusee", + coffre=coffre["slug"], + secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}", + resultat="kek_absente") + raise HTTPException(status_code=503, detail="kek_absente") from None + except CryptoIndisponibleError: + _audit(_username(current_user), "revelation_refusee", + coffre=coffre["slug"], + secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}", + resultat="crypto_indisponible") + raise HTTPException(status_code=503, + detail="crypto_indisponible") from None + toucher_utilisation(secret["id"]) + _audit(_username(current_user), "revelation_manuelle", + coffre=coffre["slug"], + secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}", + resultat="ok") + return {"reference": f"coffre:{coffre['slug']}/{secret['nom']}", + "valeur": valeur, "version": secret["version_courante"]} + + @router.delete("/api/coffres/{slug}/secrets/{nom}") async def api_delete_secret( slug: str, nom: str, current_user=Depends(require_auth)