Commit Graph
18 Commits
Author SHA1 Message Date
alientimandClaude Opus 5 8e66d9e7bc apt-Installation laeuft ausserhalb des Dienst-Sandkastens
Der Sandkasten wird an dpkg und dessen Maintainer-Skripte weitervererbt; mit
ProtectKernelLogs=yes liefert "dmesg" ein EPERM und ein Postinst-Skript bricht
daran ab -- die Installation scheitert mit "dpkg returned an error code (1)",
obwohl mit dem Paket alles in Ordnung ist. apt laeuft jetzt ueber systemd-run,
also von PID 1 gestartet und damit ohne unsere Haertung.

Der zuvor eingebaute nsenter-Umweg beim Einbinden von Freigaben ist
zurueckgenommen. Diese Verben ruft der Lizenzserver nicht auf -- der Helfer
bleibt aber in beiden Repositorien dieselbe Datei, weil eine zweite Fassung
zwei Stellen waere, die auseinanderlaufen koennen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:49:56 +02:00
alientimandClaude Opus 5 4bc8637a74 Freigaben einbinden: Mount im Namensraum des Wirts
Eine Freigabe wurde nie eingebunden -- "mount error(1): Operation not
permitted". Gemessen: mit denselben Optionen und Zugangsdaten gelingt die
Einbindung aus einer normalen Root-Shell sofort. Ursache ist der eigene
Mount-Namensraum des Dienstes (ProtectSystem, ProtectHome, PrivateTmp,
ReadWritePaths); darin laesst sich kein neues Dateisystem einbinden.

Nachgebaut und belegt: derselbe Aufruf scheitert in einem transienten Unit mit
dieser Haertung und gelingt, sobald er ueber nsenter im Namensraum des Wirts
laeuft. Von dort wird der Mount in den Namensraum des Dienstes weitergegeben,
die Anwendung sieht ihn also -- geprueft, inklusive Lesbarkeit fuer den
Dienstbenutzer.

mount-cifs und umount laufen deshalb ueber nsenter. Ohne nsenter im System
bleibt das Verhalten unveraendert. Die Haertung des Dienstes bleibt vollstaendig
erhalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v2.0.1
2026-09-02 21:20:52 +02:00
alientimandClaude Opus 5 6ac2ed0a62 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>
v2.0.0
2026-09-02 18:37:12 +02:00
alientimandClaude Opus 5 f7805a2180 TESM-Lizenzserver 2.0.0 -- Neubau
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>
2026-09-02 18:01:23 +02:00
alientimandClaude Sonnet 5 c130c85b17 Sub-Lizenzserver melden ihren Kunden-/Ticket-/Lizenzstand an den Master
Ein Sub-Lizenzserver (role='license_server') meldet jetzt seinen
kompletten eigenen Kunden-/Ticket-/Lizenzstand an den Master, der ihn
als Ueberblick auf einer neuen Kunden-Detailseite anzeigt: unter dem
Kunden, der die Lizenzserver-Lizenz haelt, erscheinen dessen eigene
Kunden mit ihren Tickets/Lizenzen -- genau wie gewuenscht.

Transport: huckepack auf dem bereits bestehenden 6-Stunden-Heartbeat
(licensing.py: neues optionales 'report'-Feld in
build_client_request(), signiert wie jedes andere Feld -- ein
Angreifer ohne den privaten Lizenzschluessel kann keinen gefaelschten
Bericht unterschieben). Zusaetzlich sofortiger Best-Effort-Report per
Hintergrund-Thread nach jeder lokalen Ticket-/Kunden-/Lizenz-Aenderung
(_report_to_master_async()), damit der Master nicht bis zu 6h auf
Aktualitaet warten muss. Funktioniert automatisch auch ueber den
Offline-Code-Weg (/licenses/manual-code), da beide Transportwege
denselben _process_heartbeat()-Code nutzen.

- Drei neue, rein informative Tabellen (reported_sub_customers/
  _tickets/_licenses), verankert an source_license_id (die Lizenz,
  die den Sub-Lizenzserver identifiziert). Ein Bericht ist immer ein
  VOLLER Schnappschuss -- bei jedem Empfang werden alle bisherigen
  Zeilen dieser source_license_id ersetzt (kein Diffing noetig, ein
  beim Sub-Lizenzserver geloeschtes Ticket verschwindet dadurch
  automatisch korrekt auch hier).
- _build_own_issuance_report(): baut den Schnappschuss auf der
  Sub-Lizenzserver-Seite.
- _ingest_sub_report(): uebernimmt ihn auf der Master-Seite, NUR wenn
  row['role']=='license_server' fuer die absendende Lizenz (Verteid-
  igung in der Tiefe -- row['role'] steht in der Master-DB fest, nicht
  im Client-Request, ein gewoehnlicher Kunde kann sich also nicht als
  Lizenzserver ausgeben).
- Neue Kunden-Detailseite (/customers/<id>, customer_detail.html):
  Stammdaten, eigene Tickets bei uns, und -- falls dieser Kunde eine
  aktive role='license_server'-Lizenz haelt -- die gemeldeten eigenen
  Kunden mit Tickets/Lizenzen (Ampelfarbe/Resttage ueber dieselbe
  _license_dict_for_display()-Funktion wie ueberall sonst). Kunden-
  liste verlinkt jetzt auf die Detailseite, zeigt 'Lizenzserver'-Badge.

