6 Dateien geändert
2 Dateien hinzugefügt
| | |
| | | ## 6. "Rechner einrichten" – Verteilungs-Mechanismus |
| | | |
| | | Route `/einrichten` (app.py) erzeugt bei Bedarf ein individuelles |
| | | Installations-Paket (ZIP mit `einrichten.ps1` + `Rechner-einrichten-starten.bat`), |
| | | Installations-Paket (ZIP mit `einrichten.ps1` + `Rechner-einrichten-starten.bat` |
| | | **+ den echten Installer-Dateien/ZIPs selbst** unter `tools/<id>.<endung>`), |
| | | statt vorab für jede Mitarbeiter/Tool-Kombination ein festes Skript zu pflegen. |
| | | |
| | | **Wichtige Design-Entscheidung (Runde r16):** Die Installer-Dateien werden |
| | | bereits beim Erzeugen des Pakets (auf dem PC, der die Checkliste ausfüllt) |
| | | eingebettet, nicht erst beim Ausführen auf dem Zielrechner nachgeladen. Ein |
| | | früherer Ansatz lud die Dateien per `Invoke-WebRequest` zur Laufzeit vom |
| | | Server nach – das erforderte Netzwerkzugriff des Zielrechners auf den Server |
| | | in genau dem Moment der Einrichtung, was in der Praxis nicht immer gegeben |
| | | ist (z. B. Rechner noch nicht ans Firmennetz angebunden). Jetzt braucht der |
| | | Zielrechner für die eigentliche Installation **kein Netzwerk mehr** – nur die |
| | | abschließende, optionale Meldung ans zentrale Protokoll (`/zuweisungen/neu`) |
| | | versucht noch einen Netzwerkzugriff, scheitert dieser, bleibt trotzdem das |
| | | lokale Protokoll (`protokoll.log`) vollständig. |
| | | |
| | | Ablauf beim Ausführen auf dem Zielrechner: |
| | | 1. `.bat` kopiert sich selbst + das `.ps1` nach `C:\ClientBibliothek` |
| | | 1. `.bat` kopiert den **kompletten entpackten Ordner** (inkl. `tools/`- |
| | | Unterordner mit den echten Installer-Dateien) nach `C:\ClientBibliothek` |
| | | (per `xcopy /E /I /Y`) |
| | | 2. Self-Elevation (Admin-Rechte-Anfrage) über `Start-Process -Verb RunAs` |
| | | 3. Automatische Windows-Defender-Ausnahme für `C:\ClientBibliothek` |
| | | (`Add-MpPreference -ExclusionPath`) – ein einziger Pfad statt vieler |
| | | Einzelausnahmen |
| | | 4. Pro ausgewähltem Tool: entweder stille Installation (Download + Ausführung |
| | | mit hinterlegtem Silent-Parameter, `.msi` läuft über `msiexec`) oder – bei |
| | | Tools ohne einzelne Installer-Datei ("Ordner-Typ", z. B. portable |
| | | Programme wie Blender) – Download als ZIP nach `C:\ClientBibliothek\pakete\`, |
| | | **bewusst ohne automatisches Entpacken** (Entscheidung in Runde r14: der |
| | | Zielort für portable Tools variiert, das soll eine Person bewusst |
| | | 4. Pro ausgewähltem Tool: entweder stille Installation aus der **lokal |
| | | mitgelieferten** Datei unter `tools/<id>.<endung>` (mit hinterlegtem |
| | | Silent-Parameter, `.msi` läuft über `msiexec`) oder – bei Tools ohne |
| | | einzelne Installer-Datei ("Ordner-Typ", z. B. portable Programme wie |
| | | Blender) – die mitgelieferte ZIP-Datei wird nach `C:\ClientBibliothek\pakete\` |
| | | kopiert, **bewusst ohne automatisches Entpacken** (Entscheidung in Runde |
| | | r14: der Zielort für portable Tools variiert, das soll eine Person bewusst |
| | | entscheiden) |
| | | 5. Lokales Protokoll unter `C:\ClientBibliothek\protokoll.log` |
| | | 6. Automatischer POST an `/zuweisungen/neu` auf dem Server, um das zentrale |
| | | 6. Optionaler POST an `/zuweisungen/neu` auf dem Server, um das zentrale |
| | | Protokoll zu befüllen (Rechnername wird dabei über `$env:COMPUTERNAME` |
| | | automatisch erkannt) |
| | | automatisch erkannt) – best effort, kein Abbruch bei Fehlschlag |
| | | |
| | | Die Ordner-Struktur unter `C:\ClientBibliothek`: |
| | | - `\downloads` – transiente Installer-Dateien, werden nach Gebrauch gelöscht |
| | | - `\tools` – die mitgelieferten Installer-Dateien/ZIPs (Quelle für die |
| | | Installation, kommen direkt aus dem heruntergeladenen Paket) |
| | | - `\pakete` – ZIPs von portablen Tools, bleiben bewusst liegen |
| | | - `\protokoll.log` – lokale Mitschrift |
| | | |
| | | Serverseitig teilen sich `/einrichten` (POST) und `/software/<id>/download` |
| | | die Hilfsfunktion `_zippe_ordner_in_bytes()` für das ZIP-Packen eines |
| | | Ordners, um Code-Duplikation zu vermeiden. |
| | | |
| | | **Der Client-Ordner-Pfad ist eine Konstante** (`CLIENT_ORDNER` in `app.py`, |
| | | aktuell `C:\ClientBibliothek`) – zentral anpassbar, falls die Infrastruktur |
| | | einen anderen Standardpfad vorgibt. |
| | | |
| | | ## 6a. Zugriffsbeschränkung: lokal (Server) vs. Netzwerk |
| | | |
| | | Seit Runde r17 unterscheidet die Anwendung, ob eine Anfrage direkt vom Server |
| | | selbst kommt (z. B. per RDP-Sitzung, im Browser dort `localhost`/`127.0.0.1` |
| | | aufgerufen) oder über das Netzwerk von einem anderen Rechner. |
| | | |
| | | - `ist_lokaler_zugriff()` prüft `request.remote_addr` gegen `127.0.0.1`/`::1` |
| | | - `@app.before_request`-Hook `_zugriff_beschraenken()` blockiert für |
| | | Netzwerk-Zugriffe alle Routen außer denen in `OEFFENTLICHE_ENDPUNKTE` |
| | | (aktuell nur `einrichten` und `static`) mit einer 403-Seite |
| | | - Die Seite `/einrichten` zeigt sich bei Netzwerkzugriff unter anderem Namen |
| | | ("Software anfordern" statt "Rechner einrichten") und mit Hinweis auf die |
| | | Protokollierung |
| | | - Jeder Aufruf/Download über `/einrichten` wird in der Tabelle `zugriffslog` |
| | | festgehalten (Zeitpunkt, IP-Adresse, per Reverse-DNS ermittelter |
| | | Rechnername falls auflösbar, selbst angegebener Name, Aktion) – einsehbar |
| | | unter `/zugriffslog` (selbst wieder nur lokal erreichbar) |
| | | |
| | | **Wichtige Einschränkung:** Ein echter Windows-Benutzername lässt sich über |
| | | reines HTTP **nicht** zuverlässig ermitteln – dafür wäre eine |
| | | NTLM/Kerberos-Integration nötig (z. B. über einen IIS-Reverse-Proxy vor |
| | | Flask, der die Windows-Anmeldung übernimmt und den Benutzernamen per Header |
| | | an die Flask-App durchreicht). Das ist in diesem Prototyp **nicht** |
| | | umgesetzt. Aktuell basiert die Identifizierung auf der IP-Adresse, einem per |
| | | Reverse-DNS aufgelösten Rechnernamen (funktioniert nur, wenn im |
| | | Firmennetzwerk DNS-Einträge für Client-Rechner existieren) und dem selbst im |
| | | Formular angegebenen Namen (nicht verifiziert). Für eine belastbarere |
| | | Identifizierung müsste diese IIS/NTLM-Integration nachgerüstet werden. |
| | | |
| | | **Anpassung an andere Infrastruktur:** Sollte der Server z. B. hinter einem |
| | | Reverse-Proxy laufen, liefert `request.remote_addr` ggf. die Proxy-IP statt |
| | | der echten Client-IP – in dem Fall müssten `X-Forwarded-For`-Header |
| | | ausgewertet werden (aktuell nicht implementiert, da kein Proxy im Einsatz). |
| | | |
| | | ## 7. Bekannte Grenzen / nicht vollständig getestet |
| | | |
| | | - **Keine Authentifizierung.** Es gibt kein Login-System. Jeder, der den |
| | | Server im Netzwerk erreichen kann, kann Software verwalten, Installer |
| | | herunterladen und Installations-Pakete erzeugen. Das war für den internen, |
| | | vertrauenswürdigen Einsatz im Firmennetz eine bewusste Vereinfachung – |
| | | **bei einer Anpassung an eine andere/größere Infrastruktur unbedingt |
| | | prüfen, ob das so bleiben darf**, insbesondere wenn der Server auch von |
| | | außerhalb des Firmennetzes erreichbar sein könnte. |
| | | - **Keine echte Authentifizierung, nur Herkunfts-Beschränkung.** Seit r17 |
| | | ist die Verwaltung (Katalog, Protokolle, Zugriffsprotokoll) nur noch vom |
| | | Server selbst aus erreichbar (siehe Abschnitt 6a) – das ist aber eine |
| | | Netzwerk-/IP-basierte Einschränkung, **kein Login mit Benutzername/Passwort**. |
| | | Wer direkt am Server sitzt (oder sich als diesen ausgibt, z. B. über |
| | | IP-Spoofing im selben Netzwerksegment), hat vollen Zugriff. Für ein |
| | | belastbares Sicherheitskonzept mit echten Benutzerkonten wäre ein |
| | | richtiges Login-System nötig. |
| | | - **Kein HTTPS/TLS.** Die Kommunikation läuft unverschlüsselt über HTTP. Für |
| | | ein internes, abgeschottetes Netzwerk meist unkritisch, sollte aber vor |
| | | einem größeren Rollout bewertet werden. |
| | |
| | | automatisch selbst eingetragen (`Add-MpPreference`) |
| | | - Firewall-Freigabe-Skript ergänzt, nachdem der Test im echten |
| | | Firmennetzwerk anstand (Port 5000 war durch die Windows-Firewall blockiert) |
| | | - **Installer-Dateien werden direkt ins Paket gebündelt statt zur Laufzeit |
| | | nachzuladen.** Auslöser: beim ersten Test im Firmennetz enthielt der |
| | | Download nur die Skript-Dateien, die eigentlichen Installer wurden erst |
| | | beim Ausführen auf dem Zielrechner per Netzwerk nachgeladen – das setzt |
| | | Netzwerkzugriff des Zielrechners auf den Server in genau dem Moment |
| | | voraus. Jetzt werden die Dateien beim Erzeugen des Pakets direkt mit |
| | | eingepackt, der Zielrechner braucht für die Installation selbst kein |
| | | Netzwerk mehr (nur die abschließende, optionale Protokoll-Meldung). |
| | | |
| | | ## v9 – Zugriffsbeschränkung lokal (Server) vs. Netzwerk |
| | | Auslöser: Wird der Server über das Netzwerk erreicht (nicht direkt am |
| | | Server), soll nur die Software-Anforderung sichtbar sein, um "Blödsinn" zu |
| | | verhindern (Katalog verändern, Protokolle einsehen usw. nur direkt am |
| | | Server). Wer aus dem Netzwerk Software herunterlädt, muss sich mit Namen |
| | | eintragen, und alle Zugriffe werden protokolliert. |
| | | - Unterscheidung über `request.remote_addr` (127.0.0.1/::1 = lokal, alles |
| | | andere = Netzwerk) |
| | | - Für Netzwerk-Zugriffe: alle Routen außer der Software-Anforderung |
| | | (`/einrichten`) geben 403 mit Hinweisseite zurück |
| | | - Software-Anforderung heißt bei Netzwerkzugriff bewusst anders |
| | | ("Software anfordern" statt "Rechner einrichten") und weist auf die |
| | | Protokollierung hin |
| | | - Neues Zugriffsprotokoll (`/zugriffslog`, ebenfalls nur lokal einsehbar): |
| | | Zeitpunkt, IP-Adresse, per Reverse-DNS ermittelter Rechnername (falls |
| | | auflösbar), selbst angegebener Name, Aktion |
| | | - **Bewusst nicht umgesetzt:** echte Windows-Benutzername-Erkennung. Das |
| | | bräuchte NTLM/Kerberos-Integration (z. B. IIS-Reverse-Proxy vor Flask), |
| | | was über reines Flask/HTTP nicht möglich ist – im Architektur-Dokument als |
| | | offener Punkt festgehalten, nicht stillschweigend als "erledigt" behandelt |
| | | |
| | | ## Offene Design-Frage: Skript-Ansatz vs. automatische Verteilung |
| | | Nach dem ersten Test im Firmennetz wurde nochmal grundsätzlich hinterfragt, |
| | | ob der Skript-Ansatz (manueller Download + Ausführen auf dem Zielrechner) |
| | | der richtige Weg ist, oder ob Daten "automatisch" auf dem Zielrechner landen |
| | | sollten. Eine echte automatische Verteilung ohne manuellen Schritt am |
| | | Zielrechner würde entweder eine dauerhaft laufende Helfer-Software auf jedem |
| | | Zielrechner oder Windows-Remoting (PowerShell Remoting) voraussetzen – beide |
| | | Wege brauchen im Kern eine Domänen-Infrastruktur bzw. zentrale |
| | | Zugangsdaten-Verwaltung, die aktuell nicht vorhanden ist. Diese Frage wurde |
| | | bewusst nicht in diesem Prototyp entschieden, sondern als offener Punkt für |
| | | die Übernahme festgehalten (siehe README.md). |
| | | |
| | | ## Testmethodik (durchgehend) |
| | | Da die Zielumgebung Windows ist, die Entwicklung aber in einer Linux-Umgebung |
| | |
| | | |
| | | --- |
| | | |
| | | ## 3a. Rechner einrichten (Verteilung an neue Rechner) |
| | | |
| | | Menüpunkt "Rechner einrichten": Häkchen bei den benötigten Tools setzen, |
| | | Mitarbeiter eintragen, "Installations-Paket herunterladen" klicken. Das |
| | | erzeugte ZIP enthält die ausgewählten Installer-Dateien **direkt mit** (kein |
| | | Nachladen vom Server nötig). |
| | | |
| | | Auf dem **neuen Rechner**: ZIP entpacken, `Rechner-einrichten-starten.bat` |
| | | doppelklicken. Das Skript: |
| | | - fragt bei Bedarf automatisch nach Admin-Rechten |
| | | - installiert jedes Tool still mit dem hinterlegten Silent-Parameter |
| | | - trägt automatisch eine Windows-Defender-Ausnahme für `C:\ClientBibliothek` |
| | | ein (dort landen alle Dateien gesammelt) |
| | | - meldet das Ergebnis am Ende automatisch ans zentrale Protokoll zurück |
| | | (Rechnername wird dabei selbst erkannt) |
| | | |
| | | Tools ohne einzelne Installer-Datei (z. B. bereits entpackte/portable Tools |
| | | wie Blender) werden als ZIP nach `C:\ClientBibliothek\pakete\` gelegt – |
| | | bewusst ohne automatisches Entpacken, das entscheidet ihr von Fall zu Fall |
| | | selbst. |
| | | |
| | | ⚠️ Die Windows-spezifischen Teile (Admin-Rechte-Anfrage, stille Installation) |
| | | wurden nicht auf einem echten Windows-Rechner entwickelt und getestet, nur |
| | | mit dem echten PowerShell-Parser auf Syntaxfehler geprüft sowie die |
| | | Netzwerk-/Datei-Logik separat verifiziert. Bei größerem Rollout zuerst an |
| | | 1–2 unkritischen Rechnern gegentesten. |
| | | |
| | | **Zugriff aus dem Netzwerk:** Ruft jemand die Bibliothek nicht direkt am |
| | | Server, sondern über das Netzwerk auf (z. B. von einem anderen PC), sieht |
| | | diese Person nur diese eingeschränkte Seite (dort "Software anfordern" |
| | | genannt) – der Software-Katalog, die Protokolle usw. bleiben dem Server |
| | | selbst vorbehalten (z. B. per RDP-Sitzung, dort im Browser "localhost" |
| | | statt der Netzwerk-Adresse aufrufen). Jeder Zugriff/Download aus dem |
| | | Netzwerk wird im Zugriffsprotokoll (Menüpunkt "Zugriffsprotokoll", |
| | | ebenfalls nur direkt am Server einsehbar) mit IP-Adresse und angegebenem |
| | | Namen festgehalten. |
| | | |
| | | --- |
| | | |
| | | ## 4. Automatische Aktualisierung |
| | | |
| | | Bei jedem Tool kann beim Anlegen/Bearbeiten "Automatische Aktualisierung |
| | |
| | | # 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() |
| | | |
| | | |
| | | # --------------------------------------------------------------------------- |
| | |
| | | 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 |
| | | ); |
| | | """ |
| | | ) |
| | |
| | | "@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" |
| | |
| | | |
| | | |
| | | 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)}'", |
| | |
| | | " 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 {", |
| | |
| | | "", |
| | | "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', |
| | |
| | | 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")', |
| | |
| | | "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 ""', |
| | |
| | | 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" |
| | |
| | | "typ": "installer", |
| | | "endung": os.path.splitext(gefunden)[1].lower(), |
| | | "parameter": sw["install_parameter"] or "", |
| | | "installer_pfad": pfad, |
| | | }) |
| | | continue |
| | | |
| | |
| | | "typ": "ordner", |
| | | "endung": "", |
| | | "parameter": "", |
| | | "installer_pfad": pfad, |
| | | }) |
| | | continue |
| | | |
| | |
| | | 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, |
| | |
| | | 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): |
| | |
| | | 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__": |
| | |
| | | <div class="brand-text">Server-Bibliothek<small>Software & Doku-Ablage</small></div> |
| | | </div> |
| | | <nav class="nav"> |
| | | {% if ist_lokal %} |
| | | <a href="{{ url_for('dashboard') }}" class="{{ 'active' if request.endpoint == 'dashboard' }}"> |
| | | <span class="nav-dot"></span> Software-Katalog |
| | | </a> |
| | | <a href="{{ url_for('protokolle') }}" class="{{ 'active' if request.endpoint in ['protokolle', 'protokoll_detail', 'zuweisung_neu'] }}"> |
| | | <span class="nav-dot"></span> Protokolle |
| | | </a> |
| | | <a href="{{ url_for('zugriffslog') }}" class="{{ 'active' if request.endpoint == 'zugriffslog' }}"> |
| | | <span class="nav-dot"></span> Zugriffsprotokoll |
| | | </a> |
| | | <a href="{{ url_for('einrichten') }}" class="{{ 'active' if request.endpoint == 'einrichten' }}"> |
| | | <span class="nav-dot"></span> Rechner einrichten |
| | | </a> |
| | | {% else %} |
| | | <a href="{{ url_for('einrichten') }}" class="active"> |
| | | <span class="nav-dot"></span> Software anfordern |
| | | </a> |
| | | {% endif %} |
| | | </nav> |
| | | <div class="sidebar-foot">Version: {{ app_version }}</div> |
| | | <div class="sidebar-foot">Version: {{ app_version }}{% if not ist_lokal %}<br>Netzwerkzugriff{% endif %}</div> |
| | | </aside> |
| | | <main class="main"> |
| | | {% with messages = get_flashed_messages(with_categories=true) %} |
| | |
| | | {% extends "base.html" %} |
| | | {% block title %}Rechner einrichten – Server-Bibliothek{% endblock %} |
| | | {% block title %}{{ "Rechner einrichten" if ist_lokal else "Software anfordern" }} – Server-Bibliothek{% endblock %} |
| | | {% block content %} |
| | | |
| | | <div class="page-head"> |
| | | <div> |
| | | <p class="eyebrow">Verteilung</p> |
| | | <h1>Rechner einrichten</h1> |
| | | <p class="eyebrow">{{ "Verteilung" if ist_lokal else "Selbstbedienung" }}</p> |
| | | <h1>{{ "Rechner einrichten" if ist_lokal else "Software anfordern" }}</h1> |
| | | </div> |
| | | </div> |
| | | |
| | | {% if not ist_lokal %} |
| | | <div class="panel" style="margin-bottom:18px; border-color: var(--accent);"> |
| | | <p style="margin:0; font-size:13.5px;"> |
| | | Dein Name wird zusammen mit deiner Rechner-/IP-Adresse protokolliert. |
| | | Bitte nur Tools anfordern, die du für deine Arbeit wirklich brauchst. |
| | | </p> |
| | | </div> |
| | | {% endif %} |
| | | |
| | | <div class="panel" style="margin-bottom:18px;"> |
| | | <p style="margin:0 0 8px; font-size:13.5px;"> |
| | | Häkchen bei den benötigten Tools setzen, Mitarbeiter eintragen und |
| | | herunterladen. Auf dem <strong>neuen Rechner</strong> das ZIP entpacken |
| | | und <code>Rechner-einrichten-starten.bat</code> doppelklicken. Alles landet |
| | | dabei gesammelt unter <code>C:\ClientBibliothek</code> – dafür wird |
| | | automatisch eine Windows-Defender-Ausnahme für genau diesen einen Ordner |
| | | eingetragen. Tools mit Installer-Datei werden still installiert |
| | | (<code>\downloads</code>, danach automatisch aufgeräumt). Tools ohne |
| | | Installer ("als ZIP bereitgestellt", z. B. portable Tools) landen |
| | | unangetastet als ZIP in <code>\pakete</code> – die entpackt ihr bewusst |
| | | von Hand an den gewünschten Zielort. Anschließend trägt das Skript selbst |
| | | ein Protokoll ein (zentral auf dem Server und lokal unter |
| | | <code>\protokoll.log</code>). |
| | | herunterladen. Die ausgewählten Installer-Dateien werden dabei direkt mit |
| | | ins Paket gepackt – auf dem <strong>neuen Rechner</strong> braucht ihr nur |
| | | das ZIP zu entpacken und <code>Rechner-einrichten-starten.bat</code> |
| | | doppelzuklicken, ganz ohne dass der Zielrechner Netzwerkzugriff auf |
| | | diesen Server haben muss. Alles landet dabei gesammelt unter |
| | | <code>C:\ClientBibliothek</code> – dafür wird automatisch eine |
| | | Windows-Defender-Ausnahme für genau diesen einen Ordner eingetragen. |
| | | Tools mit Installer-Datei werden still installiert. Tools ohne Installer |
| | | ("als ZIP bereitgestellt", z. B. portable Tools) landen unangetastet als |
| | | ZIP in <code>\pakete</code> – die entpackt ihr bewusst von Hand an den |
| | | gewünschten Zielort. Nur die abschließende Meldung ans zentrale Protokoll |
| | | braucht (optional) Netzwerkzugriff – schlägt das fehl, bleibt trotzdem |
| | | das lokale Protokoll unter <code>\protokoll.log</code> vollständig. |
| | | </p> |
| | | </div> |
| | | |
| Neue Datei |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="de"> |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| | | <title>Zugriff eingeschränkt – Server-Bibliothek</title> |
| | | <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |
| | | </head> |
| | | <body> |
| | | <div class="layout"> |
| | | <main class="main" style="max-width:520px; margin:60px auto;"> |
| | | <div class="panel"> |
| | | <p class="eyebrow">Zugriff eingeschränkt</p> |
| | | <h1 style="margin-bottom:14px;">Nur eingeschränkter Zugriff verfügbar</h1> |
| | | <p style="font-size:13.5px; line-height:1.6;"> |
| | | Von diesem Rechner aus (über das Netzwerk) ist nur die Software-Anforderung |
| | | verfügbar. Die vollständige Verwaltung (Software-Katalog, Protokolle usw.) |
| | | ist ausschließlich direkt am Server selbst zugänglich. |
| | | </p> |
| | | <a href="{{ url_for('einrichten') }}" class="btn" style="margin-top:10px; display:inline-block;"> |
| | | Zur Software-Anforderung |
| | | </a> |
| | | </div> |
| | | </main> |
| | | </div> |
| | | </body> |
| | | </html> |
| Neue Datei |
| | |
| | | {% extends "base.html" %} |
| | | {% block title %}Zugriffsprotokoll – Server-Bibliothek{% endblock %} |
| | | {% block content %} |
| | | |
| | | <div class="page-head"> |
| | | <div> |
| | | <p class="eyebrow">Sicherheit</p> |
| | | <h1>Zugriffsprotokoll</h1> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="hint" style="margin-bottom:16px;"> |
| | | Zeigt jeden Zugriff auf die Software-Anforderung von außerhalb des Servers. |
| | | Ein echter Windows-Benutzername lässt sich ohne Domänen-Integration nicht |
| | | zuverlässig ermitteln – hier stehen IP-Adresse, per Reverse-DNS ermittelter |
| | | Rechnername (falls auflösbar) und der selbst angegebene Name. |
| | | </div> |
| | | |
| | | {% if eintraege %} |
| | | <div class="panel" style="padding:0; overflow-x:auto;"> |
| | | <table class="data-table" style="margin:0;"> |
| | | <thead> |
| | | <tr> |
| | | <th style="padding-left:22px;">Zeitpunkt</th> |
| | | <th>Herkunft</th> |
| | | <th>IP-Adresse</th> |
| | | <th>Rechnername</th> |
| | | <th>Angegebener Name</th> |
| | | <th style="padding-right:22px;">Aktion</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | {% for e in eintraege %} |
| | | <tr> |
| | | <td style="padding-left:22px;">{{ e.zeitpunkt.replace("T", " ") }}</td> |
| | | <td> |
| | | {% if e.lokal %} |
| | | <span class="badge status-ok">Lokal (Server)</span> |
| | | {% else %} |
| | | <span class="badge status-warnung">Netzwerk</span> |
| | | {% endif %} |
| | | </td> |
| | | <td>{{ e.ip_adresse or "–" }}</td> |
| | | <td>{{ e.rechnername or "–" }}</td> |
| | | <td>{{ e.angegebener_name or "–" }}</td> |
| | | <td style="padding-right:22px;">{{ e.aktion }}</td> |
| | | </tr> |
| | | {% endfor %} |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | {% else %} |
| | | <div class="empty-state">Noch keine Zugriffe protokolliert.</div> |
| | | {% endif %} |
| | | |
| | | {% endblock %} |