Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert im Zweig SONNET5 erhalten. Aufbau: apps/tesm-license (Anwendung), packages/tesm-core (gemeinsamer Kern), packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd, privilegierter Helfer), docs, tests. Die verwaltete Anwendung liegt in ihrem eigenen Repository; beide Repositorien bringen die gemeinsamen Pakete mit, damit sich jedes allein installieren laesst. Die wichtigsten Unterschiede zum Vorgaenger: * Keine doppelte licensing.py -- ein Paket, das beide Anwendungen installieren, statt zweier Dateien, die byte-identisch bleiben sollen. * Der Webprozess laeuft unprivilegiert; alles, was Root braucht, geht ueber einen einzigen Helfer mit Positivlisten fuer jedes Argument. * CSRF-Schutz ueberhaupt -- der Vorgaenger hatte keinen. * Rechte werden serverseitig geprueft, nicht nur im Template ausgeblendet. * Keine Lizenz ohne master_endpoint: eine Ausstellung ohne Endpunkt wird abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann. * Offline-Aktivierung in beide Richtungen; die Lizenz bleibt als "Aktivierung offen" markiert, bis sie zurueckkommt. * Getrennte Signaturkontexte je Nachrichtenart, Nonce gegen Wiedereinspielung, seq gegen das Zurueckrollen auf eine aeltere Lizenz. * Kein Hostname im Maschinen-Fingerabdruck. * Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.0 KiB
Architektur
Zwei Anwendungen, ein gemeinsamer Kern, ein gemeinsames Lizenzprotokoll.
packages/
tesm-licensing/ Lizenzformat und -protokoll (Ed25519). Keine Web-Abhaengigkeit.
tesm-core/ Der Kern: Datenbank, Rechte, Anmeldung, Sitzungen, Protokolle,
Einstellungen, Systemzugriff, Oberflaechenbausteine.
apps/
tesm/ Geraete- und Switchverwaltung (PoE, DHCP, Dateifreigaben, Wartung).
tesm-license/ Lizenzserver (Kunden, Tickets, Lizenzen, Portal).
deploy/ Helfer, sudoers, systemd, nginx, logrotate, install.sh
tests/ Unit- und Integrationstests, dazu drei E2E-Skripte fuer den Zielhost.
Warum genau diese Schnitte
tesm-licensing ist ein eigenes Paket, keine kopierte Datei.
Im Vorgaengerprojekt lag licensing.py zweimal im Repo -- einmal beim Client,
einmal beim Server -- mit der Auflage, beide byte-identisch zu halten. Das ist
keine Invariante, die ein Mensch dauerhaft einhaelt. Hier gibt es das Modul
einmal; beide Anwendungen installieren dasselbe Paket.
tests/test_repo_hygiene.py schlaegt Alarm, sobald irgendwo wieder eine Datei
namens licensing.py auftaucht.
tesm-core enthaelt alles, was beide Anwendungen gleich machen.
Nicht nur Technik (Datenbank, Sitzungen, CSRF), sondern auch ganze Seiten:
Benutzer- und Gruppenverwaltung, Protokollansichten, Papierkorb, Diagnose,
Netzwerk-, Webserver- und Verzeichnisdiensteinstellungen. Eine Seite, die in
beiden Anwendungen gleich aussehen soll, existiert genau einmal.
Die Apps bringen nur ihre Fachlichkeit mit -- plus vier Beschreibungen, die
der Kern auswertet: permissions.py (Rechtebaum), navigation.py,
schema.py (Migrationen), settings_spec.py.
Datenbank
Rohes sqlite3, kein ORM. Begruendung: das Datenmodell ist klein und stabil,
die Abfragen sind lesbar, und es gibt keine Schicht, die im Fehlerfall erst
verstanden werden muss.
Die Klasse tesm_core.db.Database erzwingt drei Dinge:
- WAL-Modus,
foreign_keys=ON,busy_timeout=15000. - Verschachtelte Transaktionen laufen ueber SAVEPOINTs.
- Ein
connect()waehrend einer offenen Schreibtransaktion loest einenNestedConnectionErroraus, statt in einen Deadlock zu laufen. Das ist der strukturelle Riegel gegen die haeufigste Fehlerklasse des Vorgaengers: eine Funktion oeffnete waehrend eines Schreibvorgangs eine zweite Verbindung, und die Anwendung blieb stehen, bis das Zeitlimit zuschlug.
Migrationen sind versioniert nach (komponente, version) mit Pruefsumme. Sie
laufen ueber split_statements(), nicht ueber executescript() -- letzteres
committet implizit und zerlegt die umgebende Transaktion.
Rechte
Ein Rechtebaum aus Bereich -> Ressource -> Aktion. Die Aktionen sind ein
festes Vokabular: VIEW, CREATE, EDIT, DELETE, EXECUTE, EXPORT,
SECRETS.
SECRETSundEXPORTsind eigene Rechte. Wer Zugangsdaten bearbeiten darf, darf sie damit noch nicht im Klartext sehen oder ausfuehren.- Bereiche haben einen Hauptschalter: ein abgeschalteter Bereich ist weg, unabhaengig von Einzelrechten.
- Keine Rechteausweitung: niemand kann einem anderen Konto ein Recht geben,
das er selbst nicht hat (
guard_no_amplification). Ebenso wenig laesst sich der letzte Administrator entfernen oder sperren (guard_last_admin). - Jede Pruefung ist serverseitig. Das Template blendet zusaetzlich aus, was nicht erlaubt ist -- aber es entscheidet nichts. Im Vorgaenger wurden Verzeichnisdienst und Webserver nur im Template geprueft; ein direkter POST kam durch.
Lizenzierung
Ed25519, mit den Haertungen, die ein Signaturprotokoll erst brauchbar machen:
- Getrennte Signaturkontexte fuer Lizenz, Anfrage und Antwort. Eine Signatur aus dem einen Kontext ist im anderen wertlos.
- Kanonisches JSON -- die Signatur haengt nicht an der Schluesselreihenfolge.
- Nonce gegen Wiedereinspielung, Antwort an die Anfrage gebunden.
- Frischefenster gegen alte, mitgeschnittene Antworten.
seqgegen Rueckstufung: eine aeltere Revision wird abgelehnt.- Aussteller-Pinning (Trust on first use). Die erste eingespielte Lizenz pinnt den oeffentlichen Schluessel ihres Ausstellers. Ein Wechsel verlangt eine ausdrueckliche Bestaetigung durch einen Administrator.
- Fingerabdruck ohne Hostnamen. Er bildet sich aus Maschinen-ID und DMI-UUID. Der Hostname ist bewusst nicht enthalten: im Vorgaenger brach jedes Umbenennen des Rechners die Bindung.
Der Ablauf: Kunde -> Ticket -> Lizenz. Das Ticket haelt die Parameter (Typ, Module, Laufzeit, Rolle); die Lizenz ist das signierte Ergebnis. Ein zweiter Klick auf "Ausstellen" erzeugt keine zweite Lizenz, sondern zeigt die offene.
Aenderungen an einer laufenden Lizenz brauchen keine Neuinstallation: der Anbieter signiert eine neue Revision, der Client holt sie beim naechsten Heartbeat.
Systemzugriff
Der Webprozess laeuft unprivilegiert. Root-Aktionen gehen ausschliesslich durch
deploy/tesm-helper: ein Skript mit fester Verbliste, Argumentpruefung gegen
Positivlisten und ohne jede Weitergabe an eine Shell. sudoers gibt genau diesen
einen Pfad frei.
Aktionen, die mehrere Schritte brauchen, sind ein Verb -- nicht drei.
nginx-apply etwa schreibt, prueft und laedt neu; scheitert nginx -t, stellt
es den vorherigen Stand wieder her. Getrennte Verben hatten hier eine echte
Luecke: nginx-write verlinkt sofort nach sites-enabled, und eine
anschliessend abgelehnte Datei blieb aktiviert liegen. Der laufende nginx merkte
nichts -- der naechste Reload durch irgendwen legte den ganzen Webserver lahm.
Mehrere Installationen auf einem Host
Zwei Schluessel, mit klarer Aufgabenteilung:
app_key(tesm,tesm-license) -- was die Anwendung ist. Bestimmt Env-Praefix, Rechtebaum, Migrationsnamensraum.site_key(tesm,tesm-opus, ...) -- welche Installation es ist. Bestimmt alles ausserhalb von/srv/<name>: nginx-Site, netplan-Datei, logrotate-Regel, Zertifikatsverzeichnis und den Namen des Sitzungscookies.
Ohne diese Trennung wuerde eine Testinstanz beim Speichern der Webserver-Einstellungen die Site-Datei der Produktion ueberschreiben -- und beide Installationen teilten sich dasselbe Sitzungscookie, weil Cookies keine Ports unterscheiden.
Die Adresse des Anwendungsservers (BIND) steht in derselben Umgebungsdatei,
aus der auch gunicorn seinen --bind bekommt. Sie ist zugleich das
proxy_pass-Ziel der erzeugten nginx-Site. Vorher war die 5000 fest im Code:
eine Instanz auf Port 5100 erzeugte damit eine Konfiguration, die auf die
andere Installation zeigte.
Oberflaeche
Handgeschriebenes CSS in @layern mit semantischen Tokens, drei Themen (hell,
dunkel, System). JavaScript als ES-Module, angebunden ueber
data-behavior-Attribute -- kein Inline-Script, kein Build-Schritt, und damit
vertraeglich mit einer strikten CSP, die unsafe-inline fuer Skripte nicht
erlaubt.
Tests
| Datei | Was sie absichert |
|---|---|
test_licensing.py |
Format, Signaturen, Protokoll, Fingerabdruck |
test_core_smoke.py |
Kern: Datenbank, Migrationen, Schluesselspeicher, Sitzungen |
test_core_security.py |
Passwoerter, TOTP, Ratenbegrenzung, CSRF, Rechte |
test_webserver.py |
nginx-Vorlage, TLS, Zertifikate, Instanztrennung |
test_tesm_smoke.py |
TESM: Seiten, Validierung, Rechtematrix |
test_license_server.py |
Lizenzserver: Ausstellung, Portal, API |
test_repo_hygiene.py |
Zeilenenden, Geheimnisse im Repo, Helferpruefungen, sudoers |
Dazu drei Skripte, die auf dem Zielhost gegen die echte Installation laufen (nginx, gunicorn, systemd, TLS -- keine Attrappen):
python3 tests/e2e_smoke.py https://host admin 'Passwort'
python3 tests/e2e_license_server.py https://lizenz.host admin 'Passwort'
python3 tests/e2e_license.py https://lizenz.host https://host admin 'Passwort' http://127.0.0.1:5001
Das dritte spielt den vollstaendigen Lizenz-Lebenszyklus ueber beide Anwendungen hinweg durch: ausstellen, herunterladen, einspielen, aktivieren, Module pruefen, Heartbeat, Revision, Widerruf.