Getestet: licensing.py isoliert (report signiert+verifiziert,
Manipulation bricht die Signatur, unveraendertes Verhalten ohne
report-Argument), _ingest_sub_report() isoliert (Erstbericht korrekt
uebernommen, Zweitbericht ersetzt korrekt inkl. verschwindender
Tickets, Isolation zwischen mehreren source_license_id sauber).
ZUSAETZLICH ein vollstaendiger Zwei-Prozess-Integrationstest ueber
echtes HTTP (zwei unabhaengige, native laufende Instanzen: 'Master' +
'Sub-Lizenzserver' auf unterschiedlichen Ports): Ticket mit
role=license_server angelegt, Lizenz ausgestellt, auf der zweiten
Instanz hochgeladen und ONLINE aktiviert, dort ein eigener Endkunde
samt Ticket/Lizenz angelegt, automatischer Report ausgeloest --
Master-Kundendetailseite zeigt den gemeldeten Endkunden mit korrektem
Ticket-Typ/Laufzeit/Status und aktuellem Heartbeat-Zeitstempel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.7
2026-08-23 22:45:17 +02:00
alientimandClaude Sonnet 5 ed2ed00a4e Sub-Lizenzserver: Lizenzen fuer Lizenzserver ausstellbar (role='license_server')
Der Master kann jetzt ueber den normalen Kunde->Ticket->Lizenz-Flow
Lizenzen ausstellen, die eine ANDERE Lizenzserver-Instanz voll
funktionsfaehig machen (Kunden/Tickets/Lizenzen fuer eigene Kunden
ausstellen, Benutzer/Gruppen/Einstellungen verwalten) -- mit genau
einer strukturellen Einschraenkung: der so lizenzierte Sub-Lizenzserver
kann selbst KEINE weiteren Lizenzserver-Lizenzen ausstellen. Die
eigene Lizenz DIESES Masters bleibt weiterhin ausschliesslich per
create_master_license.py erzeugbar, nie ueber die Ticket-UI.

- licensing.py: neues, signiertes role-Feld (master/license_server/
  customer), siehe separates TESM-Release fuer die synchronisierte
  Kopie (harte Invariante: beide Dateien bleiben identisch).
- create_master_license.py: setzt role='master' beim Selbstausstellen.
- Neue STRUKTURELLE Sperre can_issue_license_server_licenses(): True
  NUR wenn die eigene, aktive Lizenz dieser Instanz role=='master' ist
  -- haengt an der Instanz-Rolle, nicht an einer vergebbaren Permission,
  daher von lokalen Admins eines Sub-Lizenzservers nicht aufhebbar.
  Kombiniert mit current_user.is_admin an jeder Aufrufstelle (nur echte
  Admins duerfen es je nutzen, wie gewuenscht -- kein delegierbares
  Recht).
- license_tickets.role-Spalte (Migration, Default 'customer').
  license_tickets.role bestimmt, welche Rolle eine daraus ausgestellte
  Lizenz traegt -- nach Anlage unveraendert wie customer_id.
- ticket_new(): neue, nur fuer can_issue_license_server_licenses()+Admin
  sichtbare Option 'Dies ist eine Lizenzserver-Lizenz'. Jeder andere
  Versuch (manipuliertes Formular, Sub-Lizenzserver, Nicht-Admin) wird
  serverseitig still auf 'customer' zurueckgestuft. Module werden fuer
  role!='customer' erzwungen auf [] (TESM-Client-Konzept, fuer einen
  Lizenzserver bedeutungslos).
- Self-Service (unauthentifiziert, nur ueber die Ticket-ID) ist fuer
  role!='customer'-Tickets komplett gesperrt (404) -- Lizenzserver-
  Lizenzen sind ausschliesslich admin-verwaltet, kein Kundenweg. Trial-
  Ausstellung ('30 Tage, alle Module') ebenfalls nur fuer role=customer
  moeglich (ValueError sonst, sauber abgefangen statt 500er).
- license_send_email(): ueberspringt Self-Service-/Kundenportal-Hinweise
  fuer role='license_server'-Lizenzen (waeren ohnehin blockiert).
- UI: 'Lizenzserver'-Badge auf Tickets-/Lizenzen-Liste und Ticket-
  Detailseite.

Getestet: licensing.py isoliert (role signiert+verifiziert, Manipulation
bricht die Signatur, Altformat ohne role faellt auf 'customer' zurueck),
can_issue_license_server_licenses()-Logik (6 Kombinationen), Schema-
Migration (Alt-DB ohne role-Spalte, idempotent), Ende-zu-Ende Ticket->
Lizenz-Ausstellung gegen echte create_db.py-DB. ZUSAETZLICH gegen einen
echten, lokal laufenden Flask-Server (native Windows-Python, WSL-
Netzwerksandbox in dieser Umgebung unzuverlaessig) voll durchgespielt:
Checkbox erscheint nur fuer role=master, Ticket+Lizenz-Ausstellung mit
role=license_server funktioniert Ende-zu-Ende (signierte Datei, Module
leer, Self-Service liefert 404), UND der Sicherheitstest: nach Wechsel
der eigenen Lizenz auf role=license_server verschwindet die Checkbox
UND ein direkter manipulierter POST mit role=license_server wird
serverseitig zuverlaessig auf 'customer' zurueckgestuft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.6
2026-08-23 22:08:19 +02:00
alientimandClaude Sonnet 5 286a3464f1 Eigenes Recht fuer Tickets (tickets.view/.create/.edit)
Tickets liefen im Rechtebaum komplett ueber licenses.view/.create/.edit
mit, obwohl Tickets ein eigener, gleichberechtigter Navigationspunkt
neben Kunden und Lizenzen ist -- analog zu TESM, wo Clients/Switche/
Zugangsdaten trotz enger fachlicher Verwandtschaft jeweils eigene
Rechte haben. Ergaenzt die im vorigen Release (v1.2.4) behobene
Lizenz-Rechte-Luecke um diese zweite, gleich gelagerte Luecke.

