refactor: consolidate checklist management into registry
This commit is contained in:
@@ -476,38 +476,6 @@
|
||||
{% endfor %}{% endfor %}{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
+1
-73
@@ -26,7 +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
|
||||
from config_upload import normalize_json, validate_xlsx
|
||||
from registry_service import (
|
||||
RegistryError, RegistryNotFoundError, RegistryService, RegistryValidationError
|
||||
)
|
||||
@@ -3072,78 +3072,6 @@ 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))
|
||||
|
||||
# Compatibility endpoint: translate the fixed OS form into a new immutable
|
||||
# profile version. ``install_config_files`` remains available for old
|
||||
# deployments, but registry artifacts are now the authoritative source.
|
||||
resolved = resolve_audit_profile(os_type=os_type)
|
||||
with open(resolved['checklist_path'], 'rb') as stream:
|
||||
active_checklist = stream.read()
|
||||
with open(resolved['rules_path'], 'rb') as stream:
|
||||
active_rules = stream.read()
|
||||
checklist_content = next((content for name, content in pending_files if name == os_info['checklist']),
|
||||
active_checklist)
|
||||
normalized_config = next((content for name, content in pending_files if name == os_info['config']),
|
||||
active_rules)
|
||||
version_id = datetime.now(timezone.utc).strftime("upload-%Y%m%dT%H%M%S%fZ")
|
||||
registry_service.create_version(resolved['profile']['id'], version_id, checklist_content,
|
||||
normalized_config, activate=True,
|
||||
created_by=user.get('username', 'admin'))
|
||||
|
||||
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,
|
||||
|
||||
@@ -6,27 +6,21 @@ TEMPLATE = (ROOT / "app" / "templates" / "admin_tools.html").read_text(encoding=
|
||||
COMPOSE = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_admin_checklist_upload_route_exists_and_is_admin_only():
|
||||
assert '@app.post("/admin/checklists/upload", name="admin_checklists_upload")' in APP
|
||||
assert "async def admin_upload_checklist_config(" in APP
|
||||
function = APP.split("async def admin_upload_checklist_config(", 1)[1].split("@app.", 1)[0]
|
||||
assert "require_admin_session(request)" in function
|
||||
def test_legacy_checklist_upload_ui_and_route_are_removed():
|
||||
assert "admin_checklists_upload" not in APP
|
||||
assert "admin_checklists_upload" not in TEMPLATE
|
||||
assert "Cập nhật Checklist & Rule Config" not in TEMPLATE
|
||||
|
||||
|
||||
def test_upload_uses_server_side_os_mapping_and_validates_json():
|
||||
function = APP.split("async def admin_upload_checklist_config(", 1)[1].split("@app.", 1)[0]
|
||||
assert "OS_CONFIG.get(os_type)" in function
|
||||
assert "normalize_json" in function
|
||||
assert "validate_xlsx" in function
|
||||
assert "install_config_files" in function
|
||||
|
||||
|
||||
def test_admin_ui_contains_checklist_configuration_form():
|
||||
assert "admin_checklists_upload" in TEMPLATE
|
||||
def test_registry_is_the_single_checklist_rule_management_flow():
|
||||
assert "registry_upload_version_v2" in APP
|
||||
assert "registry_upload_version_v2" in TEMPLATE
|
||||
assert 'name="checklist_file"' in TEMPLATE
|
||||
assert 'name="config_file"' in TEMPLATE
|
||||
assert "registry_activate_v2" in TEMPLATE
|
||||
assert "registry_rollback_v2" in TEMPLATE
|
||||
|
||||
|
||||
def test_config_mount_is_writable_for_persistent_uploads():
|
||||
assert "./config:/app/config:ro" not in COMPOSE
|
||||
assert "./config:/app/config" in COMPOSE
|
||||
def test_registry_mount_is_writable_and_persistent():
|
||||
assert "./registry:/app/registry" in COMPOSE
|
||||
assert "AUDIT_REGISTRY_ROOT=/app/registry" in COMPOSE
|
||||
|
||||
Reference in New Issue
Block a user