patchcenter/app/routers/snapshots.py
Admin MPCZ e665fd94e7 fix(snapshots): exclure uniquement le compte technique 'admin' local (pas le role admin)
Avant: WHERE role <> 'admin' -> excluait TOUS les users avec role admin (notamment
les vrais utilisateurs ayant ce role pour leur travail patching).

Apres: WHERE LOWER(username) <> 'admin' -> exclut uniquement le compte technique
'admin' (seed local), peu importe son role. Tous les autres users actifs apparaissent.
2026-05-07 21:06:13 +02:00

131 lines
4.9 KiB
Python

"""Router /snapshots — listing + suppression des snapshots VM existants sur les vCenters."""
import logging
from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import text
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit, base_context
from ..config import APP_NAME
log = logging.getLogger("patchcenter.snapshots_router")
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
def _can_view_snaps(perms):
return can_view(perms, "campaigns") or can_view(perms, "planning") or can_view(perms, "settings")
def _can_delete_snaps(perms):
return can_edit(perms, "campaigns") or can_edit(perms, "planning") or can_edit(perms, "settings")
def _get_user_intervenant_name(db, user):
"""Retourne le nom utilisé comme préfixe dans les snapshots PatchCenter de cet utilisateur.
Priorité : settings 'patcher' > username (sub du JWT) > display_name."""
try:
from ..services.secrets_service import get_secret
patcher = (get_secret(db, "patcher") or "").strip()
if patcher:
return patcher
except Exception:
pass
# JWT stocke le username dans 'sub' (cf auth.py)
return (user.get("sub") or user.get("username") or user.get("display_name") or "").strip()
@router.get("/snapshots", response_class=HTMLResponse)
async def snapshots_page(request: Request, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not _can_view_snaps(perms):
return RedirectResponse(url="/dashboard")
intervenant = _get_user_intervenant_name(db, user)
vcenters = db.execute(text(
"SELECT id, name, endpoint FROM vcenters WHERE is_active = true ORDER BY name"
)).fetchall()
# Liste des intervenants disponibles : users actifs, hors compte technique 'admin' local
intervenants = db.execute(text("""
SELECT username, display_name FROM users
WHERE is_active = true
AND LOWER(username) <> 'admin'
ORDER BY username
""")).fetchall()
ctx = base_context(request, db, user)
ctx.update({
"app_name": APP_NAME,
"intervenant_default": intervenant,
"vcenters": vcenters,
"intervenants_list": intervenants,
"can_delete": _can_delete_snaps(perms),
})
return templates.TemplateResponse("snapshots.html", ctx)
@router.post("/snapshots/list")
async def snapshots_list(request: Request, db=Depends(get_db),
vcenter_id: str = Form("")):
"""Endpoint AJAX : liste les snapshots (tous vCenters ou un seul)."""
user = get_current_user(request)
if not user:
return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401)
perms = get_user_perms(db, user)
if not _can_view_snaps(perms):
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
from ..services.snapshot_mgmt_service import list_snapshots
vc_id = int(vcenter_id) if vcenter_id and vcenter_id.isdigit() else None
res = list_snapshots(db, vcenter_filter_id=vc_id)
return JSONResponse(res)
@router.post("/snapshots/delete")
async def snapshots_delete(request: Request, db=Depends(get_db)):
"""Endpoint AJAX : supprime une liste de snapshots.
Body JSON : [{vcenter_id, vm_moid, snap_id}, ...]"""
user = get_current_user(request)
if not user:
return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401)
perms = get_user_perms(db, user)
if not _can_delete_snaps(perms):
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
try:
body = await request.json()
except Exception:
return JSONResponse({"ok": False, "msg": "Body JSON invalide"}, status_code=400)
items = body.get("items") if isinstance(body, dict) else None
if not isinstance(items, list) or not items:
return JSONResponse({"ok": False, "msg": "Aucun snapshot ciblé"}, status_code=400)
from ..services.snapshot_mgmt_service import delete_snapshot
results = []
n_ok = 0
for it in items:
try:
r = delete_snapshot(
db,
vcenter_id=int(it["vcenter_id"]),
vm_moid=str(it["vm_moid"]),
snap_id=str(it["snap_id"]),
remove_children=bool(it.get("remove_children", False)),
)
except Exception as e:
r = {"ok": False, "msg": f"Param error: {e}"}
r["vm_name"] = it.get("vm_name", "?")
r["snap_name"] = it.get("snap_name", "?")
results.append(r)
if r.get("ok"):
n_ok += 1
return JSONResponse({
"ok": n_ok == len(items),
"summary": f"{n_ok}/{len(items)} snapshots supprimés",
"results": results,
})