- Neues Rechtepaar tickets.view/tickets.create/tickets.edit
  (PERMISSIONS/PERMISSION_LABELS), Property can_manage_tickets.
- Nav-Sichtbarkeit sowie tickets_list()/ticket_new()/ticket_edit()/
  ticket_delete() auf das neue Recht umgestellt.
- ticket_detail(): GET-Zugriff jetzt ueber can_manage_tickets. POST
  jetzt PRO SUB-AKTION geprueft statt eines einzigen Gates:
  'toggle_block' (Sperren/Entsperren) -> tickets.edit (neu, Ticket-
  Domain); 'issue_license' (erstellt eine LIZENZ) bleibt bewusst
  unveraendert an licenses.edit gebunden, damit sich 'Tickets
  verwalten' und 'Lizenzen ausstellen' unabhaengig voneinander
  vergeben lassen, ohne dass irgendjemand automatisch die eine
  Faehigkeit durch die andere verliert.
- tickets.html/ticket_detail.html: can_edit-Variable entsprechend in
  can_edit_ticket/can_issue_license aufgeteilt.
- Migration (_migrated_tickets_permission_v1, einmalig geguarded):
  jede Gruppe mit licenses.view/.create/.edit bekommt automatisch das
  gespiegelte tickets.view/.create/.edit dazu -- niemand verliert beim
  Update stillschweigend eine Faehigkeit, die vorher implizit ueber
  licenses.* mitlief.
- Getestet gegen isolierte Test-DB: Backfill korrekt (Gruppe mit voller
  Lizenz-Berechtigung bekommt volle Ticket-Berechtigung, Gruppe mit nur
  .view bekommt nur tickets.view), idempotent, keine Duplikate.
  PERMISSIONS/PERMISSION_LABELS/NAV_ITEMS erneut vollstaendig
  konsistent (keine Nav-Keys mehr ohne Rechte-Entsprechung), app.py
  syntaktisch fehlerfrei kompiliert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.5
2026-08-23 21:33:33 +02:00
alientimandClaude Sonnet 5 619673ac76 Eigenes Recht fuer die Lizenzseite (settings_license.view/.edit)
Die Lizenzseite (Einstellungen -> Lizenz) hatte im Rechtebaum keinen
eigenen Eintrag -- sie lief komplett ueber settings_system.view/.edit
mit, genau wie im TESM-Client (siehe dortiger Fix). Damit konnte
'Lizenz verwalten' nicht unabhaengig von 'Systemeinstellungen
verwalten' vergeben/eingeschraenkt werden.

- Neues Rechtepaar settings_license.view/settings_license.edit
  (PERMISSIONS/PERMISSION_LABELS), Property can_view_settings_license.
- Nav-Sichtbarkeit und alle /settings/license*-Routen auf das neue
  Recht umgestellt (view-Route: can_view_settings_license; alle
  mutierenden Routen -upload/-activate(/confirm)/-deactivate(/confirm):
  settings_license.edit). Das 'auch ohne aktive Lizenz sichtbar
  bleiben'-Verhalten bleibt unveraendert (andere Pruefung, license_active()).
- settings_license.html: can_edit auf settings_license.edit umgestellt.
- Migration (_migrated_settings_license_permission_v1, einmalig
  geguarded): jede Gruppe mit settings_system.view/.edit bekommt
  automatisch settings_license.view/.edit dazu -- niemand verliert beim
  Update stillschweigend Zugriff, den er vorher implizit hatte.
- Getestet gegen isolierte Test-DB (create_db.py-Frischinstallation):
  Migration korrekt (inkl. Gruppe mit nur .view, die korrekt KEIN .edit
  bekommt), idempotent bei zweitem Lauf, keine Duplikate. PERMISSIONS/
  PERMISSION_LABELS/NAV_ITEMS strukturell konsistent geprueft, app.py
  syntaktisch fehlerfrei kompiliert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.4
2026-08-23 21:11:02 +02:00
alientimandClaude Sonnet 5 20abadb0e7 Dashboard erfordert Login, Magic-Links nutzen konfigurierten Endpunkt statt IP (v1.2.3)
## Dashboard nur noch intern

'/' (das Kachel-Dashboard) verlangt jetzt @login_required -- ein anonymer
Besuch landet zuerst auf der Login-Seite (ueber Flask-Logins bereits
konfigurierten login_manager.login_view), statt innerhalb der Dashboard-
Huelle einen 'bitte anmelden'-Platzhalter zu zeigen. Anders als bei TESM
selbst (dessen Geraete-Dashboard bewusst auch ohne Login als Status-Board
sichtbar bleibt) ist der Lizenzserver ein reines internes Verwaltungs-
werkzeug.

