Server-Bibliothek
2026-07-23 548bab93e55f6e514557571d3c9da9b1c1d59c35
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)