diff --git a/web/blueprints/auth.py b/web/blueprints/auth.py index 6aa8164..0e7cf46 100644 --- a/web/blueprints/auth.py +++ b/web/blueprints/auth.py @@ -85,6 +85,10 @@ def admin_login(): @auth.route("/logout", methods=["POST"]) def logout(): + user_id = session.get("user_id") + username = session.get("username") + if user_id: + log_audit_event(user_id, username, "session.logout", ip_address=request.remote_addr) session.clear() return redirect(url_for("auth.login")) diff --git a/web/blueprints/group_admin.py b/web/blueprints/group_admin.py index 8e7ba42..5d2ca24 100644 --- a/web/blueprints/group_admin.py +++ b/web/blueprints/group_admin.py @@ -345,6 +345,12 @@ def database(): ) test_conn.close() db.set_group_db_creds(group_id, host, int(port), user, password, database_name) + db.log_audit_event( + session["user_id"], session["username"], "db.credentials_changed", + entity_type="group", entity_id=group_id, + details={"host": host, "port": port, "database": database_name}, + group_id=group_id, ip_address=request.remote_addr, + ) flash("Database connection saved and tested ✓", "success") return redirect(url_for("group_admin.database")) except Exception as e: @@ -357,6 +363,12 @@ def database(): @group_admin.route("/database/delete", methods=["POST"]) @group_admin_required def database_delete(): - db.delete_group_db_creds(session["group_id"]) + group_id = session["group_id"] + db.delete_group_db_creds(group_id) + db.log_audit_event( + session["user_id"], session["username"], "db.credentials_deleted", + entity_type="group", entity_id=group_id, + group_id=group_id, ip_address=request.remote_addr, + ) flash("Database connection removed.", "success") return redirect(url_for("group_admin.database")) diff --git a/web/blueprints/site_admin.py b/web/blueprints/site_admin.py index 6f7d759..25aed60 100644 --- a/web/blueprints/site_admin.py +++ b/web/blueprints/site_admin.py @@ -216,6 +216,11 @@ def group_delete(group_id): @site_admin.route("/groups//members") @admin_required def group_members(group_id): + db.log_audit_event( + session["user_id"], session["username"], "admin.view_group_members", + entity_type="group", entity_id=group_id, + ip_address=request.remote_addr, + ) group = db.get_group_by_id(group_id) members = db.get_group_members(group_id) pending_invites = db.list_active_group_invites(group_id) @@ -424,6 +429,10 @@ def group_invite_resend(group_id, invite_id): @site_admin.route("/users") @admin_required def users(): + db.log_audit_event( + session["user_id"], session["username"], "admin.view_users", + ip_address=request.remote_addr, + ) return render_template( "admin/users.html", users=db.list_all_users(), @@ -597,6 +606,12 @@ def user_edit(user_id): db.update_user(user_id, username, email, is_site_admin) if new_password: db.change_password(user_id, new_password) + db.log_audit_event( + session["user_id"], session["username"], "user.password_changed", + entity_type="user", entity_id=user_id, + details={"target": username}, + ip_address=request.remote_addr, + ) flash("Password changed.", "info") db.log_audit_event( session["user_id"], session["username"], "user.updated", @@ -715,4 +730,18 @@ def audit_log(): actor_filter=actor_f or "", all_groups=all_groups, actions=actions, + retention_days=Config.AUDIT_LOG_RETENTION_DAYS, ) + + +@site_admin.route("/audit/purge", methods=["POST"]) +@admin_required +def audit_purge(): + deleted = db.purge_old_audit_events(Config.AUDIT_LOG_RETENTION_DAYS) + db.log_audit_event( + session["user_id"], session["username"], "audit.purged", + details={"deleted_count": deleted, "retention_days": Config.AUDIT_LOG_RETENTION_DAYS}, + ip_address=request.remote_addr, + ) + flash(f"Purged {deleted} audit log entries older than {Config.AUDIT_LOG_RETENTION_DAYS} days.", "success") + return redirect(url_for("site_admin.audit_log")) diff --git a/web/config.py b/web/config.py index dafd17d..0cfb236 100644 --- a/web/config.py +++ b/web/config.py @@ -58,6 +58,12 @@ class Config: INVITE_MAX_ACTIVE_PER_GROUP = int(os.getenv("INVITE_MAX_ACTIVE_PER_GROUP") or "200") INVITE_RESEND_COOLDOWN_SECONDS = int(os.getenv("INVITE_RESEND_COOLDOWN_SECONDS") or "120") + # ── Audit-Log-Aufbewahrung ──────────────────────────────── + # Audit-Log-Einträge, die älter als dieser Wert (in Tagen) sind, werden automatisch gelöscht. + # IP-Adressen gelten als personenbezogene Daten (DSGVO Art. 4 Nr. 1); nach 90 Tagen sollten + # diese nicht mehr benötigt werden. Auf 0 setzen, um automatisches Löschen zu deaktivieren. + AUDIT_LOG_RETENTION_DAYS = int(os.getenv("AUDIT_LOG_RETENTION_DAYS") or "90") + DEFAULT_PERMISSIONS = { "view_dashboard": True, "view_players": True, diff --git a/web/panel_db.py b/web/panel_db.py index 29e360f..29dca34 100644 --- a/web/panel_db.py +++ b/web/panel_db.py @@ -237,6 +237,9 @@ def init_databases(): finally: creds.close() + # Auto-Bereinigung: Audit-Log-Einträge älter als Retention-Tage löschen + purge_old_audit_events(Config.AUDIT_LOG_RETENTION_DAYS) + # ───────────────────────────────────────────────────────────── # Nutzer @@ -812,3 +815,30 @@ def get_audit_log_distinct_actions() -> list[str]: rows = _panel_query("SELECT DISTINCT action FROM audit_log ORDER BY action") return [r["action"] for r in rows] if rows else [] + +def purge_old_audit_events(retention_days: int) -> int: + """Deletes audit log entries older than *retention_days* days. + Returns the number of deleted rows. Skips if retention_days <= 0.""" + import logging + _log = logging.getLogger(__name__) + if retention_days <= 0: + return 0 + try: + conn = get_panel_db() + try: + with conn.cursor() as cur: + cur.execute( + "DELETE FROM audit_log WHERE created_at < UTC_TIMESTAMP() - INTERVAL %s DAY", + (retention_days,), + ) + deleted = cur.rowcount + conn.commit() + finally: + conn.close() + if deleted: + _log.info("Purged %d audit log entries older than %d days", deleted, retention_days) + return deleted + except Exception as exc: + _log.warning("Failed to purge audit log: %s", exc) + return 0 + diff --git a/web/templates/admin/audit_log.html b/web/templates/admin/audit_log.html index 352a30c..1f7c68c 100644 --- a/web/templates/admin/audit_log.html +++ b/web/templates/admin/audit_log.html @@ -4,7 +4,22 @@ {% block content %}

Audit Log

- {{ total }} event{{ 's' if total != 1 }} +
+ {{ total }} event{{ 's' if total != 1 }} + {% if retention_days > 0 %} + + Retention: {{ retention_days }}d + + {% endif %} +
+ + +
+
@@ -75,25 +90,33 @@ {% set action_class = { - 'user.login': 'badge bg-success', - 'user.login_failed': 'badge bg-danger', - 'admin.login': 'badge bg-warning text-dark', - 'admin.login_failed': 'badge bg-danger', - 'invite.created': 'badge bg-primary', - 'invite.accepted': 'badge bg-success', - 'invite.revoked': 'badge bg-secondary', - 'invite.resent': 'badge bg-info text-dark', - 'member.added': 'badge bg-primary', - 'member.removed': 'badge bg-danger', - 'member.role_changed': 'badge bg-warning text-dark', - 'member.updated': 'badge bg-warning text-dark', - 'group.created': 'badge bg-success', - 'group.updated': 'badge bg-secondary', - 'group.deleted': 'badge bg-danger', - 'user.updated': 'badge bg-secondary', - 'user.deleted': 'badge bg-danger', - 'mail.settings_saved': 'badge bg-info text-dark', - 'mail.settings_deleted':'badge bg-danger', + 'user.login': 'badge bg-success', + 'user.login_failed': 'badge bg-danger', + 'user.password_changed': 'badge bg-warning text-dark', + 'session.logout': 'badge bg-secondary', + 'admin.login': 'badge bg-warning text-dark', + 'admin.login_failed': 'badge bg-danger', + 'admin.view_users': 'badge bg-dark border border-secondary', + 'admin.view_group': 'badge bg-dark border border-secondary', + 'admin.view_group_members': 'badge bg-dark border border-secondary', + 'invite.created': 'badge bg-primary', + 'invite.accepted': 'badge bg-success', + 'invite.revoked': 'badge bg-secondary', + 'invite.resent': 'badge bg-info text-dark', + 'member.added': 'badge bg-primary', + 'member.removed': 'badge bg-danger', + 'member.role_changed': 'badge bg-warning text-dark', + 'member.updated': 'badge bg-warning text-dark', + 'group.created': 'badge bg-success', + 'group.updated': 'badge bg-secondary', + 'group.deleted': 'badge bg-danger', + 'db.credentials_changed': 'badge bg-warning text-dark', + 'db.credentials_deleted': 'badge bg-danger', + 'user.updated': 'badge bg-secondary', + 'user.deleted': 'badge bg-danger', + 'mail.settings_saved': 'badge bg-info text-dark', + 'mail.settings_deleted': 'badge bg-danger', + 'audit.purged': 'badge bg-danger', } %} {{ row.action }}