## Magic-Links zeigen den konfigurierten Lizenzserver-Endpunkt, nicht die IP

Self-Service-Link, Kundenportal-Anfrage/-Zugang und der in 'Lizenz per
E-Mail senden' eingebettete Link wurden bisher per url_for(...,
_external=True) gebaut -- das nutzt den Host-Header der GERADE laufenden
Anfrage, zeigt also z.B. die interne IP, ueber die zufaellig der Admin
gerade eingeloggt ist, statt der unter Einstellungen -> Lizenz ->
Anbieter & Lizenzserver gepflegten oeffentlichen Domain. Neue Hilfs-
funktion _customer_facing_url() baut diese Links jetzt stattdessen aus
dem admin-gepflegten Lizenzserver-Endpunkt (_get_master_endpoint(),
derselbe Wert, der auch in jede Lizenzdatei eingebettet wird) -- faellt
auf die alte, request-basierte externe URL zurueck, wenn der Endpunkt
noch nicht konfiguriert ist (frische Installation).

Verifiziert gegen eine echte Kopie der Live-Datenbank: mit konfiguriertem
Endpunkt zeigt der Self-Service-Link jetzt https://tesml.int.eertmoed.net/...
statt der IP, unabhaengig davon, ueber welchen Host der Admin gerade
zugreift; ohne konfigurierten Endpunkt bleibt der bisherige Fallback
erhalten.

## Ausserdem live behoben (kein Code-Fix, reine Konfiguration)

/etc/nginx/sites-available/tesm-license auf dem Master zeigte fuer
/static/ faelschlich auf /srv/tesm/static/ (ein laengst nicht mehr
existierendes Verzeichnis -- Ueberbleibsel aus der Zeit vor der
Repo-Trennung) statt auf /srv/tesm-license/static/, wodurch CSS/JS/
Bilder ueberall 404eten (nackte, ungestylte Seiten). install.sh schreibt
die nginx-Konfiguration bewusst nur bei einer FRISCHEN Installation
(um manuelle Anpassungen wie TLS-Zertifikate bei Updates nicht zu
ueberschreiben) -- das im Repo hinterlegte Template selbst war bereits
korrekt, nur die schon laenger bestehende Datei auf dem Master war es
nicht. Direkt auf dem Master korrigiert und nginx neu geladen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.3
2026-08-23 17:34:39 +02:00
alientimandClaude Sonnet 5 2d920f581f Eigenstaendige Lizenzen-Seite, entschachtelte Ticket-Ansicht, zeitlich begrenzter Kundenportal-Zugang (v1.2.2)
Reaktion auf Feedback: 'Tickets und Lizenzen je Kunden war sehr sehr
unintuitiv dargestellt... Kunde -> Ticket -> Lizenz aber alles auf extra
Seiten, im Menü einzeln anwählbar ohne diese riesen Verschachtelung.'

## Lizenzen als eigene, gleichberechtigte Ebene (neu: /licenses)

Bisher war eine Lizenz nur ueber Kunde -> gefilterte Tickets -> Ticket-
Detail -> eingebettete Historie-Tabelle erreichbar. Jetzt: Kunden,
Tickets UND Lizenzen sind drei eigene, direkt in der Navigation
erreichbare Seiten (Reihenfolge im Menü spiegelt bewusst die fachliche
Hierarchie). Kunden- und Ticket-Seite verlinken mit Filter (?customer_id=/
?ticket_id=) dorthin, statt Lizenzdaten selbst einzubetten.

Ticket-Detailseite dadurch massiv entschlackt: statt einer vollen
Lizenz-Historie-Tabelle nur noch eine kompakte 'Aktuelle Lizenz'-Zeile
mit Link auf die (ggf. gefilterte) Lizenzen-Seite fuer die volle Historie.

## Kundenportal: kein dauerhafter Link mehr, sondern zeitlich begrenzter Zugang

