feat: allow admins to update audit checklist configuration

This commit is contained in:
Luftmensch
2026-09-04 09:17:11 +07:00
parent aeba055e43
commit 4f4c7df4ed
8 changed files with 309 additions and 2 deletions
+49
View File
@@ -0,0 +1,49 @@
import json
import os
import shutil
import zipfile
from io import BytesIO
from pathlib import Path
from typing import Iterable
def validate_xlsx(content: bytes) -> bytes:
"""Validate that content is an XLSX-compatible ZIP containing workbook metadata."""
try:
with zipfile.ZipFile(BytesIO(content)) as archive:
names = set(archive.namelist())
except (zipfile.BadZipFile, OSError) as exc:
raise ValueError("Checklist không phải file XLSX hợp lệ") from exc
if "[Content_Types].xml" not in names or "xl/workbook.xml" not in names:
raise ValueError("Checklist không phải file XLSX hợp lệ")
return content
def normalize_json(content: bytes) -> bytes:
"""Parse and normalize an object/array JSON configuration."""
try:
parsed = json.loads(content.decode("utf-8-sig"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ValueError(f"Rule config JSON không hợp lệ: {exc}") from exc
if not isinstance(parsed, (dict, list)):
raise ValueError("Rule config JSON phải là object hoặc array")
return json.dumps(parsed, ensure_ascii=False, indent=2).encode("utf-8") + b"\n"
def install_config_files(config_root: str | Path, files: Iterable[tuple[str, bytes]]) -> None:
"""Atomically replace allowlisted config files and keep one .bak copy."""
root = Path(config_root)
root.mkdir(parents=True, exist_ok=True)
for target_name, content in files:
if Path(target_name).name != target_name:
raise ValueError("Tên file đích không hợp lệ")
target = root / target_name
temporary = root / f"{target_name}.tmp"
backup = root / f"{target_name}.bak"
with temporary.open("wb") as output:
output.write(content)
output.flush()
os.fsync(output.fileno())
if target.exists():
shutil.copy2(target, backup)
os.replace(temporary, target)
+32
View File
@@ -428,6 +428,38 @@
</div>
{% if user_info and user_info.role == 'admin' %}
<!-- Upload Checklist and Rule Configuration -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📋 Cập nhật Checklist & Rule Config</h2>
</div>
<p style="color: var(--text-muted); margin-bottom: 18px;">
Chọn hệ điều hành và tải lên checklist XLSX, rule JSON hoặc cả hai. File cũ được sao lưu với đuôi <code>.bak</code>.
</p>
<form action="{{ url_for('admin_checklists_upload') }}" method="post" enctype="multipart/form-data" class="upload-form">
<div class="form-group">
<label>Hệ điều hành *</label>
<select name="os_type" required>
<option value="">-- Chọn OS --</option>
{% for os_key, os_info in os_config.items() %}
<option value="{{ os_key }}">{{ os_info.name }} — {{ os_info.checklist }} / {{ os_info.config }}</option>
{% endfor %}
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Checklist (.xlsx)</label>
<input type="file" name="checklist_file" accept=".xlsx">
</div>
<div class="form-group">
<label>Rule config (.json)</label>
<input type="file" name="config_file" accept=".json">
</div>
</div>
<button type="submit" class="btn btn-primary">⬆️ Cập nhật Checklist/Config</button>
</form>
</div>
<!-- Upload Documentation Form -->
<div class="card">
<div class="card-header">
+59
View File
@@ -26,6 +26,7 @@ import ipaddress
import re
import requests as http_requests
from authenticator.radius_login import check_radius_login, check_radius_login_extended
from config_upload import install_config_files, normalize_json, validate_xlsx
# Database imports
import models
@@ -2615,6 +2616,7 @@ async def admin_tools_page(request: Request, msg: str = None, msg_type: str = "s
"request": request,
"tools_list": get_available_tools(),
"os_metadata": OS_METADATA,
"os_config": OS_CONFIG,
"message": message,
"user": user,
"user_info": user,
@@ -2930,6 +2932,63 @@ async def admin_delete_tool(
params = urlencode({"msg": f"Lỗi khi xóa: {str(e)}", "msg_type": "error"})
return RedirectResponse(url=f"{request.url_for('admin_tools')}?{params}", status_code=303)
@app.post("/admin/checklists/upload", name="admin_checklists_upload")
async def admin_upload_checklist_config(
request: Request,
os_type: str = Form(...),
checklist_file: Optional[UploadFile] = File(None),
config_file: Optional[UploadFile] = File(None),
):
"""Upload/replace the checklist XLSX and rule JSON for an OS. Admin only."""
user = require_admin_session(request)
if not user:
return RedirectResponse(url=request.url_for('login'), status_code=303)
from urllib.parse import urlencode
os_info = OS_CONFIG.get(os_type)
if not os_info:
params = urlencode({"msg": "Hệ điều hành không hợp lệ", "msg_type": "error"})
return RedirectResponse(url=f"{request.url_for('admin_tools')}?{params}", status_code=303)
has_checklist = bool(checklist_file and checklist_file.filename)
has_config = bool(config_file and config_file.filename)
if not has_checklist and not has_config:
params = urlencode({"msg": "Cần chọn ít nhất một file checklist hoặc rule config", "msg_type": "error"})
return RedirectResponse(url=f"{request.url_for('admin_tools')}?{params}", status_code=303)
pending_files = []
try:
if has_checklist:
if not checklist_file.filename.lower().endswith('.xlsx'):
raise ValueError("Checklist phải là file .xlsx")
checklist_content = validate_xlsx(await checklist_file.read())
pending_files.append((os_info['checklist'], checklist_content))
if has_config:
if not config_file.filename.lower().endswith('.json'):
raise ValueError("Rule config phải là file .json")
normalized_config = normalize_json(await config_file.read())
pending_files.append((os_info['config'], normalized_config))
install_config_files(CONFIG_FOLDER, pending_files)
print(Fore.GREEN + f"[ADMIN] Checklist/config updated for {os_type} by {user.get('username')}" + Fore.RESET)
changed = ', '.join(name for name, _ in pending_files)
params = urlencode({"msg": f"Đã cập nhật: {changed}", "msg_type": "success"})
except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
for target_name, _ in pending_files:
temp_path = os.path.join(CONFIG_FOLDER, target_name) + '.tmp'
if os.path.exists(temp_path):
os.remove(temp_path)
params = urlencode({"msg": str(exc), "msg_type": "error"})
except Exception as exc:
print(Fore.RED + f"[ADMIN] Checklist/config upload error: {exc}" + Fore.RESET)
params = urlencode({"msg": f"Lỗi cập nhật checklist/config: {exc}", "msg_type": "error"})
return RedirectResponse(url=f"{request.url_for('admin_tools')}?{params}", status_code=303)
@app.post("/admin/docs/upload", name="admin_docs_upload")
async def admin_upload_doc(
request: Request,