Nur noch, was zum Installieren und Betreiben des Lizenzservers noetig ist
Entfernt: Tests, Architekturdokumentation und die Anwendung TESM. Beides liegt im Entwicklungsrepository alientim/TESM-DEV. Neu: packages/tesm-licensing-server -- die Serverhaelfte des Lizenzprotokolls (issue_license, resign_license, build_master_response, verify_client_request, Schluesselerzeugung). Sie war bisher Teil von tesm-licensing und lag damit auch in jeder Client-Installation. Die gemeinsame Haelfte bleibt in tesm-licensing und wird von beiden Seiten benutzt. install.sh installiert diese Serverhaelfte ausschliesslich fuer --app tesm-license und bricht ab, wenn sie fehlt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,166 +0,0 @@
|
||||
# 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 einen
|
||||
`NestedConnectionError` aus, 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`.
|
||||
|
||||
* `SECRETS` und `EXPORT` sind 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.
|
||||
* **`seq` gegen 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 `@layer`n 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):
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user