Vorheriger Ansatz (v1.2.0): ein dauerhafter, unbefristeter Token pro
Kunde, von Admins verteilt. Auf Nachfrage ('wie bekommt der Kunde seinen
Magic-Link, wenn er ihn verloren hat?') kam eine oeffentliche
'Zugang anfordern'-Seite dazu (E-Mail eingeben -> Link zugeschickt) --
und direkt im Anschluss die berechtigte Sicherheitsfrage, ob so ein
oeffentlich auf blossen Zuruf ausloesbarer Link nicht zeitlich begrenzt
sein sollte. Ergebnis: der dauerhafte Link wurde komplett entfernt.

Neuer, einziger Weg ins Kundenportal:
1. /portal/request -- Kunde gibt seine E-Mail ein (oeffentlich, keine
   Anmeldung noetig). Immer dieselbe generische Erfolgsmeldung,
   unabhaengig davon ob die Adresse bekannt ist (keine Enumeration).
2. Bei bekannter Adresse: ein frischer, einmalig nutzbarer Zugangs-Token
   (portal_access_tokens-Tabelle) wird per E-Mail verschickt, gueltig nur
   PORTAL_ACCESS_LINK_MINUTES (30) ab Anforderung.
3. /portal/access/<token> prueft Gueltigkeit + Einmalnutzung, markiert
   den Token als benutzt und setzt eine Session, die ihrerseits nach
   PORTAL_SESSION_MINUTES (60) automatisch ablaeuft.
4. /portal/me zeigt das eigentliche Portal (alle Tickets/Lizenzen,
   Kontaktdaten pflegen) -- ausschliesslich session-basiert erreichbar.

license_customers.portal_token bleibt in der DB bestehen (keine riskante
Schema-Ruecknahme), wird fuer die Zugriffsentscheidung aber nicht mehr
herangezogen -- alle Stellen, die vorher den dauerhaften Portal-Link
eingebettet haben (Ticket-Seite, Lizenz-per-E-Mail, Self-Service-Seite),
verweisen jetzt stattdessen auf /portal/request.

Verifiziert: bestehende Funktionstests erneut komplett durchlaufen (neue
Lizenzen-Seite, entschachtelte Ticket-Seite, entfernter dauerhafter
Zugang liefert 404), plus 11 neue Tests fuer den kompletten Magic-Link-
Fluss (Token-Erzeugung, Einmalnutzung, Ablehnen bei Wiederverwendung/
unbekanntem/abgelaufenem Token, Session bleibt nach Erstnutzung gueltig,
/portal/me ohne Session leitet zur Anfrage-Seite um) -- alle bestanden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.2
2026-08-23 17:03:55 +02:00
alientimandClaude Sonnet 5 c2989a6e2f KRITISCH: 'database is locked' beim Aktivieren blockierte Online- UND Offline-Aktivierung (v1.2.1)
_process_activate() hielt beim Ersetzen einer bereits aktiven Lizenz
fuer denselben Fingerprint (z.B. Trial durch Enterprise, oder schlicht
eine Re-Aktivierung nach einem Systemwechsel) eine offene, noch nicht
committete Schreibtransaktion auf 'conn', waehrend es innerhalb der
Supersede-Schleife log_action_system() aufrief -- diese Funktion oeffnet
INTERN eine EIGENE, zweite Verbindung zur selben Datenbank und versucht
sofort zu committen. SQLite erlaubt aber nur einen Schreiber gleichzeitig
(kein WAL-Modus): die zweite Verbindung wartet auf die erste, die
wiederum auf die zweite wartet, bis das Busy-Timeout (Python-Default 5s)
mit 'sqlite3.OperationalError: database is locked' aufgibt.

Live reproduziert: sowohl /api/activate (Online) als auch
/licenses/manual-code (Offline-Code) schlugen dadurch fehl, sobald ein
Kunde eine Lizenz aktivierte, die eine fuer denselben Fingerprint bereits
aktive ersetzt -- de facto war jede Aktivierung auf einem bereits
bekannten System blockiert.

Fix: die betroffenen log_action_system()-Aufrufe in der Supersede-
Schleife werden nur noch vorgemerkt (reine Lesezugriffe auf die
offene Verbindung sind unproblematisch) und erst NACH conn.commit()/
conn.close() nachgeholt. Zusaetzlich get_db_connection() auf
timeout=15 (statt Python-Default 5s) angehoben als generelle
Verteidigungslinie gegen kurze, legitime Ueberschneidungen -- ersetzt
nicht die eigentliche Regel (niemals eine zweite Verbindung waehrend
einer noch offenen Schreibtransaktion committen), ist aber zusaetzliche
Sicherheit.

Alle anderen log_action()/log_action_system()-Aufrufstellen in dieser
Session (customers(), ticket_new/-edit/-delete, license_delete,
customer_portal, _issue_license_from_ticket, _revoke_license_row,
_process_deactivate, _process_heartbeat) wurden systematisch geprueft --
committen bereits korrekt VOR dem Logging, betroffen war ausschliesslich
diese eine Stelle.

Verifiziert: direkter Reproduktionstest gegen eine echte Kopie der
Live-Datenbank (POETESTs echte aktive Lizenz wird durch eine zweite
ersetzt, exakt das gemeldete Szenario) -- vorher deadlock/Timeout,
nachher 0.04s, korrekt superseded + Hostname + Audit-Log-Eintraege.
Als Notfall-Hotfix bereits direkt auf dem Live-Master eingespielt
(Dienst neu gestartet, seither keine weiteren 'database is locked'-
Fehler); dieser Commit bringt Repo/Release auf denselben Stand, damit
'latest' nicht wieder auf die kaputte v1.2.0 zurueckfaellt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.1
2026-08-23 16:33:16 +02:00
alientimandClaude Sonnet 5 cee5311b1d Ticketverwaltung, Kundenstammdaten, Kundenportal, Lifetime-Lizenzen, Dashboard-Kacheln (v1.2.0)
Reaktion auf mehrere Bug-Reports und Feature-Wuensche zum bestehenden
Ticket-System (v1.1.0): 'Logs gehen nicht', 'Ticketverwaltung fehlt',
'Lizenzen koennen nicht vom Dashboard geloescht werden', plus eine Reihe
konkreter neuer Anforderungen.

## Bugfixes

- /logs: behob einen Absturz (jinja2.exceptions.UndefinedError:
  'global_check_interval' is undefined) UND die eigentliche Ursache
  dahinter -- logs.html war unveraendert aus dem TESM-Fork uebernommen
  (PoE-Geraete-Polling-Text/-Farblogik, ein Template-Feld, das app.py nie
  befuellt hat) und ausserdem schrieb auf diesem Server ueberhaupt nichts
  in live.log (kein poe.sh-Aequivalent vorhanden). Live-Log zeigt jetzt
  echte Ticket-/Lizenzaktionen, mit passendem Hinweistext und Faerbung.
- Lizenz-Loeschung war bereits vorhanden, aber nur ueber Ticket- ->
  Lizenz-Detailseite erreichbar -- jetzt zusaetzlich inline in der
  Lizenzhistorie jedes Tickets.

## Ticketverwaltung (neu: eigene Seite /tickets)

- Volle CRUD: Liste mit Suche, Bearbeiten (Typ/Module/Laufzeit/Lifetime,
  wirkt nur auf die naechste Ausstellung), Loeschen (nur wenn keine
  offene Lizenz mehr besteht, dann inkl. Historie).
- Dashboard (/) zeigt seitdem NICHT mehr die Ticket-Tabelle, sondern eine
  rein informative Kachel-Uebersicht der aktuell AKTIVEN Lizenzen, exakt
  im Kachel-Stil von TESMs eigenem Geraete-Dashboard: Ampelfarben (gruen
  >90 Tage, orange 30-90, rot <30/abgelaufen), gruppiert, Firma + Hostname
  der Kundeninstanz. Hostname kommt neu vom TESM-Client per Activate/
  Heartbeat (srv/tesm/licensing.py, abwaerts-kompatibel: alte Clients ohne
  das Feld ueberschreiben nie einen bereits bekannten Hostnamen).

## Kundenstammdaten

- Neue Felder: Strasse/Hausnummer/PLZ/Ort/Ansprechpartner. E-Mail ist beim
  Anlegen jetzt Pflicht; beim Bearbeiten bewusst nur validiert, wenn
  ausgefuellt (sonst waeren Bestandskunden aus der Zeit vor dieser
  Umstellung fuer JEDE Aenderung blockiert gewesen).

## Kundenportal (neu, per Magic-Link, kein Login)

- /portal/<token>: zeigt einem Kunden ALLE seine Tickets/Lizenzen auf
  einen Blick (ergaenzt den bestehenden Pro-Ticket-Link
  /self-service/<ticket_id>) und erlaubt ihm, seine eigenen Kontakt-/
  Anschriftdaten selbst zu pflegen -- Firmenname und kommerzielle
  Ticket-Eckdaten bleiben bewusst admin-verwaltet. token = eigene Spalte
  license_customers.portal_token, rueckwirkend fuer Bestandskunden erzeugt.

## Lifetime-Lizenzen

- Neues Ticket-Flag 'Lifetime': ignoriert die Laufzeit fuer jede REGULAERE
  Ausstellung (Sentinel-Ablaufdatum 2099-12-31, licensing.
  LIFETIME_EXPIRES_AT) -- eine zusaetzlich erzeugte Trial-Lizenz aus
  demselben Ticket bleibt immer die normale 30-Tage-Variante. Trial ALS
  TICKET-GRUNDTYP + Lifetime wird serverseitig verhindert (haette eine
  dauerhafte, nie ablaufende Lizenz ganz ohne Module ergeben).

## Absicherung nach Review

Ein Adversarial-Review-Durchlauf ueber den kompletten Diff deckte vor dem
Deploy zusaetzlich auf und wurde behoben:
- TOCTOU-Race: zwei nahezu gleichzeitige 'Lizenz erstellen'-Anfragen fuer
  dasselbe Ticket (z.B. Doppelklick im Self-Service) konnten beide
  durchkommen. Jetzt durch einen partiellen Unique-Index
  (idx_one_open_license_per_ticket) auf DB-Ebene ausgeschlossen; die
  zweite Anfrage bekommt sauber die bereits erstellte Lizenz zurueck statt
  eine zweite zu erzeugen.
- Analoger Unique-Index auf license_customers.portal_token als Backstop
  fuer den neuen Magic-Link.
- tickets.html nutzte noch die alte, feste '<=30 Tage'-Schwelle statt der
  gemeinsamen Ampelfarbe -- Lizenzen im 30-90-Tage-Bereich blieben dort
  unmarkiert, obwohl das Dashboard sie schon als orange zeigte.

SCHEMA_VERSION 2 -> 3 (neue Spalten/Tabellen, ueber install.sh mit
Backup+Health-Check abgesichert).

Verifiziert: vollstaendige Migration gegen eine echte Kopie der
Live-Datenbank, anschliessend 22 Funktionstests direkt gegen die echten
Routen (Validierung, Lifetime-Ausstellung, Sperr-/Kaskadenregeln bei
Loeschungen, Kundenportal inkl. ungueltiger Eingaben) sowie ein Playwright-
Seitenladetest ohne Konsolenfehler -- beides nach den Review-Fixes erneut
komplett durchlaufen, inkl. direktem Nachweis, dass der neue Unique-Index
eine doppelte offene Lizenz pro Ticket tatsaechlich auf DB-Ebene ablehnt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.2.0
2026-08-23 15:51:20 +02:00
alientimandClaude Sonnet 5 7e39320c87 Ticket-System fuer Kunden-Self-Service + Fingerprint-Auto-Supersede (v1.1.0)
Konzept: Kunde kauft eine Lizenz -> Admin legt einen Kunden an (oder nutzt
einen bestehenden) und weist ihm ein TICKET fuer eine Lizenz mit festen
Eckdaten (Typ/Module/Laufzeit) zu -- das Ticket ist die dauerhafte,
dem Kunden gehoerende Berechtigung, unabhaengig von der konkreten
signierten Lizenzdatei darunter. Admin ODER Kunde (per Self-Service-Link,
kein Login noetig) koennen aus einem offenen Ticket jederzeit eine Lizenz
erstellen, widerrufen und erneut erstellen -- ein Kunde kann mehrere
Tickets haben (z.B. mehrere Systeme).

Neu:
- license_tickets-Tabelle (id = unerratbares Token, customer_id, Typ/
  Module/Laufzeit, status 'open'/'blocked'), licenses.ticket_id-Spalte.
  Rueckwirkende Migration erzeugt fuer jede bereits VOR diesem Feature
  direkt ausgestellte Lizenz automatisch ein passendes Ticket (sonst
  waeren z.B. POETESTs bereits aktivierte Produktivlizenz oder die
  Demo-Lizenzen im neuen, rein ticket-basierten Dashboard verschwunden) --
  gegen eine echte Kopie der Live-Datenbank getestet.
- /tickets/new (Ticket anlegen, admin), /tickets/<id> (Details, Lizenz
  aus Ticket erstellen -- reguraer laut Ticket-Typ ODER als Trial mit
  vollem Modulumfang unabhaengig vom Ticket-Typ --, Ticket sperren/
  entsperren).
- /self-service/<ticket_id>: oeffentlich erreichbar (kein Login), Zugriff
  ueber Besitz des unerratbaren Ticket-Tokens (192 Bit Zufall) als 'Magic
  Link' -- Kunde kann eigene Lizenz herunterladen, widerrufen und (mit den
  Ticket-Eckdaten) neu erstellen. Link wird auf der Ticket-Seite angezeigt
  und in die 'Lizenz per E-Mail senden'-Nachricht aufgenommen.
