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 | 173 ++++++++++++++++++++++++++++++++++++++++++++-------------
1 files changed, 134 insertions(+), 39 deletions(-)
diff --git a/app.py b/app.py
index dd6e9f4..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-22 · r15 (Firewall-Freigabe-Skript ergänzt)"
+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
);
"""
)
@@ -189,8 +246,7 @@
"@echo off\r\n"
"set ZIEL=C:\\ClientBibliothek\r\n"
'if not exist "%ZIEL%" mkdir "%ZIEL%"\r\n'
- 'copy /Y "%~dp0einrichten.ps1" "%ZIEL%\\einrichten.ps1" >nul\r\n'
- 'copy /Y "%~f0" "%ZIEL%\\Rechner-einrichten-starten.bat" >nul\r\n'
+ 'xcopy "%~dp0*" "%ZIEL%\\" /E /I /Y >nul\r\n'
"echo ============================================================\r\n"
"echo Rechner-Einrichtung wird gestartet...\r\n"
"echo Admin-Rechte werden bei Bedarf per Windows-Abfrage angefragt.\r\n"
@@ -203,10 +259,12 @@
def _erstelle_einrichtungs_skript(server_url, mitarbeiter, tools):
- """Baut den Inhalt der einrichten.ps1 zusammen: laedt jedes ausgewaehlte
- Tool ueber die bestehende Download-Route herunter, installiert es mit
- dem hinterlegten Silent-Parameter und meldet das Ergebnis am Ende als
- Protokoll an die Server-Bibliothek zurueck."""
+ """Baut den Inhalt der einrichten.ps1 zusammen: installiert jedes
+ ausgewaehlte Tool aus der MITGELIEFERTEN Datei (liegt im 'tools'-Unterordner
+ desselben Pakets, kein Netzwerkzugriff auf den Server noetig) und meldet
+ das Ergebnis am Ende BEST-EFFORT als Protokoll an die Server-Bibliothek
+ zurueck (falls Netzwerkzugriff vorhanden ist - schlaegt das fehl, bleibt
+ trotzdem das lokale Protokoll erhalten)."""
zeilen = [
'$ErrorActionPreference = "Stop"',
f"$server = '{_ps_escape(server_url)}'",
@@ -218,10 +276,9 @@
" exit",
"}",
"",
- "$tempOrdner = Join-Path $clientOrdner \"downloads\"",
+ "$toolsOrdner = Join-Path $clientOrdner \"tools\"",
"$paketeOrdner = Join-Path $clientOrdner \"pakete\"",
"$protokollDatei = Join-Path $clientOrdner \"protokoll.log\"",
- "New-Item -ItemType Directory -Force -Path $tempOrdner | Out-Null",
"New-Item -ItemType Directory -Force -Path $paketeOrdner | Out-Null",
"",
"try {",
@@ -246,41 +303,38 @@
"",
"function InstalliereTool($id, $name, $endung, $parameter) {",
' Write-Host "Installiere: $name ..." -ForegroundColor Cyan',
- ' $zielDatei = Join-Path $tempOrdner ("sb_" + $id + $endung)',
- " try {",
- ' Invoke-WebRequest -Uri "$server/software/$id/download" -OutFile $zielDatei -UseBasicParsing',
- " } catch {",
- ' Write-Host (" FEHLER beim Herunterladen von " + $name + ": " + $_) -ForegroundColor Red',
- ' Protokolliere ($name + ": FEHLER beim Herunterladen")',
+ ' $quelle = Join-Path $toolsOrdner ($id.ToString() + $endung)',
+ " if (-not (Test-Path $quelle)) {",
+ ' Write-Host (" FEHLER: Datei fehlt im Paket unter " + $quelle) -ForegroundColor Red',
+ ' Protokolliere ($name + ": FEHLER, Datei fehlt im Paket")',
" return $false",
" }",
" try {",
' if ($endung -eq ".msi") {',
- " Start-Process \"msiexec.exe\" -ArgumentList ('/i \"' + $zielDatei + '\" ' + $parameter) -Wait",
+ " Start-Process \"msiexec.exe\" -ArgumentList ('/i \"' + $quelle + '\" ' + $parameter) -Wait",
" } else {",
- " Start-Process $zielDatei -ArgumentList $parameter -Wait",
+ " Start-Process $quelle -ArgumentList $parameter -Wait",
" }",
" } catch {",
' Write-Host (" FEHLER bei der Installation von " + $name + ": " + $_) -ForegroundColor Red',
' Protokolliere ($name + ": FEHLER bei der Installation")',
" return $false",
" }",
- " Remove-Item -Path $zielDatei -Force -ErrorAction SilentlyContinue",
' Protokolliere ($name + ": installiert")',
' Write-Host " fertig." -ForegroundColor Green',
" return $true",
"}",
"",
"function BereitstellenPortablesTool($id, $name) {",
- ' Write-Host "Lade herunter (portables Tool): $name ..." -ForegroundColor Cyan',
- ' $zielZip = Join-Path $paketeOrdner ($name + ".zip")',
- " try {",
- ' Invoke-WebRequest -Uri "$server/software/$id/download" -OutFile $zielZip -UseBasicParsing',
- " } catch {",
- ' Write-Host (" FEHLER beim Herunterladen von " + $name + ": " + $_) -ForegroundColor Red',
- ' Protokolliere ($name + ": FEHLER beim Herunterladen")',
+ ' Write-Host "Portables Tool: $name ..." -ForegroundColor Cyan',
+ ' $quelle = Join-Path $toolsOrdner ($id.ToString() + ".zip")',
+ " if (-not (Test-Path $quelle)) {",
+ ' Write-Host (" FEHLER: ZIP fehlt im Paket unter " + $quelle) -ForegroundColor Red',
+ ' Protokolliere ($name + ": FEHLER, ZIP fehlt im Paket")',
" return $false",
" }",
+ ' $zielZip = Join-Path $paketeOrdner ($name + ".zip")',
+ " Copy-Item -Path $quelle -Destination $zielZip -Force",
' Protokolliere ($name + ": als ZIP bereitgestellt unter " + $zielZip)',
' Write-Host (" fertig, liegt als ZIP unter " + $zielZip) -ForegroundColor Green',
' Write-Host " (bewusst NICHT automatisch entpackt - bitte manuell an den Zielort entpacken)" -ForegroundColor DarkGray',
@@ -304,7 +358,7 @@
zeilen += [
"",
'Write-Host ""',
- 'Write-Host "Protokoll wird auf dem Server gespeichert..."',
+ 'Write-Host "Protokoll wird an den Server gemeldet (optional, braucht Netzwerkzugriff)..."',
'$koerper = "mitarbeiter=" + [uri]::EscapeDataString($mitarbeiter)',
'$koerper += "&rechnername=" + [uri]::EscapeDataString($env:COMPUTERNAME)',
'$koerper += "&installiert_von=" + [uri]::EscapeDataString("Automatisches Einrichtungs-Skript")',
@@ -312,9 +366,9 @@
"foreach ($id in $erfolgreicheIds) { $koerper += \"&software_ids=$id\" }",
"try {",
' Invoke-RestMethod -Uri "$server/zuweisungen/neu" -Method POST -Body $koerper -ContentType "application/x-www-form-urlencoded" | Out-Null',
- ' Write-Host "Protokoll gespeichert." -ForegroundColor Green',
+ ' Write-Host "Zentrales Protokoll auf dem Server aktualisiert." -ForegroundColor Green',
"} catch {",
- ' Write-Host ("Konnte Protokoll nicht speichern: " + $_) -ForegroundColor Yellow',
+ ' Write-Host "Kein Netzwerkzugriff auf den Server - zentrales Protokoll nicht aktualisiert (lokales Protokoll unter protokoll.log ist trotzdem vollstaendig)." -ForegroundColor Yellow',
"}",
"",
'Write-Host ""',
@@ -326,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"
@@ -353,6 +418,7 @@
"typ": "installer",
"endung": os.path.splitext(gefunden)[1].lower(),
"parameter": sw["install_parameter"] or "",
+ "installer_pfad": pfad,
})
continue
@@ -367,6 +433,7 @@
"typ": "ordner",
"endung": "",
"parameter": "",
+ "installer_pfad": pfad,
})
continue
@@ -384,10 +451,29 @@
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)
zf.writestr("Rechner-einrichten-starten.bat", _BAT_LAUNCHER)
+
+ # Die echten Installer-Dateien/ZIPs direkt mit ins Paket packen,
+ # damit auf dem Zielrechner beim Ausfuehren KEIN Netzwerkzugriff
+ # auf den Server mehr noetig ist (nur die abschliessende
+ # Protokoll-Rueckmeldung ist optional und best-effort).
+ for t in ausgewaehlt:
+ if t["typ"] == "installer":
+ quelldatei = updater.verwaltete_datei_pfad(t["installer_pfad"], t["name"])
+ if quelldatei and os.path.isfile(quelldatei):
+ zf.write(quelldatei, f"tools/{t['id']}{t['endung']}")
+ else:
+ ordner = updater.effektiver_ablageordner(t["installer_pfad"], t["name"])
+ zf.writestr(f"tools/{t['id']}.zip", _zippe_ordner_in_bytes(ordner))
+
speicher.seek(0)
return send_file(
speicher, as_attachment=True,
@@ -397,6 +483,21 @@
return render_template(
"einrichten.html", installierbar=installierbar, nicht_installierbar=nicht_installierbar
)
+
+
+def _zippe_ordner_in_bytes(ordner):
+ """Packt einen Ordner (ohne '_sicherungen'-Unterordner) in ein ZIP im
+ Arbeitsspeicher und gibt die rohen Bytes zurueck. Wird sowohl fuer den
+ Download-Button als auch fuer das Einrichtungs-Paket verwendet."""
+ speicher = io.BytesIO()
+ with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf:
+ for wurzel, unterordner, dateien in os.walk(ordner):
+ unterordner[:] = [o for o in unterordner if o != "_sicherungen"]
+ for datei in dateien:
+ voller_pfad = os.path.join(wurzel, datei)
+ rel_pfad = os.path.relpath(voller_pfad, ordner)
+ zf.write(voller_pfad, rel_pfad)
+ return speicher.getvalue()
def _ordner_fuer_installer_sicherstellen(installer_pfad, name):
@@ -913,17 +1014,11 @@
flash(f"Ordner nicht gefunden unter: {ordner}", "fehler")
return redirect(url_for("software_detail", software_id=software_id))
- speicher = io.BytesIO()
- with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf:
- for wurzel, unterordner, dateien in os.walk(ordner):
- unterordner[:] = [o for o in unterordner if o != "_sicherungen"]
- for datei in dateien:
- voller_pfad = os.path.join(wurzel, datei)
- rel_pfad = os.path.relpath(voller_pfad, ordner)
- zf.write(voller_pfad, rel_pfad)
- speicher.seek(0)
+ zip_bytes = _zippe_ordner_in_bytes(ordner)
zip_name = os.path.basename(ordner.rstrip("\\/")) + ".zip"
- return send_file(speicher, as_attachment=True, download_name=zip_name, mimetype="application/zip")
+ return send_file(
+ io.BytesIO(zip_bytes), as_attachment=True, download_name=zip_name, mimetype="application/zip"
+ )
if __name__ == "__main__":
--
Gitblit v1.9.3