audit logging
This commit is contained in:
@@ -14,6 +14,7 @@ from ..config import (
|
||||
read_env_value,
|
||||
)
|
||||
from ..db.apps import get_apps, update_app_resources
|
||||
from ..db.audit import log_audit_event
|
||||
from ..shell import run_command
|
||||
from ..templates.layout import page, render_result
|
||||
|
||||
@@ -203,6 +204,20 @@ def create_app(
|
||||
deploy_result = run_command([DEPLOY_SCRIPT, app_id])
|
||||
|
||||
status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED"
|
||||
log_audit_event(
|
||||
user,
|
||||
action="create_app",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={
|
||||
"app_id": app_id,
|
||||
"app_name": app_name,
|
||||
"template": template,
|
||||
"status": status,
|
||||
"create_returncode": create_result.returncode,
|
||||
"deploy_returncode": deploy_result.returncode,
|
||||
},
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title=f"Vytvoření aplikace: {status}",
|
||||
@@ -223,6 +238,17 @@ def create_app(
|
||||
def delete_app(app_id: str = Form(...), user=Depends(require_user)):
|
||||
result = run_command([DELETE_APP_SCRIPT, app_id])
|
||||
status = "OK" if result.returncode == 0 else "FAILED"
|
||||
log_audit_event(
|
||||
user,
|
||||
action="delete_app",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={
|
||||
"app_id": app_id,
|
||||
"status": status,
|
||||
"returncode": result.returncode,
|
||||
},
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title=f"Smazání aplikace: {status}",
|
||||
@@ -253,6 +279,21 @@ def update_resources(
|
||||
if catalog_result.returncode == 0 and compose_result.returncode == 0 and deploy_result.returncode == 0
|
||||
else "FAILED"
|
||||
)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="update_resources",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={
|
||||
"app_id": app_id,
|
||||
"memory": memory,
|
||||
"cpus": cpus,
|
||||
"status": status,
|
||||
"catalog_returncode": catalog_result.returncode,
|
||||
"compose_returncode": compose_result.returncode,
|
||||
"deploy_returncode": deploy_result.returncode,
|
||||
},
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title=f"Úprava prostředků: {status}",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import html
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from app.auth import require_user
|
||||
from app.db.audit import get_audit_events
|
||||
from app.templates.layout import page
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/audit", response_class=HTMLResponse)
|
||||
def audit_page(request: Request, user=Depends(require_user)):
|
||||
rows = ""
|
||||
|
||||
for event in get_audit_events():
|
||||
metadata = event.get("metadata") or "{}"
|
||||
metadata_preview = metadata if len(metadata) <= 180 else f"{metadata[:177]}..."
|
||||
|
||||
rows += f"""
|
||||
<tr>
|
||||
<td>{html.escape(event.get("created_at", "") or "")}</td>
|
||||
<td>{html.escape(event.get("username", "") or "")}</td>
|
||||
<td><span class="pill pill-muted">{html.escape(event.get("action", "") or "")}</span></td>
|
||||
<td>{html.escape(event.get("target_type", "") or "")}</td>
|
||||
<td>{html.escape(str(event.get("target_id") or ""))}</td>
|
||||
<td>{html.escape(event.get("source", "") or "")}</td>
|
||||
<td><code>{html.escape(metadata_preview)}</code></td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="7">Zatím nejsou evidované žádné auditní události.</td></tr>'
|
||||
|
||||
return page(
|
||||
"Audit",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Audit</h2>
|
||||
<p class="muted">Přehled interních akcí provedených v portálu.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Události</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Čas</th>
|
||||
<th>Uživatel</th>
|
||||
<th>Akce</th>
|
||||
<th>Typ cíle</th>
|
||||
<th>Cíl</th>
|
||||
<th>Zdroj</th>
|
||||
<th>Metadata</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</table>
|
||||
</div>
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from app.auth import authenticate_user, current_user
|
||||
from app.db.audit import log_audit_event
|
||||
from app.templates.layout import page
|
||||
|
||||
router = APIRouter()
|
||||
@@ -26,12 +27,27 @@ def login(request: Request, username: str = Form(...), password: str = Form(...)
|
||||
|
||||
request.session.clear()
|
||||
request.session["user_id"] = user["id"]
|
||||
log_audit_event(
|
||||
user,
|
||||
action="login_success",
|
||||
target_type="user",
|
||||
target_id=user.get("id"),
|
||||
metadata={"username": user.get("username")},
|
||||
)
|
||||
|
||||
return RedirectResponse(url="/portal", status_code=303)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(request: Request):
|
||||
user = current_user(request)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="logout",
|
||||
target_type="user",
|
||||
target_id=user.get("id") if user else None,
|
||||
metadata={"username": user.get("username")} if user else {},
|
||||
)
|
||||
request.session.clear()
|
||||
return RedirectResponse(url="/portal/login", status_code=303)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from ..auth import require_user
|
||||
from ..backups import is_backup_path, list_backups
|
||||
from ..config import BACKUP_SCRIPT
|
||||
from ..db.audit import log_audit_event
|
||||
from ..shell import run_command
|
||||
from ..templates.layout import page, render_result
|
||||
|
||||
@@ -44,6 +45,10 @@ def backups_page(request: Request, user=Depends(require_user)):
|
||||
<input readonly value="{restore_cmd}">
|
||||
<button type="button" class="copy-button" data-copy-value="{restore_cmd}" onclick="copyText(this)">Kopírovat</button>
|
||||
</div>
|
||||
<form method="post" action="/portal/backups/restore-trigger">
|
||||
<input type="hidden" name="backup_path" value="{backup_path}">
|
||||
<button type="submit" class="btn-secondary">Evidovat obnovu</button>
|
||||
</form>
|
||||
<div class="muted">Obnova je nebezpečná a musí se spustit ručně přes SSH.</div>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
@@ -95,6 +100,15 @@ def backups_page(request: Request, user=Depends(require_user)):
|
||||
def create_backup(user=Depends(require_user)):
|
||||
result = run_command([BACKUP_SCRIPT])
|
||||
status = "OK" if result.returncode == 0 else "FAILED"
|
||||
log_audit_event(
|
||||
user,
|
||||
action="backup_create",
|
||||
target_type="backup",
|
||||
metadata={
|
||||
"status": status,
|
||||
"returncode": result.returncode,
|
||||
},
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title=f"Vytvoření zálohy: {status}",
|
||||
@@ -116,3 +130,23 @@ def delete_backup(backup_path: str = Form(...), user=Depends(require_user)):
|
||||
target.unlink()
|
||||
|
||||
return RedirectResponse(url="/portal/backups", status_code=303)
|
||||
|
||||
|
||||
@router.post("/backups/restore-trigger")
|
||||
def restore_trigger(backup_path: str = Form(...), user=Depends(require_user)):
|
||||
target = Path(backup_path)
|
||||
|
||||
if not is_backup_path(target):
|
||||
return HTMLResponse("Neplatná cesta k záloze", status_code=400)
|
||||
|
||||
log_audit_event(
|
||||
user,
|
||||
action="restore_trigger",
|
||||
target_type="backup",
|
||||
target_id=target.name,
|
||||
metadata={
|
||||
"backup_path": str(target),
|
||||
},
|
||||
)
|
||||
|
||||
return RedirectResponse(url="/portal/backups", status_code=303)
|
||||
|
||||
Reference in New Issue
Block a user