- /licenses/<id>/delete (admin): raeumt widerrufene oder nie aktivierte
  Lizenzen auf, OHNE das zugehoerige Ticket anzutasten -- der Kunde (oder
  Admin) kann sich ueber das Ticket jederzeit eine neue mit denselben
  Eckdaten erzeugen.
- Bugfix: wird fuer einen Fingerprint eine neue Lizenz aktiviert (z.B.
  Trial durch Enterprise ersetzt), werden jetzt automatisch alle ANDEREN
  fuer denselben Fingerprint noch 'active' vermerkten Lizenzen auf
  'deactivated' gesetzt -- vorher blieben beide faelschlich gleichzeitig
  aktiv in der Uebersicht stehen. Bewusst nach Fingerprint gefiltert (ein
  Kunde darf mehrere Systeme mit je eigener Lizenz betreiben).
- Dashboard zeigt jetzt Tickets statt roher Lizenzzeilen (inkl. aktueller
  Lizenz je Ticket), Kunden-Seite zeigt Ticket-Anzahl statt Lizenz-Anzahl
  mit Filter-Link aufs Dashboard.

SCHEMA_VERSION 1 -> 2 (neue Tabelle + Spalte, ueber install.sh-
Sicherheitsnetz mit Backup+Health-Check abgesichert).

