From 13290c1ebbbb25ff0857918bd9cecab1b99af03b Mon Sep 17 00:00:00 2001 From: Khalid MOUTAOUAKIL Date: Wed, 8 Apr 2026 16:46:05 +0200 Subject: [PATCH] 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 --- app/routers/audit.py | 9 +++++++ app/routers/audit_full.py | 20 ++++++++++++++- app/routers/auth.py | 8 +++++- app/routers/campaigns.py | 50 +++++++++++++++++++++++++++++++++++- app/routers/contacts.py | 21 +++++++++++++++ app/routers/planning.py | 14 ++++++++++ app/routers/qualys.py | 43 +++++++++++++++++++++++++++++-- app/routers/quickwin.py | 21 +++++++++++++++ app/routers/safe_patching.py | 20 ++++++++++++++- app/routers/servers.py | 31 ++++++++++++++++++---- app/routers/settings.py | 29 ++++++++++++++++++++- app/routers/specifics.py | 12 +++++++++ 12 files changed, 266 insertions(+), 12 deletions(-) diff --git a/app/routers/audit.py b/app/routers/audit.py index b050d3d..ee25543 100644 --- a/app/routers/audit.py +++ b/app/routers/audit.py @@ -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 = {} diff --git a/app/routers/audit_full.py b/app/routers/audit_full.py index 25e1624..795821f 100644 --- a/app/routers/audit_full.py +++ b/app/routers/audit_full.py @@ -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: diff --git a/app/routers/auth.py b/app/routers/auth.py index 07f5ca0..c024d93 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -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: diff --git a/app/routers/campaigns.py b/app/routers/campaigns.py index 067f76e..56af088 100644 --- a/app/routers/campaigns.py +++ b/app/routers/campaigns.py @@ -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("

Non autorise

") + return HTMLResponse("

Non autorise

", status_code=401) + perms = get_user_perms(db, user) + if not can_view(perms, "campaigns"): + return HTMLResponse("

Acces interdit

", 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() diff --git a/app/routers/contacts.py b/app/routers/contacts.py index 6dff326..76acfa1 100644 --- a/app/routers/contacts.py +++ b/app/routers/contacts.py @@ -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() diff --git a/app/routers/planning.py b/app/routers/planning.py index fe61781..2face36 100644 --- a/app/routers/planning.py +++ b/app/routers/planning.py @@ -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"), diff --git a/app/routers/qualys.py b/app/routers/qualys.py index 63c9ef5..6a84461 100644 --- a/app/routers/qualys.py +++ b/app/routers/qualys.py @@ -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'{result["msg"]}') @@ -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'{result["msg"]}') @@ -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("

Non autorise

") + return HTMLResponse("

Non autorise

", status_code=401) + perms = get_user_perms(db, user) + if not can_view(perms, "qualys"): + return HTMLResponse("

Acces interdit

", 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("

Non autorisé

") + return HTMLResponse("

Non autorisé

", status_code=401) + perms = get_user_perms(db, user) + if not can_view(perms, "qualys"): + return HTMLResponse("

Acces interdit

", status_code=403) asset = db.execute(text("SELECT * FROM qualys_assets WHERE qualys_asset_id = :aid"), {"aid": asset_id}).fetchone() diff --git a/app/routers/quickwin.py b/app/routers/quickwin.py index 6b527d8..fa8ef75 100644 --- a/app/routers/quickwin.py +++ b/app/routers/quickwin.py @@ -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}) diff --git a/app/routers/safe_patching.py b/app/routers/safe_patching.py index 3ea8f3f..89f1a41 100644 --- a/app/routers/safe_patching.py +++ b/app/routers/safe_patching.py @@ -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: diff --git a/app/routers/servers.py b/app/routers/servers.py index e68e381..7b5d80b 100644 --- a/app/routers/servers.py +++ b/app/routers/servers.py @@ -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("

Non autorise

") + return HTMLResponse("

Non autorise

", status_code=401) + perms = get_user_perms(db, user) + if not can_view(perms, "servers"): + return HTMLResponse("

Acces interdit

", status_code=403) s = get_server_full(db, server_id) if not s: return HTMLResponse("

Serveur non trouve

") @@ -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("

Non autorise

") + return HTMLResponse("

Non autorise

", status_code=401) + perms = get_user_perms(db, user) + if not can_edit(perms, "servers"): + return HTMLResponse("

Acces interdit

", status_code=403) s = get_server_full(db, server_id) if not s: return HTMLResponse("

Serveur non trouve

") @@ -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("

Non autorise

") + return HTMLResponse("

Non autorise

", status_code=401) + perms = get_user_perms(db, user) + if not can_edit(perms, "servers"): + return HTMLResponse("

Acces interdit

", 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("

Non autorise

") + return HTMLResponse("

Non autorise

", status_code=401) + perms = get_user_perms(db, user) + if not can_edit(perms, "servers"): + return HTMLResponse("

Acces interdit

", 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 [] diff --git a/app/routers/settings.py b/app/routers/settings.py index 0abc6fa..481e8a8 100644 --- a/app/routers/settings.py +++ b/app/routers/settings.py @@ -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("

Section inconnue

", 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) diff --git a/app/routers/specifics.py b/app/routers/specifics.py index 03f9747..9541577 100644 --- a/app/routers/specifics.py +++ b/app/routers/specifics.py @@ -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("

Non autorise

") + perms = get_user_perms(db, user) + if not can_edit(perms, "specifics"): + return HTMLResponse("

Acces interdit

", 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: