secrets: afficher/masquer + copier la valeur, revelation gestionnaire auditee (og/coffres.py)

This commit is contained in:
2026-10-30 18:34:07 -04:00
parent ead3393211
commit 1e59781d77
+52
View File
@@ -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)