Verifiziert: Migration gegen eine echte Kopie der Live-Datenbank
(POETESTs aktive Produktivlizenz + 3 Demo-Lizenzen) durchlaufen lassen --
alle 8 bestehenden Lizenzen haben danach ein Ticket, keine verwaist.
Anschliessend alle neuen Seiten (Dashboard, Ticket-Anlage, Ticket-Detail,
Self-Service) per Playwright gegen diese migrierte Kopie auf einem
isolierten Scratch-Port durchgeklickt, keine Fehler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.1.0
2026-08-23 11:41:51 +02:00
alientimandClaude Sonnet 5 b280cbf121 Version 1.0.1: update.sh-Fix fuer 'latest'-Aufloesung
Kein App-Code geaendert -- reiner Versions-/Release-Bump, damit 'latest'
auch als heruntergeladenes Paket das bereits korrigierte update.sh
enthaelt (siehe vorheriger Commit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.0.1
2026-08-23 10:50:48 +02:00
alientimandClaude Sonnet 5 5fa60ec3ef update.sh: 'latest' zuverlaessig ueber die Gitea-API aufloesen
Identischer Bug/Fix wie im TESM-Hauptrepo (siehe dortiger Commit fuer
Details): /releases/download/latest/tesm-license-latest.tar.gz liefert
HTTP 200, aber Giteas automatisch generiertes Quellcode-Archiv des
aktuellen Default-Branch-Stands statt des echten Release-Assets.
'latest' wird jetzt zuerst per Gitea-API auf den echten Tag aufgeloest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 10:48:27 +02:00
alientimandClaude Sonnet 5 a9fc56ff47 Eigenstaendiges Repo: install.sh/update.sh auf tesm-license umgestellt, README + .gitignore
Ausgelagert aus dem TESM-Hauptrepo (srv/tesm-license/, install-license.sh,
update-license.sh, zugehoerige systemd-/nginx-Vorlagen) per
git-filter-repo, komplette bisherige Historie erhalten. install-license.sh
und update-license.sh wurden zu install.sh/update.sh (jetzt die einzigen
Skripte in diesem Repo, keine Namenskollision mit TESM mehr noetig).
update.sh zeigt jetzt auf dieses eigene Repo/Release-Paket
(tesm-license-vX.Y.Z.tar.gz) statt auf das gemeinsame TESM-Paket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.0.0
2026-08-23 10:34:53 +02:00
alientimandClaude Sonnet 5 3f5d85c7ee Lizenzserver-Deployment: install-license.sh/update-license.sh + systemd/nginx-Vorlagen
Fork von install.sh/update.sh/tesm.service/nginx-Site (TESM selbst),
beschraenkt auf /srv/tesm-license -- deployt ausschliesslich den in Phase 2
gebauten Master-Lizenzserver, laesst eine evtl. auf demselben Host
vorhandene TESM-Installation komplett unberuehrt (eigener Pfad-/Env-Var-
Namespace, siehe app.py). Gleiches Backup+Health-Check+Rueckroll-
Sicherheitsnetz wie bei TESMs eigenem Installer.

Teil von Phase 3 (Infrastruktur), siehe Plan toasty-twirling-hickey.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 10:15:02 +02:00
alientimandClaude Sonnet 5 83eb0be3b1 Master-Lizenzserver Phase 2: neue Schwester-Anwendung srv/tesm-license (v1.0.0)
Fork von TESM (srv/tesm), auf Kunden-/Lizenzverwaltung reduziert statt
Geraete-/PoE-Management. Wiederverwendet unveraendert: Login/Session/
Benutzer- und Gruppenverwaltung, LDAP/AD, NGINX- und Systemeinstellungen,
Live-Log/Verlauf/Auditlog, Im-/Export-Grundgeruest, das komplette
PERMISSIONS/NAV_ITEMS/inject_nav()-Rechtesystem sowie das in Phase 1
gebaute Lizenzsystem selbst sowohl fuer die MASTER-eigene Bootstrap-Lizenz
als auch fuer das exakt gleiche licensing.py (byte-identisch zu TESM --
Signieren/Verifizieren muss zwischen beiden Seiten kompatibel bleiben).

Entfernt: Clients/Switche/Zugangsdaten/DHCP/Fileshare/Wartung/Papierkorb/
manueller PoE-Neustart/SSH-Terminal inkl. aller zugehoerigen Tabellen,
Routen, Permissions, Nav-Eintraege und Abhaengigkeiten (paramiko/Flask-Sock/
simple-websocket/PyNaCl/pyasn1 aus requirements.txt). Benutzer-/Gruppen-
Loeschung von Soft- auf Hard-Delete umgestellt (kein Papierkorb mehr).
Eigener Pfad-/Env-Var-Namespace (TESM_LICENSE_* statt TESM_*, /var/log/
tesm-license statt /var/log/tesm usw.), damit Master und TESM testweise
sogar auf demselben Host nebeneinander laufen koennen, ohne sich Log-/
Config-Pfade streitig zu machen.

Neu -- der eigentliche Lizenzserver:
- license_customers/licenses-Tabellen, Master-Signaturschluessel
  (master_signing_key.json, einmalig erzeugt, NIE automatisch rotiert --
  jede Kundenlizenz traegt den zum Ausstellungszeitpunkt aktuellen
  master_pubkey fest eingebettet).
- Dashboard ('/') als Kunden-/Lizenzuebersicht (Typ, Module, Ablauf,
  Status, letzter Heartbeat), eigene Kunden-Verwaltungsseite.
- Lizenz-Ausstellung (Typ/Module/Laufzeit -> signierte Datei via
  licensing.issue_license), Detailseite, Download, Widerruf.
- /api/activate, /api/deactivate, /api/heartbeat (unauthentifiziert per
  Design -- die Signatur der Anfrage IST der Berechtigungsnachweis) sowie
  eine manuelle Offline-Code-Seite, die dieselben drei Verarbeitungs-
  funktionen (_process_activate/_process_deactivate/_process_heartbeat)
  nutzt wie die Online-API -- ein Protokoll, zwei Transportwege.
- E-Mail-Versand ausgestellter Lizenzen per Microsoft Graph
  (Client-Credentials-Flow, reine Standardbibliothek/urllib, keine neue
  Abhaengigkeit) inkl. Einrichtungsanleitung und Verbindungstest.
- create_master_license.py: lokales Bootstrap-/Erneuerungs-Skript fuer
  die eigene Lizenz des Masters (kein externer Super-Master noetig).

Verifiziert auf dem Testsystem (192.168.82.51): App laeuft parallel zu der
dort laufenden echten TESM-Instanz (Port 5001 vs. 80/5000, eigene
Log-/DB-Pfade, TESM unangetastet). Kompletter ECHTER End-to-End-Rundlauf
per Playwright durchgespielt -- kein simulierter Gegenpart: Kunde anlegen
-> Custom-Lizenz (dhcp+fileshare) ausstellen -> herunterladen -> auf der
echten TESM-Instanz hochladen -> ECHTE Online-Aktivierung (Master
verzeichnet Fingerprint, TESM zeigt 'Aktiviert') -> ECHTE Online-
Deaktivierung (TESM zeigt 'lizenzlos, Export bleibt moeglich', Master
zeigt Status 'Deaktiviert'). Lokale Syntax-/Templatepruefung (py_compile,
pyflakes, jinja2-Parse aller Templates) sauber.

Bewusst NICHT nach main gemergt/getaggt/ausgerollt -- folgt zusammen mit
Phase 3 (Infrastruktur-Umzug + POETEST-Enterprise-Lizenz), siehe Plan
toasty-twirling-hickey.md (Phase 2 von 3).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 02:07:30 +02:00