| | |
| | | # 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() |
| | | |
| | | |
| | | # --------------------------------------------------------------------------- |
| | |
| | | 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 |
| | | ); |
| | | """ |
| | | ) |
| | |
| | | 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" |
| | |
| | | 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) |