Phase 1 securite: permission checks sur tous les routers

- auth: verification is_active au login (compte desactive = bloque)
- settings: enforcement backend can_edit(settings) + role/section
- servers: can_view/can_edit(servers) sur toutes les routes
- planning: can_view/can_edit(planning) sur toutes les routes
- specifics: can_view/can_edit(specifics) sur toutes les routes
- contacts: rattache au module servers (can_view/can_edit)
- campaigns: can_view/can_edit(campaigns) sur toutes les routes manquantes
- audit/audit_full: can_view/can_edit(audit) sur toutes les routes
- qualys: can_view/can_edit(qualys) sur toutes les routes
- safe_patching: perm checks + authentification sur SSE stream
- quickwin: can_view/can_edit(campaigns|quickwin) sur toutes les routes

97 points d'injection securises, 0 route sans controle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Khalid MOUTAOUAKIL 2026-04-08 16:46:05 +02:00
parent 5cc10c5b6c
commit 13290c1ebb
12 changed files with 266 additions and 12 deletions

View File

@ -18,6 +18,9 @@ async def audit_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(perms, "audit"):
return RedirectResponse(url="/dashboard")
where = ["1=1"]
params = {}
@ -223,6 +226,9 @@ async def audit_realtime_save(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_edit(perms, "audit"):
return RedirectResponse(url="/audit")
results = getattr(request.app.state, "last_audit_results", None)
if not results:
@ -238,6 +244,9 @@ async def audit_export_csv(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(perms, "audit"):
return RedirectResponse(url="/audit")
where = ["1=1"]
params = {}

View File

@ -4,7 +4,7 @@ from fastapi import APIRouter, Request, Depends, UploadFile, File
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import text
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, base_context
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit, base_context
from ..services.server_audit_full_service import (
import_json_report, get_latest_audits, get_audit_detail,
get_flow_map, get_flow_map_for_server, get_app_map,
@ -208,6 +208,9 @@ async def audit_full_import(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_edit(perms, "audit"):
return RedirectResponse(url="/audit-full")
try:
content = await file.read()
@ -229,6 +232,9 @@ async def audit_full_patching(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(perms, "audit"):
return RedirectResponse(url="/dashboard")
year = int(request.query_params.get("year", "2026"))
search = request.query_params.get("q", "").strip()
@ -413,6 +419,9 @@ async def patching_export_csv(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(perms, "audit"):
return RedirectResponse(url="/audit-full")
import io, csv
year = int(request.query_params.get("year", "2026"))
@ -484,6 +493,9 @@ async def audit_full_export_csv(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(perms, "audit"):
return RedirectResponse(url="/audit-full")
import io, csv
filtre = request.query_params.get("filter", "")
@ -558,6 +570,9 @@ async def audit_full_flow_map(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(perms, "audit"):
return RedirectResponse(url="/audit-full")
domain_filter = request.query_params.get("domain", "")
server_filter = request.query_params.get("server", "").strip()
@ -648,6 +663,9 @@ async def audit_full_detail(request: Request, audit_id: int, 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(perms, "audit"):
return RedirectResponse(url="/audit-full")
audit = get_audit_detail(db, audit_id)
if not audit:

View File

@ -18,7 +18,7 @@ async def login_page(request: Request):
@router.post("/login")
async def login(request: Request, username: str = Form(...), password: str = Form(...), db=Depends(get_db)):
row = db.execute(text("SELECT id, username, password_hash, role FROM users WHERE LOWER(username) = LOWER(:u)"),
row = db.execute(text("SELECT id, username, password_hash, role, is_active FROM users WHERE LOWER(username) = LOWER(:u)"),
{"u": username}).fetchone()
if not row:
log_login_failed(db, request, username)
@ -26,6 +26,12 @@ async def login(request: Request, username: str = Form(...), password: str = For
return templates.TemplateResponse("login.html", {
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Utilisateur inconnu"
})
if not row.is_active:
log_login_failed(db, request, username)
db.commit()
return templates.TemplateResponse("login.html", {
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Compte desactive"
})
try:
ok = verify_password(password, row.password_hash)
except Exception:

View File

@ -85,7 +85,10 @@ async def campaign_preview(request: Request, db=Depends(get_db),
year: int = Query(...), week: int = Query(...)):
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
return HTMLResponse("<p>Non autorise</p>", status_code=401)
perms = get_user_perms(db, user)
if not can_view(perms, "campaigns"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
servers, planning = get_servers_for_planning(db, year, week)
scope = ", ".join(set(f"{p.domain_name} ({p.env_scope})" for p in planning if p.domain_code))
return templates.TemplateResponse("partials/campaign_preview.html", {
@ -99,6 +102,9 @@ async def campaign_create(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_edit(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
form = await request.form()
year = int(form.get("year", datetime.now().year))
week = int(form.get("week_number", 0))
@ -128,6 +134,9 @@ async def campaign_detail(request: Request, campaign_id: int, 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(perms, "campaigns"):
return RedirectResponse(url="/dashboard")
campaign = get_campaign(db, campaign_id)
if not campaign:
return RedirectResponse(url="/campaigns")
@ -212,6 +221,9 @@ async def session_prereq(request: Request, session_id: int, 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(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
validate_prereq(db, session_id, prereq_ssh, prereq_satellite,
rollback_method or None, rollback_justif, user.get("sub"))
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
@ -224,6 +236,9 @@ async def campaign_check_prereqs(request: Request, campaign_id: int, db=Depends(
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_edit(perms, "campaigns"):
return RedirectResponse(url=f"/campaigns/{campaign_id}")
checked, auto_excluded = check_prereqs_campaign(db, campaign_id)
log_prereq_check(db, request, user, campaign_id, checked, auto_excluded)
db.commit()
@ -235,6 +250,9 @@ async def session_check_prereq(request: Request, session_id: int, db=Depends(get
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_view(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
check_single_prereq(db, session_id)
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
@ -247,6 +265,9 @@ async def session_exclude(request: Request, session_id: int, 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_edit(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
exclude_session(db, session_id, reason, detail, user.get("sub"))
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
@ -258,6 +279,9 @@ async def session_restore(request: Request, session_id: int, 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_edit(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
restore_session(db, session_id)
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
@ -272,6 +296,9 @@ async def session_take(request: Request, session_id: int, 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(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
row = db.execute(text("SELECT campaign_id, intervenant_id, forced_assignment FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
if row.intervenant_id:
@ -292,6 +319,9 @@ async def session_release(request: Request, session_id: int, 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(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
if is_forced(db, session_id):
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
@ -309,6 +339,9 @@ async def session_assign(request: Request, session_id: int, 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_edit(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
oid = int(operator_id) if operator_id else None
is_forced_flag = forced == "on"
if oid:
@ -329,6 +362,9 @@ async def set_op_limit(request: Request, campaign_id: int, 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_edit(perms, "campaigns"):
return RedirectResponse(url=f"/campaigns/{campaign_id}")
set_operator_limit(db, campaign_id, operator_id, max_servers, note or None)
return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=limit_set", status_code=303)
@ -375,6 +411,9 @@ async def assignment_add(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_edit(perms, "campaigns"):
return RedirectResponse(url="/assignments")
try:
db.execute(text("""
INSERT INTO default_assignments (rule_type, rule_value, user_id, priority, note)
@ -393,6 +432,9 @@ async def assignment_delete(request: Request, rule_id: int, 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_edit(perms, "campaigns"):
return RedirectResponse(url="/assignments")
db.execute(text("DELETE FROM default_assignments WHERE id = :id"), {"id": rule_id})
db.commit()
return RedirectResponse(url="/assignments?msg=deleted", status_code=303)
@ -406,6 +448,9 @@ async def bulk_take(request: Request, campaign_id: int, 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(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
ids = [int(x) for x in session_ids.split(",") if x.strip().isdigit()]
limit = get_operator_limit(db, campaign_id, user.get("uid"))
current = get_operator_count(db, campaign_id, user.get("uid"))
@ -461,6 +506,9 @@ async def session_schedule(request: Request, session_id: int, 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_edit(perms, "campaigns"):
return RedirectResponse(url="/campaigns")
update_session_schedule(db, session_id, date_prevue or None, heure_prevue or None)
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()

View File

@ -43,6 +43,9 @@ async def contacts_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(perms, "servers"):
return RedirectResponse(url="/dashboard")
where = ["1=1"]
params = {}
@ -170,6 +173,9 @@ async def contact_add(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_edit(perms, "servers"):
return RedirectResponse(url="/contacts")
try:
db.execute(text("""
INSERT INTO contacts (name, email, role, is_active)
@ -188,6 +194,9 @@ async def contact_edit(request: Request, contact_id: int, 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_edit(perms, "servers"):
return RedirectResponse(url="/contacts")
updates = []; params = {"id": contact_id}
if name: updates.append("name = :n"); params["n"] = name
if email: updates.append("email = :e"); params["e"] = email.lower()
@ -203,6 +212,9 @@ async def contact_toggle(request: Request, contact_id: int, 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_edit(perms, "servers"):
return RedirectResponse(url="/contacts")
db.execute(text("UPDATE contacts SET is_active = NOT is_active WHERE id = :id"), {"id": contact_id})
db.commit()
return RedirectResponse(url="/contacts?msg=toggled", status_code=303)
@ -215,6 +227,9 @@ async def scope_add(request: Request, contact_id: int, 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_edit(perms, "servers"):
return RedirectResponse(url="/contacts")
try:
db.execute(text("""
INSERT INTO contact_scopes (contact_id, scope_type, scope_value, env_scope)
@ -231,6 +246,9 @@ async def scope_delete(request: Request, scope_id: int, 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_edit(perms, "servers"):
return RedirectResponse(url="/contacts")
db.execute(text("DELETE FROM contact_scopes WHERE id = :id"), {"id": scope_id})
db.commit()
return RedirectResponse(url="/contacts?msg=scope_deleted", status_code=303)
@ -241,6 +259,9 @@ async def contact_delete(request: Request, contact_id: int, 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_edit(perms, "servers"):
return RedirectResponse(url="/contacts")
db.execute(text("DELETE FROM contact_scopes WHERE contact_id = :cid"), {"cid": contact_id})
db.execute(text("DELETE FROM contacts WHERE id = :cid"), {"cid": contact_id})
db.commit()

View File

@ -84,6 +84,8 @@ async def planning_page(request: Request, db=Depends(get_db),
next_week = 1
perms = get_user_perms(db, user)
if not can_view(perms, "planning"):
return RedirectResponse(url="/dashboard")
return templates.TemplateResponse("planning.html", {
"request": request, "user": user, "perms": perms, "app_name": APP_NAME,
"year": year, "domains": domains, "grid": grid,
@ -104,6 +106,9 @@ async def planning_add(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_edit(perms, "planning"):
return RedirectResponse(url="/planning")
y = int(year)
wn = int(week_number) if week_number else 0
@ -146,6 +151,9 @@ async def planning_edit(request: Request, entry_id: int, 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_edit(perms, "planning"):
return RedirectResponse(url="/planning")
row = db.execute(text("SELECT year FROM patch_planning WHERE id = :id"), {"id": entry_id}).fetchone()
cyc = int(cycle) if cycle.strip() else None
db.execute(text("""
@ -163,6 +171,9 @@ async def planning_delete(request: Request, entry_id: int, 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_edit(perms, "planning"):
return RedirectResponse(url="/planning")
row = db.execute(text("SELECT year FROM patch_planning WHERE id = :id"), {"id": entry_id}).fetchone()
db.execute(text("DELETE FROM patch_planning WHERE id = :id"), {"id": entry_id})
db.commit()
@ -177,6 +188,9 @@ async def planning_duplicate(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_edit(perms, "planning"):
return RedirectResponse(url="/planning")
# Verifier que l'annee cible est vide
existing = db.execute(text("SELECT COUNT(*) FROM patch_planning WHERE year = :y"),

View File

@ -168,6 +168,9 @@ async def qualys_tags_resync(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_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
result = resync_all_tags(db)
msg = "resync_ok" if result["ok"] else "resync_ko"
return RedirectResponse(url=f"/qualys/tags?msg={msg}", status_code=303)
@ -179,6 +182,9 @@ async def qualys_tag_create(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_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
result = create_tag_api(db, tag_name.strip())
msg = "created" if result["ok"] else "create_error"
return RedirectResponse(url=f"/qualys/tags?msg={msg}", status_code=303)
@ -189,6 +195,9 @@ async def qualys_tag_delete(request: Request, tag_id: int, 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_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
result = delete_tag_api(db, tag_id)
msg = "deleted" if result["ok"] else "delete_error"
return RedirectResponse(url=f"/qualys/tags?msg={msg}", status_code=303)
@ -200,6 +209,9 @@ async def qualys_asset_tag_add(request: Request, asset_id: int, db=Depends(get_d
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
result = add_tag_to_asset_api(db, asset_id, int(tag_id))
color = "text-cyber-green" if result["ok"] else "text-cyber-red"
return HTMLResponse(f'<span class="text-xs {color}">{result["msg"]}</span>')
@ -211,6 +223,9 @@ async def qualys_asset_tag_remove(request: Request, asset_id: int, db=Depends(ge
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
result = remove_tag_from_asset_api(db, asset_id, int(tag_id))
color = "text-cyber-green" if result["ok"] else "text-cyber-red"
return HTMLResponse(f'<span class="text-xs {color}">{result["msg"]}</span>')
@ -228,6 +243,9 @@ async def qualys_bulk_add_tag(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_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
form = await request.form()
ids = [int(x) for x in form.get("asset_ids", "").split(",") if x.strip().isdigit()]
tid = int(form.get("tag_id", "0") or "0")
@ -244,6 +262,9 @@ async def qualys_bulk_remove_tag(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_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
form = await request.form()
ids = [int(x) for x in form.get("asset_ids", "").split(",") if x.strip().isdigit()]
tid = int(form.get("tag_id", "0") or "0")
@ -260,6 +281,9 @@ async def qualys_resync_assets(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_edit(perms, "qualys"):
return RedirectResponse(url="/qualys/search")
form = await request.form()
ids = [int(x) for x in form.get("asset_ids", "").split(",") if x.strip().isdigit()]
ok = 0
@ -303,6 +327,9 @@ async def qualys_tags_export(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(perms, "qualys"):
return RedirectResponse(url="/qualys/tags")
tags = db.execute(text("SELECT * FROM qualys_tags ORDER BY name")).fetchall()
output = io.StringIO()
writer = csv.writer(output, delimiter=";")
@ -484,6 +511,9 @@ async def export_no_agent_csv(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(perms, "qualys"):
return RedirectResponse(url="/qualys/agents")
import io, csv as _csv
rows = db.execute(text("""
SELECT s.hostname, s.os_family, s.etat, d.name as domain, e.name as env, z.name as zone
@ -512,6 +542,9 @@ async def export_inactive_csv(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(perms, "qualys"):
return RedirectResponse(url="/qualys/agents")
import io, csv as _csv
rows = db.execute(text("""
SELECT qa.hostname, qa.os, qa.agent_version, qa.last_checkin, s.etat
@ -536,7 +569,10 @@ async def qualys_vulns_detail(request: Request, ip: str, db=Depends(get_db)):
"""Retourne le detail des vulns severity 3,4,5 pour une IP (fragment HTMX)"""
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
return HTMLResponse("<p>Non autorise</p>", status_code=401)
perms = get_user_perms(db, user)
if not can_view(perms, "qualys"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
# Cache 10 min
from ..services import cache as _cache
@ -693,7 +729,10 @@ async def qualys_vulns_detail(request: Request, ip: str, db=Depends(get_db)):
async def qualys_asset_detail(request: Request, asset_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorisé</p>")
return HTMLResponse("<p>Non autorisé</p>", status_code=401)
perms = get_user_perms(db, user)
if not can_view(perms, "qualys"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
asset = db.execute(text("SELECT * FROM qualys_assets WHERE qualys_asset_id = :aid"),
{"aid": asset_id}).fetchone()

View File

@ -108,6 +108,9 @@ async def quickwin_config_save(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_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
return RedirectResponse(url="/quickwin/config")
if server_id:
upsert_server_config(db, server_id, general_excludes.strip(),
specific_excludes.strip(), notes.strip())
@ -120,6 +123,9 @@ async def quickwin_config_delete(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_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
return RedirectResponse(url="/quickwin/config")
if config_id:
delete_server_config(db, config_id)
return RedirectResponse(url="/quickwin/config?msg=deleted", status_code=303)
@ -133,6 +139,9 @@ async def quickwin_config_bulk_add(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_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
return RedirectResponse(url="/quickwin/config")
ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()]
for sid in ids:
upsert_server_config(db, sid, general_excludes.strip(), "", "")
@ -189,6 +198,9 @@ async def quickwin_detail(request: Request, run_id: int, 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(perms, "campaigns") and not can_view(perms, "quickwin"):
return RedirectResponse(url="/dashboard")
run = get_run(db, run_id)
if not run:
@ -265,6 +277,9 @@ async def quickwin_entry_update(request: Request, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return JSONResponse({"error": "unauthorized"}, 401)
perms = get_user_perms(db, user)
if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
return JSONResponse({"error": "forbidden"}, 403)
body = await request.json()
entry_id = body.get("id")
field = body.get("field")
@ -280,6 +295,9 @@ async def quickwin_inject_yum(request: Request, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return JSONResponse({"error": "unauthorized"}, 401)
perms = get_user_perms(db, user)
if not can_edit(perms, "campaigns"):
return JSONResponse({"error": "forbidden"}, 403)
body = await request.json()
if not isinstance(body, list):
return JSONResponse({"error": "expected list"}, 400)
@ -293,5 +311,8 @@ async def quickwin_prod_check(request: Request, run_id: int, db=Depends(get_db))
user = get_current_user(request)
if not user:
return JSONResponse({"error": "unauthorized"}, 401)
perms = get_user_perms(db, user)
if not can_view(perms, "campaigns") and not can_view(perms, "quickwin"):
return JSONResponse({"error": "forbidden"}, 403)
ok = can_start_prod(db, run_id)
return JSONResponse({"can_start_prod": ok})

View File

@ -89,6 +89,9 @@ async def safe_patching_detail(request: Request, campaign_id: int, db=Depends(ge
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_view(perms, "campaigns"):
return RedirectResponse(url="/dashboard")
campaign = get_campaign(db, campaign_id)
if not campaign:
@ -148,6 +151,9 @@ async def safe_patching_check_prereqs(request: Request, campaign_id: int, db=Dep
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_edit(perms, "campaigns"):
return RedirectResponse(url=f"/safe-patching/{campaign_id}")
from ..services.prereq_service import check_prereqs_campaign
checked, auto_excluded = check_prereqs_campaign(db, campaign_id)
return RedirectResponse(url=f"/safe-patching/{campaign_id}?step=prereqs&msg=prereqs_done", status_code=303)
@ -159,6 +165,9 @@ async def safe_patching_bulk_exclude(request: Request, campaign_id: int, db=Depe
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_edit(perms, "campaigns"):
return RedirectResponse(url=f"/safe-patching/{campaign_id}")
from ..services.campaign_service import exclude_session
ids = [int(x) for x in session_ids.split(",") if x.strip().isdigit()]
for sid in ids:
@ -173,6 +182,9 @@ async def safe_patching_execute(request: Request, campaign_id: int, db=Depends(g
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_edit(perms, "campaigns"):
return RedirectResponse(url=f"/safe-patching/{campaign_id}")
# Récupérer les sessions pending de la branche
if branch == "hprod":
@ -215,6 +227,9 @@ async def safe_patching_terminal(request: Request, campaign_id: int, db=Depends(
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_view(perms, "campaigns"):
return RedirectResponse(url="/safe-patching")
campaign = get_campaign(db, campaign_id)
ctx = base_context(request, db, user)
ctx.update({"app_name": APP_NAME, "c": campaign, "branch": branch})
@ -222,8 +237,11 @@ async def safe_patching_terminal(request: Request, campaign_id: int, db=Depends(
@router.get("/safe-patching/{campaign_id}/stream")
async def safe_patching_stream(request: Request, campaign_id: int):
async def safe_patching_stream(request: Request, campaign_id: int, db=Depends(get_db)):
"""SSE endpoint — stream les logs en temps réel"""
user = get_current_user(request)
if not user:
return StreamingResponse(iter([]), media_type="text/event-stream")
async def event_generator():
q = get_stream(campaign_id)
while True:

View File

@ -2,7 +2,7 @@
from fastapi import APIRouter, Request, Depends, Query, Form
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from ..dependencies import get_db, get_current_user
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit
from ..services.server_service import (
get_server_full, get_server_tags, get_server_ips,
list_servers, update_server, get_reference_data
@ -24,6 +24,9 @@ async def servers_list(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(perms, "servers"):
return RedirectResponse(url="/dashboard")
filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "os": os, "owner": owner, "search": search}
servers, total = list_servers(db, filters, page, sort=sort, sort_dir=sort_dir)
@ -47,6 +50,9 @@ async def servers_export_csv(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(perms, "servers"):
return RedirectResponse(url="/dashboard")
import io, csv
filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "os": os, "owner": owner, "search": search}
servers, total = list_servers(db, filters, page=1, per_page=99999, sort="hostname", sort_dir="asc")
@ -72,7 +78,10 @@ async def servers_export_csv(request: Request, db=Depends(get_db),
async def server_detail(request: Request, server_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
return HTMLResponse("<p>Non autorise</p>", status_code=401)
perms = get_user_perms(db, user)
if not can_view(perms, "servers"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
s = get_server_full(db, server_id)
if not s:
return HTMLResponse("<p>Serveur non trouve</p>")
@ -87,7 +96,10 @@ async def server_detail(request: Request, server_id: int, db=Depends(get_db)):
async def server_edit(request: Request, server_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
return HTMLResponse("<p>Non autorise</p>", status_code=401)
perms = get_user_perms(db, user)
if not can_edit(perms, "servers"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
s = get_server_full(db, server_id)
if not s:
return HTMLResponse("<p>Serveur non trouve</p>")
@ -111,7 +123,10 @@ async def server_update(request: Request, server_id: int, db=Depends(get_db),
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
return HTMLResponse("<p>Non autorise</p>", status_code=401)
perms = get_user_perms(db, user)
if not can_edit(perms, "servers"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
data = {
"domain_code": domain_code, "env_code": env_code, "zone": zone,
@ -139,6 +154,9 @@ async def servers_bulk(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_edit(perms, "servers"):
return RedirectResponse(url="/servers", status_code=303)
if not server_ids or not bulk_field or not bulk_value:
return RedirectResponse(url="/servers", status_code=303)
@ -189,7 +207,10 @@ async def servers_bulk(request: Request, db=Depends(get_db),
async def server_sync_qualys(request: Request, server_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
return HTMLResponse("<p>Non autorise</p>", status_code=401)
perms = get_user_perms(db, user)
if not can_edit(perms, "servers"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
result = sync_server_qualys(db, server_id)
s = get_server_full(db, server_id)
tags = get_server_tags(db, s.qid) if s else []

View File

@ -3,7 +3,7 @@ from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import text
from ..dependencies import get_db, get_current_user
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit
from ..services.secrets_service import get_secret, set_secret, list_secrets, init_secrets_from_config
from ..config import APP_NAME
@ -134,6 +134,9 @@ async def settings_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(perms, "settings"):
return RedirectResponse(url="/dashboard")
ctx = _build_context(db, user)
ctx["request"] = request
return templates.TemplateResponse("settings.html", ctx)
@ -146,6 +149,12 @@ async def settings_save(request: Request, section: str, db=Depends(get_db)):
return RedirectResponse(url="/login")
if section not in SECTIONS:
return HTMLResponse("<p>Section inconnue</p>", status_code=400)
perms = get_user_perms(db, user)
if not can_edit(perms, "settings"):
return RedirectResponse(url="/settings")
role = user.get("role", "viewer")
if section in SECTION_ACCESS and role not in SECTION_ACCESS[section]["editable"]:
return RedirectResponse(url="/settings")
form = await request.form()
for key, label, is_secret in SECTIONS[section]:
@ -174,6 +183,9 @@ async def vcenter_add(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_edit(perms, "settings"):
return RedirectResponse(url="/settings")
db.execute(text(
"INSERT INTO vcenters (name, endpoint, datacenter, description, responsable) VALUES (:n, :e, :dc, :desc, :resp)"
), {"n": vc_name, "e": vc_endpoint, "dc": vc_datacenter or None, "desc": vc_description or None, "resp": vc_responsable or None})
@ -188,6 +200,9 @@ async def vcenter_delete(request: Request, vc_id: int, 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_edit(perms, "settings"):
return RedirectResponse(url="/settings")
db.execute(text("UPDATE vcenters SET is_active = false WHERE id = :id"), {"id": vc_id})
db.commit()
ctx = _build_context(db, user, saved="vsphere")
@ -203,6 +218,9 @@ async def secret_update(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_edit(perms, "settings"):
return RedirectResponse(url="/settings")
if secret_value and secret_value != "********":
# Recuperer la description existante
existing = db.execute(text("SELECT description FROM app_secrets WHERE key = :k"),
@ -240,6 +258,9 @@ async def network_add(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_edit(perms, "settings"):
return RedirectResponse(url="/settings")
db.execute(text("INSERT INTO allowed_networks (cidr, description) VALUES (:c, :d)"),
{"c": cidr.strip(), "d": description or None})
db.commit()
@ -254,6 +275,9 @@ async def network_delete(request: Request, net_id: int, 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_edit(perms, "settings"):
return RedirectResponse(url="/settings")
db.execute(text("DELETE FROM allowed_networks WHERE id = :id"), {"id": net_id})
db.commit()
_regen_nginx_acl(db)
@ -267,6 +291,9 @@ async def network_toggle(request: Request, net_id: int, 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_edit(perms, "settings"):
return RedirectResponse(url="/settings")
db.execute(text("UPDATE allowed_networks SET is_active = NOT is_active WHERE id = :id"), {"id": net_id})
db.commit()
_regen_nginx_acl(db)

View File

@ -47,6 +47,9 @@ async def specifics_list(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(perms, "specifics"):
return RedirectResponse(url="/dashboard")
entries = _list_specifics(db, app_type, search)
# Types en base
types_in_db = db.execute(text(
@ -65,6 +68,9 @@ async def specific_edit(request: Request, spec_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
perms = get_user_perms(db, user)
if not can_edit(perms, "specifics"):
return HTMLResponse("<p>Acces interdit</p>", status_code=403)
row = db.execute(text("""
SELECT ss.*, s.hostname FROM server_specifics ss
JOIN servers s ON ss.server_id = s.id WHERE ss.id = :id
@ -81,6 +87,9 @@ async def specific_save(request: Request, spec_id: int, 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_edit(perms, "specifics"):
return RedirectResponse(url="/specifics")
form = await request.form()
def val(k): v = form.get(k, ""); return v.strip() if v else None
@ -147,6 +156,9 @@ async def specific_add(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_edit(perms, "specifics"):
return RedirectResponse(url="/specifics")
row = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname) = LOWER(:h)"),
{"h": hostname.strip()}).fetchone()
if not row: