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>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Initialisiert die SQLite-Datenbank für den TESM-Lizenzserver (Master).
|
||||
|
||||
WICHTIG: Wer hier eine neue Tabelle oder Spalte hinzufügt, muss auch die
|
||||
Datei SCHEMA_VERSION (daneben, im selben Verzeichnis) um 1 erhöhen.
|
||||
update.sh vergleicht diese Zahl zwischen der aktuell installierten und der
|
||||
neuen Version, um zu entscheiden, ob ein Update in-place laufen darf
|
||||
(bestehende Datenbank bleibt erhalten) oder eine frische Installation
|
||||
nötig ist (Datenbank wird beiseitegesichert). Ein vergessenes Hochzählen
|
||||
würde dazu führen, dass ein Host mit altem Schema faelschlich per
|
||||
In-Place-Update aktualisiert wird, statt frisch installiert zu werden --
|
||||
_ensure_schema() faengt die meisten Faelle zwar zur Laufzeit ab, ist aber
|
||||
kein Ersatz fuer eine korrekte Versionsnummer hier.
|
||||
|
||||
Gegenüber TESM entfernt: credentials/switches/devices/dhcp_*/
|
||||
ldap_fileshare_mappings (kein Geräte-/DHCP-/Fileshare-Management auf dem
|
||||
Lizenzserver). Neu: license_customers/licenses (Kern des Lizenzservers,
|
||||
siehe licensing.py für das zugehörige Kryptographie-/Protokollformat)."""
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DB_PATH = os.environ.get("TESM_LICENSE_DB_PATH", os.path.join(BASE_DIR, "sqlite.db"))
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
is_admin INTEGER DEFAULT 0,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
avatar_filename TEXT,
|
||||
auth_source TEXT NOT NULL DEFAULT 'local',
|
||||
email TEXT,
|
||||
is_locked INTEGER NOT NULL DEFAULT 0,
|
||||
deleted_at TEXT
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
is_system INTEGER DEFAULT 0,
|
||||
deleted_at TEXT
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS group_permissions (
|
||||
group_id INTEGER NOT NULL,
|
||||
permission TEXT NOT NULL,
|
||||
PRIMARY KEY (group_id, permission),
|
||||
FOREIGN KEY (group_id) REFERENCES groups(id)
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
user_id INTEGER NOT NULL,
|
||||
group_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, group_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (group_id) REFERENCES groups(id)
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS service_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
purpose TEXT UNIQUE NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ldap_group_mappings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ad_group_dn TEXT UNIQUE NOT NULL,
|
||||
ad_group_name TEXT NOT NULL,
|
||||
app_group_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (app_group_id) REFERENCES groups(id)
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
target TEXT,
|
||||
details TEXT
|
||||
);
|
||||
""")
|
||||
|
||||
# --- Kern des Lizenzservers ---------------------------------------------
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS license_customers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
contact_email TEXT,
|
||||
contact_phone TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS licenses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
license_id TEXT UNIQUE NOT NULL,
|
||||
customer_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
modules TEXT NOT NULL DEFAULT '[]',
|
||||
issued_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
license_pubkey TEXT NOT NULL,
|
||||
license_file_json TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'issued',
|
||||
fingerprint TEXT,
|
||||
activated_at TEXT,
|
||||
deactivated_at TEXT,
|
||||
last_heartbeat_at TEXT,
|
||||
revoked_at TEXT,
|
||||
role TEXT NOT NULL DEFAULT 'customer',
|
||||
created_by TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (customer_id) REFERENCES license_customers(id)
|
||||
);
|
||||
""")
|
||||
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("interval", "5"))
|
||||
|
||||
cur = c.execute("INSERT OR IGNORE INTO groups (name, is_default, is_system) VALUES (?, 1, 1)", ("Benutzer",))
|
||||
if cur.rowcount > 0:
|
||||
default_group_id = cur.lastrowid
|
||||
c.executemany(
|
||||
"INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?, ?)",
|
||||
[
|
||||
(default_group_id, "logs_group.view"),
|
||||
(default_group_id, "logs_live.view"),
|
||||
],
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"Datenbank '{DB_PATH}' wurde initialisiert inklusive Settings.")
|
||||
Reference in New Issue
Block a user