From 548bab93e55f6e514557571d3c9da9b1c1d59c35 Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Do, 23 Jul 2026 15:07:49 +0200
Subject: [PATCH] Zugriffsbeschraenkung: voller Zugriff nur lokal am Server, Netzwerk nur Software-Anforderung + Zugriffsprotokoll

---
 app.py |   77 +++++++++++++++++++++++++++++++++++++-
 1 files changed, 75 insertions(+), 2 deletions(-)

diff --git a/app.py b/app.py
index a50b718..9dff962 100644
--- a/app.py
+++ b/app.py
@@ -22,15 +22,62 @@
 # jederzeit im Fuss der Seitenleiste sichtbar ist, ob wirklich die neueste
 # Version laeuft (nicht nur auf der Festplatte liegt, sondern auch vom
 # gestarteten Programm geladen wurde).
-APP_VERSION = "2026-07-23 · r16 (Installer-Dateien direkt im Paket, kein Netzwerk zur Laufzeit nötig)"
+APP_VERSION = "2026-07-23 · r17 (Zugriffsbeschränkung lokal/Netzwerk + Zugriffsprotokoll)"
 
 
 @app.context_processor
 def _version_injizieren():
-    return {"app_version": APP_VERSION}
+    return {"app_version": APP_VERSION, "ist_lokal": ist_lokaler_zugriff()}
 
 # Nach wie vielen Tagen ohne Pruefung ein Tool als "bitte pruefen" markiert wird
 TAGE_BIS_WARNUNG = 30
+
+# ---------------------------------------------------------------------------
+# Zugriffsbeschraenkung: voller Zugriff nur direkt am Server (z. B. per RDP,
+# im Browser "localhost" statt der Netzwerk-IP aufgerufen). Von jedem anderen
+# Rechner im Netzwerk aus ist NUR die eingeschraenkte Software-Anforderung
+# erreichbar - alles andere (Katalog verwalten, Protokolle einsehen usw.)
+# bleibt dem Server selbst vorbehalten.
+# ---------------------------------------------------------------------------
+
+OEFFENTLICHE_ENDPUNKTE = {"einrichten", "static"}
+
+
+def ist_lokaler_zugriff():
+    return request.remote_addr in ("127.0.0.1", "::1")
+
+
+@app.before_request
+def _zugriff_beschraenken():
+    if ist_lokaler_zugriff():
+        return  # voller Zugriff, direkt am Server
+    if request.endpoint in OEFFENTLICHE_ENDPUNKTE:
+        return  # eingeschraenkter Bereich bleibt aus dem Netzwerk erreichbar
+    return render_template("zugriff_eingeschraenkt.html"), 403
+
+
+def protokolliere_zugriff(aktion, angegebener_name=""):
+    """Schreibt einen Eintrag ins Zugriffsprotokoll - IP-Adresse, per
+    Reverse-DNS ermittelter Rechnername (falls aufloesbar) und der selbst
+    angegebene Name. Hinweis: ein echter Windows-Benutzername laesst sich
+    ueber reines HTTP ohne Domänen-/NTLM-Integration nicht zuverlaessig
+    ermitteln, siehe ARCHITEKTUR.md."""
+    import socket
+    ip = request.remote_addr
+    try:
+        rechnername = socket.gethostbyaddr(ip)[0]
+    except Exception:
+        rechnername = None
+    db = get_db()
+    db.execute(
+        """INSERT INTO zugriffslog (zeitpunkt, ip_adresse, rechnername, angegebener_name, aktion, lokal)
+           VALUES (?, ?, ?, ?, ?, ?)""",
+        (
+            datetime.now().isoformat(timespec="seconds"), ip, rechnername, angegebener_name,
+            aktion, 1 if ist_lokaler_zugriff() else 0,
+        ),
+    )
+    db.commit()
 
 
 # ---------------------------------------------------------------------------
@@ -109,6 +156,16 @@
             dateiname TEXT NOT NULL,
             hochgeladen_am TEXT NOT NULL,
             FOREIGN KEY (software_id) REFERENCES software (id) ON DELETE CASCADE
+        );
+
+        CREATE TABLE IF NOT EXISTS zugriffslog (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            zeitpunkt TEXT NOT NULL,
+            ip_adresse TEXT,
+            rechnername TEXT,
+            angegebener_name TEXT,
+            aktion TEXT,
+            lokal INTEGER NOT NULL
         );
         """
     )
@@ -323,9 +380,20 @@
     return "\r\n".join(zeilen) + "\r\n"
 
 
+@app.route("/zugriffslog")
+def zugriffslog():
+    db = get_db()
+    eintraege = db.execute(
+        "SELECT * FROM zugriffslog ORDER BY zeitpunkt DESC LIMIT 300"
+    ).fetchall()
+    return render_template("zugriffslog.html", eintraege=eintraege)
+
+
 @app.route("/einrichten", methods=["GET", "POST"])
 def einrichten():
     db = get_db()
+    if request.method == "GET":
+        protokolliere_zugriff("Seite aufgerufen")
     alle = db.execute(
         "SELECT id, name, kategorie, version, installer_pfad, install_parameter, auto_update FROM software "
         "ORDER BY kategorie COLLATE NOCASE, name COLLATE NOCASE"
@@ -383,6 +451,11 @@
         server_url = request.host_url.rstrip("/")
         ps1_inhalt = _erstelle_einrichtungs_skript(server_url, mitarbeiter, ausgewaehlt)
 
+        protokolliere_zugriff(
+            "Paket heruntergeladen: " + ", ".join(t["name"] for t in ausgewaehlt),
+            angegebener_name=mitarbeiter,
+        )
+
         speicher = io.BytesIO()
         with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf:
             zf.writestr("einrichten.ps1", ps1_inhalt)

--
Gitblit v1.9.3