Retention-Cleanup-Job (automatische Aufbewahrungsfrist-Bereinigung, Advisory-Lock)

This commit is contained in:
mscadm 2026-07-13 14:52:17 +00:00
parent 7c24951db7
commit 86d2cf4c4a
1 changed files with 45 additions and 3 deletions

48
app.py
View File

@ -11,6 +11,10 @@ Betrieb gemaess ISO 27001 / DSGVO / NIS 2 (Nutzer-Vorgabe, 2026-07-13):
DSGVO-Angaben (Datenschutzbeauftragter, Aufbewahrungsfristen) an.
Mandantenfaehig: mehrere Organisationen auf einer Instanz, Daten strikt
per tenant_id getrennt (kein Datenzugriff ueber Mandantengrenzen).
- Automatische Aufbewahrungsfrist-Bereinigung (Hintergrund-Thread, siehe
db.run_retention_cleanup) -- loescht abgeschlossene Tickets/Audit-Log-
Eintraege, die aelter als die hinterlegte Frist sind (DSGVO Speicher-
begrenzung).
AES bleibt ein buchbarer Service im Katalog, integriert per HTTP (kein Code-
Import) -- siehe _aes_live_status().
@ -19,11 +23,13 @@ ENV:
DATABASE_URL postgresql://user:pass@host:5432/dbname
ITSM_SECRET_KEY Flask-Session-Secret
AES_DASHBOARD_URL Basis-URL des AES-Dashboards fuer die Service-Integration
RETENTION_INTERVAL_SECONDS Intervall fuer die Retention-Bereinigung (Default 24h)
"""
import os
import html
import json
import time
import secrets
import threading
import datetime as _dt
import urllib.request
import urllib.error
@ -34,6 +40,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
import db
AES_DASHBOARD_URL = os.getenv("AES_DASHBOARD_URL", "").rstrip("/")
RETENTION_INTERVAL_SECONDS = int(os.getenv("RETENTION_INTERVAL_SECONDS", str(24 * 60 * 60)))
app = Flask(__name__)
app.secret_key = os.getenv("ITSM_SECRET_KEY") or secrets.token_hex(32)
@ -303,7 +310,7 @@ def setup_new():
<div class="formrow"><label>Aufbewahrung Tickets (Tage)</label><input name="retention_tickets_days" type="number" value="1095"></div>
<div class="formrow"><label>Aufbewahrung Audit-Log (Tage)</label><input name="retention_audit_days" type="number" value="1825"></div>
</div>
<div class="hint">Diese Werte legen fest, wie lange Tickets bzw. Audit-Eintraege vorgehalten werden (DSGVO Speicherbegrenzung). Die automatische Loeschung nach Fristablauf ist als naechster Ausbauschritt vorgesehen -- aktuell wird nur die Frist hinterlegt.</div>
<div class="hint">Diese Werte legen fest, wie lange Tickets bzw. Audit-Eintraege vorgehalten werden (DSGVO Speicherbegrenzung). Ein Hintergrund-Job bereinigt automatisch alle 24 Stunden.</div>
<button class="btn" type="submit" style="margin-top:8px">Organisation anlegen</button>
<div class="sz" style="margin-top:12px">Bereits registriert? <a href="/login">Anmelden</a></div>
</form></div></body></html>""" % (_CSS_TAG, err_html)
@ -654,6 +661,8 @@ def admin_route():
)
db.log_audit(tenant_id, session["user_id"], "tenant_settings_updated", "tenant", str(tenant_id), None, _client_ip())
notice = "<p class='sz' style='color:var(--ok)'>Gespeichert.</p>"
if request.args.get("cleanup"):
notice += "<p class='sz' style='color:var(--ok)'>Bereinigung durchgefuehrt -- Details im Audit-Log.</p>"
t = db.get_tenant(tenant_id)
body = """<h1 class="page-title">Einstellungen</h1>
<div class="panel"><h3 style="margin-top:0">%s</h3>%s
@ -673,13 +682,29 @@ def admin_route():
<div class="formrow"><label>Aufbewahrung Audit-Log (Tage)</label><input type="number" name="retention_audit_days" value="%s"></div>
</div>
<button class="btn" type="submit">Speichern</button>
</form></div>""" % (
</form></div>
<div class="panel">
<h3 style="margin-top:0">Aufbewahrungsfrist-Bereinigung</h3>
<p class="sz">Laeuft automatisch alle 24 Stunden im Hintergrund: geloeste/geschlossene Tickets und Audit-Log-Eintraege, die aelter als die oben hinterlegten Fristen sind, werden entfernt. Offene/laufende Tickets werden nie geloescht.</p>
<form method="post" action="/admin/retention/run">
<button class="btn ghost" type="submit">Jetzt manuell bereinigen</button>
</form>
</div>""" % (
html.escape(t["name"]), notice, t["sla_antwort_minuten"], t["sla_loesung_minuten"],
html.escape(t["dsb_name"] or ""), html.escape(t["dsb_email"] or ""),
t["retention_tickets_days"], t["retention_audit_days"])
return _shell("/admin", "Einstellungen", body)
@app.route("/admin/retention/run", methods=["POST"])
def admin_retention_run():
r = _require_admin()
if r:
return r
db.run_retention_cleanup()
return redirect(url_for("admin_route", cleanup="1"))
@app.route("/admin/audit")
def admin_audit_route():
r = _require_admin()
@ -746,5 +771,22 @@ def health_route():
db.init_db()
def _retention_loop():
"""Hintergrund-Thread: bereinigt abgelaufene Daten periodisch (siehe
db.run_retention_cleanup). Laeuft einmal kurz nach dem Start und danach im
festen Intervall. Der Postgres-Advisory-Lock in run_retention_cleanup()
schuetzt davor, dass mehrere Worker/Prozesse gleichzeitig loeschen."""
time.sleep(30)
while True:
try:
db.run_retention_cleanup()
except Exception as e:
print("[retention] Fehler bei der Bereinigung:", e, flush=True)
time.sleep(RETENTION_INTERVAL_SECONDS)
threading.Thread(target=_retention_loop, daemon=True).start()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8090)