Files
tesm/srv/tesm-license/create_master_license.py
T
alientimandClaude Sonnet 5 90e07aa310 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

106 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Erzeugt/erneuert die Bootstrap-Lizenz DIESES Lizenzservers selbst.
Läuft rein lokal (kein Netzwerk, kein externer Super-Master) -- der Master
prüft seine eigene Lizenz über denselben license_active()-Mechanismus wie
Kundeninstanzen (siehe app.py), nur ohne Heartbeat-Ziel: dieses Skript
setzt den Aktivierungsstatus direkt in der Datenbank, da es keine höhere
Instanz gibt, die eine Aktivierung bestätigen könnte.
Nutzt denselben Master-Signaturschlüssel, den der Server auch zum
Ausstellen von Kundenlizenzen verwendet (wird beim ersten Aufruf
automatisch erzeugt, falls noch nicht vorhanden) -- WICHTIG: dieser
Schlüssel wird beim erneuten Aufruf (Verlängerung) wiederverwendet, nie
neu erzeugt, siehe _load_or_create_master_signing_key() weiter unten und
die identische Funktion in app.py: ein Schlüsselwechsel würde die
Offline-Verifikation künftiger Aktivierungs-/Heartbeat-Antworten für ALLE
bereits an Kunden ausgegebenen Lizenzen brechen.
Aufruf:
python3 create_master_license.py [--days 730] [--name "Firma XY"]
Erneuerung (z.B. kurz vor Ablauf): einfach erneut aufrufen -- überschreibt
die bestehende Bootstrap-Lizenz, der Signaturschlüssel bleibt unverändert.
"""
import argparse
import json
import os
import sqlite3
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import licensing
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MASTER_SIGNING_KEY_PATH = os.environ.get("TESM_LICENSE_MASTER_KEY", os.path.join(BASE_DIR, "master_signing_key.json"))
LICENSE_PATH = os.environ.get("TESM_LICENSE_PATH", os.path.join(BASE_DIR, "license.json"))
DB_PATH = os.environ.get("TESM_LICENSE_DB_PATH", os.path.join(BASE_DIR, "sqlite.db"))
def _load_or_create_master_signing_key():
try:
with open(MASTER_SIGNING_KEY_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
return data["private"], data["public"], False
except (OSError, ValueError, KeyError):
priv, pub = licensing.generate_keypair()
with open(MASTER_SIGNING_KEY_PATH, "w", encoding="utf-8") as f:
json.dump({"private": priv, "public": pub}, f)
try:
os.chmod(MASTER_SIGNING_KEY_PATH, 0o600)
except OSError:
pass
return priv, pub, True
def main():
parser = argparse.ArgumentParser(description="Erzeugt/erneuert die Bootstrap-Lizenz dieses Lizenzservers.")
parser.add_argument("--days", type=int, default=730, help="Gültigkeitsdauer in Tagen (Standard: 730 = 2 Jahre)")
parser.add_argument("--name", default="Lizenzserver (Eigenbetrieb)", help="Anzeigename in der Lizenzdatei")
args = parser.parse_args()
master_priv, master_pub, created = _load_or_create_master_signing_key()
if created:
print(f"Neuer Master-Signaturschlüssel erzeugt: {MASTER_SIGNING_KEY_PATH}")
else:
print(f"Bestehenden Master-Signaturschlüssel wiederverwendet: {MASTER_SIGNING_KEY_PATH}")
license_file, _pub = licensing.issue_license(
customer={"name": args.name, "contact_email": ""},
license_type="enterprise",
modules=[],
valid_days=args.days,
master_private_key_b64=master_priv,
master_public_key_b64=master_pub,
master_endpoint="",
vendor={"name": "", "phone": "", "email": "", "address": "", "logo_base64": ""},
)
with open(LICENSE_PATH, "w", encoding="utf-8") as f:
json.dump(license_file, f, indent=2)
try:
os.chmod(LICENSE_PATH, 0o600)
except OSError:
pass
print(f"Bootstrap-Lizenz geschrieben: {LICENSE_PATH} (gültig {args.days} Tage, bis {license_file['expires_at']})")
if os.path.exists(DB_PATH):
conn = sqlite3.connect(DB_PATH)
conn.execute(
"INSERT INTO settings (key, value) VALUES ('license_activation_status', 'active') "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value"
)
conn.commit()
conn.close()
print("Aktivierungsstatus in der Datenbank auf 'active' gesetzt.")
else:
print(
f"WARNUNG: Datenbank {DB_PATH} nicht gefunden -- bitte zuerst create_db.py ausführen, "
"dann dieses Skript erneut aufrufen, damit der Aktivierungsstatus gesetzt werden kann."
)
print("Fertig -- Dienst neu starten (oder warten, bis der Prozess die Lizenz beim nächsten Zugriff neu lädt).")
if __name__ == "__main__":
main()