|
|
|
@@ -212,6 +212,10 @@ PERMISSIONS = {
|
|
|
|
|
"label": "Kunden",
|
|
|
|
|
"rows": {"view": "customers.view", "create": "customers.create", "edit": "customers.edit"},
|
|
|
|
|
},
|
|
|
|
|
"tickets": {
|
|
|
|
|
"label": "Tickets",
|
|
|
|
|
"rows": {"view": "tickets.view", "create": "tickets.create", "edit": "tickets.edit"},
|
|
|
|
|
},
|
|
|
|
|
"licenses": {
|
|
|
|
|
"label": "Lizenzen",
|
|
|
|
|
"rows": {"view": "licenses.view", "create": "licenses.create", "edit": "licenses.edit"},
|
|
|
|
@@ -251,6 +255,10 @@ PERMISSIONS = {
|
|
|
|
|
"label": "Systemeinstellungen",
|
|
|
|
|
"rows": {"view": "settings_system.view", "edit": "settings_system.edit"},
|
|
|
|
|
},
|
|
|
|
|
"settings_license": {
|
|
|
|
|
"label": "Lizenz",
|
|
|
|
|
"rows": {"view": "settings_license.view", "edit": "settings_license.edit"},
|
|
|
|
|
},
|
|
|
|
|
"settings_importexport": {
|
|
|
|
|
"label": "Im-/Export",
|
|
|
|
|
"rows": {
|
|
|
|
@@ -275,6 +283,8 @@ PERMISSION_LABELS = {
|
|
|
|
|
"licensing_group.view": "Lizenzierung-Bereich anzeigen",
|
|
|
|
|
"customers.view": "Kunden lesen", "customers.create": "Kunden anlegen",
|
|
|
|
|
"customers.edit": "Kunden ändern (inkl. Löschen)",
|
|
|
|
|
"tickets.view": "Tickets lesen", "tickets.create": "Tickets anlegen",
|
|
|
|
|
"tickets.edit": "Tickets ändern (Eckdaten, Sperren/Entsperren, Löschen)",
|
|
|
|
|
"licenses.view": "Lizenzen und Aktivierungsstatus lesen",
|
|
|
|
|
"licenses.create": "Lizenzen ausstellen",
|
|
|
|
|
"licenses.edit": "Lizenzen ändern (Deaktivieren/Widerrufen, erneut versenden)",
|
|
|
|
@@ -288,6 +298,8 @@ PERMISSION_LABELS = {
|
|
|
|
|
"groups.view": "Gruppen lesen", "groups.create": "Gruppen anlegen",
|
|
|
|
|
"groups.edit": "Gruppen ändern (inkl. Löschen)",
|
|
|
|
|
"settings_system.view": "Systemeinstellungen lesen", "settings_system.edit": "Systemeinstellungen ändern",
|
|
|
|
|
"settings_license.view": "Lizenzstatus ansehen",
|
|
|
|
|
"settings_license.edit": "Lizenzdatei hochladen sowie Lizenz aktivieren/deaktivieren (online oder per Code)",
|
|
|
|
|
"settings_importexport.view": "Im-/Export-Seite ansehen",
|
|
|
|
|
"settings_importexport.edit": "Daten importieren",
|
|
|
|
|
"settings_importexport.export": "Daten exportieren (Export-Datei enthält Passwörter im Klartext)",
|
|
|
|
@@ -349,7 +361,7 @@ def _nav_key_visible(key, user):
|
|
|
|
|
if key == "index":
|
|
|
|
|
return True
|
|
|
|
|
if key == "tickets":
|
|
|
|
|
return user.has_permission("licenses.view")
|
|
|
|
|
return user.has_permission("tickets.view")
|
|
|
|
|
if key == "licenses":
|
|
|
|
|
return user.has_permission("licenses.view")
|
|
|
|
|
if key == "customers":
|
|
|
|
@@ -361,10 +373,11 @@ def _nav_key_visible(key, user):
|
|
|
|
|
if key == "settings_system":
|
|
|
|
|
return user.can_view_settings_system
|
|
|
|
|
if key == "settings_license":
|
|
|
|
|
# Immer erreichbar für jeden mit Systemeinstellungen-Leserecht --
|
|
|
|
|
# muss auch OHNE Lizenz sichtbar bleiben, sonst könnte niemand
|
|
|
|
|
# jemals eine erste Lizenz hochladen/aktivieren.
|
|
|
|
|
return user.can_view_settings_system
|
|
|
|
|
# Eigenes Recht (settings_license.view), unabhängig von
|
|
|
|
|
# settings_system -- muss auch OHNE eine aktive Lizenz sichtbar
|
|
|
|
|
# bleiben (das ist eine andere Prüfung, siehe license_active()),
|
|
|
|
|
# sonst könnte niemand jemals eine erste Lizenz hochladen/aktivieren.
|
|
|
|
|
return user.can_view_settings_license
|
|
|
|
|
if key == "settings_importexport":
|
|
|
|
|
return user.can_view_settings_importexport
|
|
|
|
|
if key == "settings_ldap":
|
|
|
|
@@ -537,6 +550,12 @@ class User(UserMixin):
|
|
|
|
|
["customers.view", "customers.create", "customers.edit"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def can_manage_tickets(self):
|
|
|
|
|
return self.has_any_permission(
|
|
|
|
|
["tickets.view", "tickets.create", "tickets.edit"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def can_manage_licenses(self):
|
|
|
|
|
return self.has_any_permission(
|
|
|
|
@@ -559,6 +578,10 @@ class User(UserMixin):
|
|
|
|
|
def can_view_settings_system(self):
|
|
|
|
|
return self.has_permission("settings_system.view")
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def can_view_settings_license(self):
|
|
|
|
|
return self.has_permission("settings_license.view")
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def can_view_settings_importexport(self):
|
|
|
|
|
return self.has_any_permission([
|
|
|
|
@@ -781,6 +804,7 @@ def _ensure_schema():
|
|
|
|
|
valid_days INTEGER NOT NULL,
|
|
|
|
|
lifetime INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
|
|
|
role TEXT NOT NULL DEFAULT 'customer',
|
|
|
|
|
created_by TEXT,
|
|
|
|
|
created_at TEXT NOT NULL,
|
|
|
|
|
FOREIGN KEY (customer_id) REFERENCES license_customers(id)
|
|
|
|
@@ -789,6 +813,13 @@ def _ensure_schema():
|
|
|
|
|
ticket_cols = {row["name"] for row in conn.execute("PRAGMA table_info(license_tickets)").fetchall()}
|
|
|
|
|
if "lifetime" not in ticket_cols:
|
|
|
|
|
conn.execute("ALTER TABLE license_tickets ADD COLUMN lifetime INTEGER NOT NULL DEFAULT 0")
|
|
|
|
|
if "role" not in ticket_cols:
|
|
|
|
|
# Rolle der aus diesem Ticket auszustellenden Lizenz (siehe
|
|
|
|
|
# licensing.LICENSE_ROLES) -- "customer" (Default) fuer gewoehnliche
|
|
|
|
|
# TESM-Kundeninstanzen, "license_server" fuer einen damit
|
|
|
|
|
# auszustellenden Sub-Lizenzserver. Bestehende Tickets bleiben
|
|
|
|
|
# unveraendert "customer".
|
|
|
|
|
conn.execute("ALTER TABLE license_tickets ADD COLUMN role TEXT NOT NULL DEFAULT 'customer'")
|
|
|
|
|
|
|
|
|
|
# DB-seitiger Schutz gegen die Doppel-Ausstellung eines Tickets: ohne
|
|
|
|
|
# dieses Backstop koennten zwei nahezu gleichzeitige Anfragen (z.B. ein
|
|
|
|
@@ -877,6 +908,64 @@ def _ensure_schema():
|
|
|
|
|
conn.execute("UPDATE licenses SET ticket_id=? WHERE license_id=?", (ticket_id, lic["license_id"]))
|
|
|
|
|
conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_tickets,))
|
|
|
|
|
|
|
|
|
|
_migration_key_license_perm = "_migrated_settings_license_permission_v1"
|
|
|
|
|
if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_license_perm,)).fetchone():
|
|
|
|
|
# "Lizenz" (settings_license) hatte bislang KEIN eigenes Recht --
|
|
|
|
|
# die Seite lief komplett über settings_system.view/.edit mit.
|
|
|
|
|
# Backfill: jede Gruppe, die settings_system.view/.edit bereits
|
|
|
|
|
# hatte, bekommt automatisch das neue, gespiegelte
|
|
|
|
|
# settings_license.view/.edit dazu -- ohne diesen Schritt würden
|
|
|
|
|
# bestehende Gruppen beim Update stillschweigend den Zugriff auf
|
|
|
|
|
# die Lizenzseite verlieren, den sie vorher implizit hatten.
|
|
|
|
|
_system_to_license = {
|
|
|
|
|
"settings_system.view": "settings_license.view",
|
|
|
|
|
"settings_system.edit": "settings_license.edit",
|
|
|
|
|
}
|
|
|
|
|
for system_perm, license_perm in _system_to_license.items():
|
|
|
|
|
rows = conn.execute(
|
|
|
|
|
"SELECT DISTINCT group_id FROM group_permissions WHERE permission=?", (system_perm,)
|
|
|
|
|
).fetchall()
|
|
|
|
|
for row in rows:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?, ?)",
|
|
|
|
|
(row["group_id"], license_perm),
|
|
|
|
|
)
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_license_perm,)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_migration_key_tickets_perm = "_migrated_tickets_permission_v1"
|
|
|
|
|
if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_tickets_perm,)).fetchone():
|
|
|
|
|
# "Tickets" hatte bislang KEIN eigenes Recht -- Ticketverwaltung
|
|
|
|
|
# (anlegen/bearbeiten/sperren/loeschen) lief komplett ueber
|
|
|
|
|
# licenses.view/.create/.edit mit, obwohl Tickets ein eigener,
|
|
|
|
|
# gleichberechtigter Navigationspunkt neben Kunden und Lizenzen
|
|
|
|
|
# ist (genau wie bei TESM Clients/Switche/Zugangsdaten trotz enger
|
|
|
|
|
# fachlicher Verwandtschaft jeweils eigene Rechte haben). Backfill:
|
|
|
|
|
# jede Gruppe mit licenses.view/.create/.edit bekommt automatisch
|
|
|
|
|
# das gespiegelte tickets.view/.create/.edit dazu -- das
|
|
|
|
|
# Ausstellen einer Lizenz AUS einem Ticket heraus bleibt bewusst
|
|
|
|
|
# weiterhin an licenses.edit gebunden (siehe ticket_detail()),
|
|
|
|
|
# nur Sperren/Entsperren/Bearbeiten/Loeschen des Tickets selbst
|
|
|
|
|
# wandert auf das neue, unabhaengige Recht.
|
|
|
|
|
_license_to_ticket = {
|
|
|
|
|
"licenses.view": "tickets.view",
|
|
|
|
|
"licenses.create": "tickets.create",
|
|
|
|
|
"licenses.edit": "tickets.edit",
|
|
|
|
|
}
|
|
|
|
|
for license_perm, ticket_perm in _license_to_ticket.items():
|
|
|
|
|
rows = conn.execute(
|
|
|
|
|
"SELECT DISTINCT group_id FROM group_permissions WHERE permission=?", (license_perm,)
|
|
|
|
|
).fetchall()
|
|
|
|
|
for row in rows:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?, ?)",
|
|
|
|
|
(row["group_id"], ticket_perm),
|
|
|
|
|
)
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_tickets_perm,)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_migration_key_old_logs = "_cleaned_up_legacy_rpi_log_files_v1"
|
|
|
|
|
if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_old_logs,)).fetchone():
|
|
|
|
|
for stale_log in glob.glob("/var/log/rpi-*.log"):
|
|
|
|
@@ -1806,6 +1895,24 @@ def module_licensed(module_key):
|
|
|
|
|
return license_active() and module_key in _license_state["status"]["modules"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def can_issue_license_server_licenses():
|
|
|
|
|
"""STRUKTURELLE Sperre (siehe licensing.LICENSE_ROLES-Kommentar), keine
|
|
|
|
|
vergebbare Permission: True nur, wenn die EIGENE, gerade aktive Lizenz
|
|
|
|
|
dieser Instanz role=='master' ist -- also ausschließlich auf der
|
|
|
|
|
tatsächlichen Wurzel-Instanz (deren Lizenz per create_master_license.py
|
|
|
|
|
lokal erzeugt wurde), niemals auf einem per role='license_server'
|
|
|
|
|
lizenzierten Sub-Lizenzserver. Ein Sub-Lizenzserver liefert hier immer
|
|
|
|
|
False zurück, unabhängig davon, welche Gruppenrechte seine lokalen
|
|
|
|
|
Admins dort einrichten -- diese Sperre lässt sich also NICHT durch
|
|
|
|
|
lokale Rechtevergabe aufheben. Aufrufer müssen zusätzlich
|
|
|
|
|
current_user.is_admin selbst prüfen (siehe ticket_new()) -- diese
|
|
|
|
|
Funktion prüft nur die Instanz-Rolle, keine Benutzerrechte."""
|
|
|
|
|
return bool(
|
|
|
|
|
license_active()
|
|
|
|
|
and _license_state["status"]["role"] == "master"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_module(module_key):
|
|
|
|
|
"""Serverseitiger Guard für die drei komplett ausgeblendeten Vollmodule
|
|
|
|
|
(dhcp/fileshare/maintenance) -- ein direkter URL-Aufruf ohne Lizenz
|
|
|
|
@@ -1977,6 +2084,23 @@ def _get_master_endpoint():
|
|
|
|
|
return get_setting("master_endpoint", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _customer_facing_url(endpoint, **values):
|
|
|
|
|
"""Baut eine externe URL fuer an KUNDEN gerichtete Magic-Links (Self-
|
|
|
|
|
Service, Kundenportal-Anfrage/-Zugang, Lizenz-per-E-Mail) auf Basis
|
|
|
|
|
des admin-gepflegten Lizenzserver-Endpunkts (siehe _get_master_endpoint(),
|
|
|
|
|
Einstellungen -> Lizenz -> Anbieter & Lizenzserver) -- NICHT per
|
|
|
|
|
url_for(..., _external=True), das stattdessen den Host-Header der
|
|
|
|
|
GERADE laufenden Anfrage verwendet und damit z.B. die interne IP
|
|
|
|
|
zeigt, ueber die zufaellig der Admin gerade eingeloggt ist, statt der
|
|
|
|
|
fuer Kunden eigentlich gedachten oeffentlichen Domain. Faellt auf die
|
|
|
|
|
normale request-basierte externe URL zurueck, wenn der Endpunkt noch
|
|
|
|
|
nicht konfiguriert ist (z.B. auf einer frischen Installation)."""
|
|
|
|
|
base = _get_master_endpoint().rstrip("/")
|
|
|
|
|
if not base:
|
|
|
|
|
return url_for(endpoint, _external=True, **values)
|
|
|
|
|
return base + url_for(endpoint, **values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _customer_row(conn, customer_id):
|
|
|
|
|
return conn.execute("SELECT * FROM license_customers WHERE id=?", (customer_id,)).fetchone()
|
|
|
|
|
|
|
|
|
@@ -2062,7 +2186,7 @@ def _ticket_has_open_license(conn, ticket_id):
|
|
|
|
|
return bool(current and current["status"] in ("issued", "active"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_ticket(conn, customer_id, license_type, modules, valid_days, actor, lifetime=False):
|
|
|
|
|
def _create_ticket(conn, customer_id, license_type, modules, valid_days, actor, lifetime=False, role="customer"):
|
|
|
|
|
"""Legt ein neues Ticket an -- die dauerhafte, dem Kunden gehoerende
|
|
|
|
|
Berechtigung auf eine Lizenz mit diesen Eckdaten. Erzeugt für sich
|
|
|
|
|
genommen noch KEINE Lizenzdatei, siehe _issue_license_from_ticket.
|
|
|
|
@@ -2070,20 +2194,25 @@ def _create_ticket(conn, customer_id, license_type, modules, valid_days, actor,
|
|
|
|
|
aus diesem Ticket (Trial-Ausstellungen bleiben davon unberuehrt, siehe
|
|
|
|
|
_issue_license_from_ticket) -- der Wert wird trotzdem gespeichert,
|
|
|
|
|
damit ein spaeteres Zurückschalten auf eine normale Laufzeit nicht bei
|
|
|
|
|
0/NULL anfangen muss."""
|
|
|
|
|
0/NULL anfangen muss. role (siehe licensing.LICENSE_ROLES) bestimmt,
|
|
|
|
|
welche Rolle eine aus diesem Ticket ausgestellte Lizenz traegt --
|
|
|
|
|
Aufrufer MUESSEN role='license_server' bereits selbst gegen
|
|
|
|
|
can_issue_license_server_licenses() + current_user.is_admin geprueft
|
|
|
|
|
haben (siehe ticket_new()), diese Funktion vertraut dem Aufrufer."""
|
|
|
|
|
ticket_id = secrets.token_urlsafe(24)
|
|
|
|
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO license_tickets (id, customer_id, type, modules, valid_days, lifetime, status, created_by, created_at) "
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, 'open', ?, ?)",
|
|
|
|
|
(ticket_id, customer_id, license_type, json.dumps(sorted(modules)), valid_days, int(bool(lifetime)), actor, now),
|
|
|
|
|
"INSERT INTO license_tickets (id, customer_id, type, modules, valid_days, lifetime, role, status, created_by, created_at) "
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)",
|
|
|
|
|
(ticket_id, customer_id, license_type, json.dumps(sorted(modules)), valid_days, int(bool(lifetime)), role, actor, now),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
customer = _customer_row(conn, customer_id)
|
|
|
|
|
duration_label = "Lifetime" if lifetime else f"{valid_days} Tage"
|
|
|
|
|
role_label = " -- LIZENZSERVER" if role == "license_server" else ""
|
|
|
|
|
log_action_system(
|
|
|
|
|
"ticket.create", customer["name"] if customer else str(customer_id),
|
|
|
|
|
f"{LICENSE_TYPE_LABELS.get(license_type, license_type)}, {duration_label} (Akteur: {actor})",
|
|
|
|
|
f"{LICENSE_TYPE_LABELS.get(license_type, license_type)}, {duration_label}{role_label} (Akteur: {actor})",
|
|
|
|
|
)
|
|
|
|
|
return ticket_id
|
|
|
|
|
|
|
|
|
@@ -2099,7 +2228,14 @@ def _issue_license_from_ticket(conn, ticket_id, actor, as_trial=False):
|
|
|
|
|
Laufzeit) bleibt dabei unverändert, nur DIESE eine Ausstellung nutzt
|
|
|
|
|
Trial-Konditionen."""
|
|
|
|
|
ticket = _ticket_row(conn, ticket_id)
|
|
|
|
|
ticket_role = ticket["role"] if "role" in ticket.keys() else "customer"
|
|
|
|
|
if as_trial:
|
|
|
|
|
if ticket_role != "customer":
|
|
|
|
|
# Trial ("30 Tage, alle TESM-Module") ist ein reines
|
|
|
|
|
# Kunden-Konzept -- ALL_MODULES (dhcp/fileshare/maintenance)
|
|
|
|
|
# ist fuer eine Lizenzserver-Lizenz bedeutungslos. Klarer
|
|
|
|
|
# Fehler statt einer stillschweigend falsch befuellten Lizenz.
|
|
|
|
|
raise ValueError("Trial-Ausstellung ist nur fuer Kunden-Tickets moeglich.")
|
|
|
|
|
# Trial ist immer ein zeitlich kurzer Override, unabhaengig davon
|
|
|
|
|
# ob das Ticket selbst als Lifetime konfiguriert ist -- siehe
|
|
|
|
|
# Modul-Docstring oben.
|
|
|
|
@@ -2128,17 +2264,18 @@ def _issue_license_from_ticket(conn, ticket_id, actor, as_trial=False):
|
|
|
|
|
master_endpoint=_get_master_endpoint(),
|
|
|
|
|
vendor=_get_vendor_info(),
|
|
|
|
|
lifetime=lifetime,
|
|
|
|
|
role=ticket_role,
|
|
|
|
|
)
|
|
|
|
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
try:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO licenses (license_id, customer_id, type, modules, issued_at, expires_at, "
|
|
|
|
|
"license_pubkey, license_file_json, status, role, ticket_id, lifetime, created_by, created_at) "
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'issued', 'customer', ?, ?, ?, ?)",
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'issued', ?, ?, ?, ?, ?)",
|
|
|
|
|
(
|
|
|
|
|
license_file["license_id"], ticket["customer_id"], license_type, json.dumps(sorted(modules)),
|
|
|
|
|
license_file["issued_at"], license_file["expires_at"], license_pubkey,
|
|
|
|
|
json.dumps(license_file), ticket_id, int(lifetime), actor, now,
|
|
|
|
|
json.dumps(license_file), ticket_role, ticket_id, int(lifetime), actor, now,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
@@ -2511,7 +2648,7 @@ def ticket_new():
|
|
|
|
|
Lizenz mit bestimmten Eckdaten. Erstellt bewusst noch KEINE Lizenz;
|
|
|
|
|
das übernimmt ein separater Schritt auf der Ticket-Detailseite (durch
|
|
|
|
|
Admin ODER Kunde per Self-Service)."""
|
|
|
|
|
if not current_user.has_permission("licenses.create"):
|
|
|
|
|
if not current_user.has_permission("tickets.create"):
|
|
|
|
|
flash("Keine Berechtigung, Tickets anzulegen.", "danger")
|
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
@@ -2544,12 +2681,31 @@ def ticket_new():
|
|
|
|
|
conn.close()
|
|
|
|
|
return redirect(url_for("ticket_new"))
|
|
|
|
|
|
|
|
|
|
# role='license_server' darf NUR ein echter Admin auf einer
|
|
|
|
|
# tatsaechlichen "master"-Instanz waehlen -- fest einprogrammiert,
|
|
|
|
|
# nicht ueber ein delegierbares Recht (siehe
|
|
|
|
|
# can_issue_license_server_licenses()-Docstring). Jeder andere
|
|
|
|
|
# Versuch (manipuliertes Formular, Sub-Lizenzserver, Nicht-Admin)
|
|
|
|
|
# wird still auf 'customer' zurueckgestuft statt nur die UI
|
|
|
|
|
# auszublenden -- Verteidigung in der Tiefe.
|
|
|
|
|
role = request.form.get("role", "customer")
|
|
|
|
|
if role == "license_server" and not (current_user.is_admin and can_issue_license_server_licenses()):
|
|
|
|
|
flash("Keine Berechtigung, Lizenzserver-Lizenzen auszustellen.", "danger")
|
|
|
|
|
role = "customer"
|
|
|
|
|
if role not in licensing.LICENSE_ROLES or role == "master":
|
|
|
|
|
role = "customer"
|
|
|
|
|
|
|
|
|
|
license_type = request.form.get("license_type", "standard")
|
|
|
|
|
if license_type not in licensing.LICENSE_TYPES:
|
|
|
|
|
flash("Ungültiger Lizenztyp.", "danger")
|
|
|
|
|
conn.close()
|
|
|
|
|
return redirect(url_for("ticket_new"))
|
|
|
|
|
if license_type == "enterprise":
|
|
|
|
|
if role != "customer":
|
|
|
|
|
# Module (dhcp/fileshare/maintenance) sind ein reines
|
|
|
|
|
# TESM-Client-Konzept -- fuer eine Lizenzserver-Lizenz
|
|
|
|
|
# bedeutungslos.
|
|
|
|
|
modules = []
|
|
|
|
|
elif license_type == "enterprise":
|
|
|
|
|
modules = list(licensing.ALL_MODULES)
|
|
|
|
|
elif license_type == "custom":
|
|
|
|
|
modules = [m for m in request.form.getlist("modules") if m in licensing.ALL_MODULES]
|
|
|
|
@@ -2565,7 +2721,10 @@ def ticket_new():
|
|
|
|
|
except ValueError:
|
|
|
|
|
valid_days = 365
|
|
|
|
|
|
|
|
|
|
ticket_id = _create_ticket(conn, customer_id, license_type, modules, valid_days, current_user.username, lifetime=lifetime)
|
|
|
|
|
ticket_id = _create_ticket(
|
|
|
|
|
conn, customer_id, license_type, modules, valid_days, current_user.username,
|
|
|
|
|
lifetime=lifetime, role=role,
|
|
|
|
|
)
|
|
|
|
|
conn.close()
|
|
|
|
|
flash("Ticket angelegt.", "success")
|
|
|
|
|
return redirect(url_for("ticket_detail", ticket_id=ticket_id))
|
|
|
|
@@ -2576,13 +2735,14 @@ def ticket_new():
|
|
|
|
|
"ticket_new.html", customers=customers_rows, license_types=licensing.LICENSE_TYPES,
|
|
|
|
|
all_modules=licensing.ALL_MODULES, module_labels=MODULE_LABELS, type_labels=LICENSE_TYPE_LABELS,
|
|
|
|
|
preselect_customer_id=request.args.get("customer_id", type=int),
|
|
|
|
|
can_issue_license_server=current_user.is_admin and can_issue_license_server_licenses(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/tickets/<ticket_id>", methods=["GET", "POST"])
|
|
|
|
|
@login_required
|
|
|
|
|
def ticket_detail(ticket_id):
|
|
|
|
|
if not current_user.can_manage_licenses:
|
|
|
|
|
if not current_user.can_manage_tickets:
|
|
|
|
|
flash("Keine Berechtigung.", "danger")
|
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
@@ -2593,10 +2753,14 @@ def ticket_detail(ticket_id):
|
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
if "issue_license" in request.form:
|
|
|
|
|
# Ausstellen erzeugt eine LIZENZ (nicht nur eine Ticket-Aenderung)
|
|
|
|
|
# -- bewusst weiterhin ueber das Lizenz-Recht gegated, nicht ueber
|
|
|
|
|
# tickets.edit, damit sich "Tickets verwalten" und "Lizenzen
|
|
|
|
|
# ausstellen" unabhaengig voneinander vergeben lassen.
|
|
|
|
|
if not current_user.has_permission("licenses.edit"):
|
|
|
|
|
flash("Keine Berechtigung.", "danger")
|
|
|
|
|
elif "issue_license" in request.form:
|
|
|
|
|
if not license_active():
|
|
|
|
|
flash("Keine Berechtigung, Lizenzen auszustellen.", "danger")
|
|
|
|
|
elif not license_active():
|
|
|
|
|
flash("Der Lizenzserver selbst hat keine gültige Lizenz -- Ausstellen ist deaktiviert.", "danger")
|
|
|
|
|
elif ticket["status"] != "open":
|
|
|
|
|
flash("Ticket ist gesperrt.", "danger")
|
|
|
|
@@ -2604,11 +2768,24 @@ def ticket_detail(ticket_id):
|
|
|
|
|
flash("Dieses Ticket hat bereits eine aktive oder ausgestellte Lizenz.", "danger")
|
|
|
|
|
else:
|
|
|
|
|
as_trial = request.form.get("issue_license") == "trial"
|
|
|
|
|
try:
|
|
|
|
|
license_file = _issue_license_from_ticket(conn, ticket_id, current_user.username, as_trial=as_trial)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
# z.B. Trial-Override auf einem Lizenzserver-Ticket
|
|
|
|
|
# versucht (siehe _issue_license_from_ticket) -- im
|
|
|
|
|
# normalen UI-Fluss nicht erreichbar (Button wird für
|
|
|
|
|
# role != 'customer' gar nicht erst angezeigt), aber
|
|
|
|
|
# eine manipulierte Anfrage soll eine klare Meldung statt
|
|
|
|
|
# eines internen Serverfehlers bekommen.
|
|
|
|
|
flash(str(exc), "danger")
|
|
|
|
|
else:
|
|
|
|
|
conn.close()
|
|
|
|
|
flash("Lizenz ausgestellt.", "success")
|
|
|
|
|
return redirect(url_for("license_detail", license_id=license_file["license_id"]))
|
|
|
|
|
elif "toggle_block" in request.form:
|
|
|
|
|
if not current_user.has_permission("tickets.edit"):
|
|
|
|
|
flash("Keine Berechtigung, Tickets zu sperren/entsperren.", "danger")
|
|
|
|
|
else:
|
|
|
|
|
new_status = "blocked" if ticket["status"] == "open" else "open"
|
|
|
|
|
conn.execute("UPDATE license_tickets SET status=? WHERE id=?", (new_status, ticket_id))
|
|
|
|
|
conn.commit()
|
|
|
|
@@ -2635,9 +2812,10 @@ def ticket_detail(ticket_id):
|
|
|
|
|
license_types=licensing.LICENSE_TYPES, all_modules=licensing.ALL_MODULES,
|
|
|
|
|
type_labels=LICENSE_TYPE_LABELS, module_labels=MODULE_LABELS,
|
|
|
|
|
master_licensed=license_active(),
|
|
|
|
|
self_service_url=url_for("self_service", ticket_id=ticket_id, _external=True),
|
|
|
|
|
portal_request_url=url_for("portal_request", _external=True),
|
|
|
|
|
can_edit=current_user.has_permission("licenses.edit"),
|
|
|
|
|
self_service_url=_customer_facing_url("self_service", ticket_id=ticket_id),
|
|
|
|
|
portal_request_url=_customer_facing_url("portal_request"),
|
|
|
|
|
can_edit_ticket=current_user.has_permission("tickets.edit"),
|
|
|
|
|
can_issue_license=current_user.has_permission("licenses.edit"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2653,7 +2831,7 @@ def ticket_edit(ticket_id):
|
|
|
|
|
hier bewusst nicht vorgesehen (das Self-Service-Ticket ist an genau
|
|
|
|
|
diesen Kunden gebunden, u.a. weil er ueber dessen Portal-Token
|
|
|
|
|
erreichbar ist)."""
|
|
|
|
|
if not current_user.has_permission("licenses.edit"):
|
|
|
|
|
if not current_user.has_permission("tickets.edit"):
|
|
|
|
|
flash("Keine Berechtigung, Tickets zu bearbeiten.", "danger")
|
|
|
|
|
return redirect(url_for("tickets_list"))
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
@@ -2706,7 +2884,7 @@ def ticket_delete(ticket_id):
|
|
|
|
|
System verschwinden). Etwaige bereits widerrufene/deaktivierte
|
|
|
|
|
Lizenzen darunter sind reine Historie und werden mitgeloescht, sonst
|
|
|
|
|
blieben verwaiste Zeilen mit einer toten ticket_id zurueck."""
|
|
|
|
|
if not current_user.has_permission("licenses.edit"):
|
|
|
|
|
if not current_user.has_permission("tickets.edit"):
|
|
|
|
|
flash("Keine Berechtigung, Tickets zu löschen.", "danger")
|
|
|
|
|
return redirect(url_for("tickets_list"))
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
@@ -2875,22 +3053,28 @@ def license_send_email(license_id):
|
|
|
|
|
flash("Für diesen Kunden ist keine E-Mail-Adresse hinterlegt.", "danger")
|
|
|
|
|
return redirect(url_for("license_detail", license_id=license_id))
|
|
|
|
|
self_service_note = ""
|
|
|
|
|
if row["role"] == "customer":
|
|
|
|
|
# Self-Service-Link und Kundenportal-Hinweis sind reine
|
|
|
|
|
# Kunden-Konzepte -- fuer eine Lizenzserver-Lizenz irrelevant (der
|
|
|
|
|
# Self-Service-Link wuerde dort ohnehin mit 404 abgewiesen, siehe
|
|
|
|
|
# self_service()).
|
|
|
|
|
if row["ticket_id"]:
|
|
|
|
|
self_service_url = url_for("self_service", ticket_id=row["ticket_id"], _external=True)
|
|
|
|
|
self_service_url = _customer_facing_url("self_service", ticket_id=row["ticket_id"])
|
|
|
|
|
self_service_note = (
|
|
|
|
|
f"\nÜber diesen Link können Sie Ihre Lizenz jederzeit selbst herunterladen, "
|
|
|
|
|
f"widerrufen oder (z.B. nach einem Systemwechsel) neu erstellen:\n{self_service_url}\n"
|
|
|
|
|
)
|
|
|
|
|
portal_request_url = url_for("portal_request", _external=True)
|
|
|
|
|
portal_request_url = _customer_facing_url("portal_request")
|
|
|
|
|
self_service_note += (
|
|
|
|
|
f"\nEine Übersicht all Ihrer Tickets/Lizenzen können Sie sich jederzeit unter folgendem Link per "
|
|
|
|
|
f"E-Mail zuschicken lassen:\n{portal_request_url}\n"
|
|
|
|
|
)
|
|
|
|
|
valid_until = "unbegrenzt (Lifetime)" if row["lifetime"] else row["expires_at"]
|
|
|
|
|
subject_prefix = "Ihre TESM-Lizenzserver-Lizenz" if row["role"] == "license_server" else "Ihre TESM-Lizenz"
|
|
|
|
|
try:
|
|
|
|
|
_graph_send_mail(
|
|
|
|
|
customer["contact_email"],
|
|
|
|
|
f"Ihre TESM-Lizenz ({LICENSE_TYPE_LABELS.get(row['type'], row['type'])})",
|
|
|
|
|
f"{subject_prefix} ({LICENSE_TYPE_LABELS.get(row['type'], row['type'])})",
|
|
|
|
|
f"Anbei Ihre Lizenzdatei. Bitte auf der Zielinstanz unter Einstellungen → Lizenz hochladen und aktivieren.\n\n"
|
|
|
|
|
f"Lizenz-ID: {license_id}\nTyp: {LICENSE_TYPE_LABELS.get(row['type'], row['type'])}\nGültig bis: {valid_until}\n"
|
|
|
|
|
f"{self_service_note}",
|
|
|
|
@@ -2967,7 +3151,15 @@ def license_delete(license_id):
|
|
|
|
|
def self_service(ticket_id):
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
|
ticket = _ticket_row(conn, ticket_id)
|
|
|
|
|
if not ticket:
|
|
|
|
|
if not ticket or ticket["role"] != "customer":
|
|
|
|
|
# Self-Service ist ein KUNDENWEG (unauthentifiziert, Zugriff allein
|
|
|
|
|
# ueber die unratbare Ticket-ID) -- Lizenzserver-Lizenzen werden
|
|
|
|
|
# ausschliesslich bewusst durch einen Admin auf der Ticket-
|
|
|
|
|
# Detailseite ausgestellt/verwaltet, nie ueber diesen Weg. Behandelt
|
|
|
|
|
# eine Lizenzserver-Ticket-ID hier bewusst wie "nicht gefunden",
|
|
|
|
|
# nicht wie "keine Berechtigung" -- sonst liesse sich allein aus der
|
|
|
|
|
# Fehlermeldung ablesen, dass eine geratene/erlangte ID zu einem
|
|
|
|
|
# existierenden Lizenzserver-Ticket gehoert.
|
|
|
|
|
conn.close()
|
|
|
|
|
abort(404)
|
|
|
|
|
customer = _customer_row(conn, ticket["customer_id"])
|
|
|
|
@@ -2988,7 +3180,7 @@ def self_service(ticket_id):
|
|
|
|
|
def self_service_download(ticket_id):
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
|
ticket = _ticket_row(conn, ticket_id)
|
|
|
|
|
if not ticket:
|
|
|
|
|
if not ticket or ticket["role"] != "customer":
|
|
|
|
|
conn.close()
|
|
|
|
|
abort(404)
|
|
|
|
|
current = _latest_license_for_ticket(conn, ticket_id)
|
|
|
|
@@ -3006,7 +3198,7 @@ def self_service_download(ticket_id):
|
|
|
|
|
def self_service_revoke(ticket_id):
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
|
ticket = _ticket_row(conn, ticket_id)
|
|
|
|
|
if not ticket:
|
|
|
|
|
if not ticket or ticket["role"] != "customer":
|
|
|
|
|
conn.close()
|
|
|
|
|
abort(404)
|
|
|
|
|
current = _latest_license_for_ticket(conn, ticket_id)
|
|
|
|
@@ -3024,7 +3216,7 @@ def self_service_revoke(ticket_id):
|
|
|
|
|
def self_service_issue(ticket_id):
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
|
ticket = _ticket_row(conn, ticket_id)
|
|
|
|
|
if not ticket:
|
|
|
|
|
if not ticket or ticket["role"] != "customer":
|
|
|
|
|
conn.close()
|
|
|
|
|
abort(404)
|
|
|
|
|
if not license_active():
|
|
|
|
@@ -3086,7 +3278,7 @@ def portal_request():
|
|
|
|
|
(now + timedelta(minutes=PORTAL_ACCESS_LINK_MINUTES)).strftime("%Y-%m-%d %H:%M:%S")),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
access_url = url_for("portal_access", token=grant_token, _external=True)
|
|
|
|
|
access_url = _customer_facing_url("portal_access", token=grant_token)
|
|
|
|
|
try:
|
|
|
|
|
_graph_send_mail(
|
|
|
|
|
customer["contact_email"],
|
|
|
|
@@ -3604,6 +3796,7 @@ def inject_license_topbar():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/")
|
|
|
|
|
@login_required
|
|
|
|
|
def index():
|
|
|
|
|
"""Dashboard des Master-Lizenzservers -- eine rein informative Kachel-
|
|
|
|
|
uebersicht der gerade AKTIVEN Lizenzen (analog zur Geraete-Kachel-
|
|
|
|
@@ -3613,8 +3806,15 @@ def index():
|
|
|
|
|
mit status='active' -- alles andere (Tickets anlegen/bearbeiten/
|
|
|
|
|
loeschen, Lizenzen ausstellen/widerrufen/loeschen) ist Verwaltung und
|
|
|
|
|
lebt seit der Kachel-Umstellung auf der eigenen Tickets-Seite
|
|
|
|
|
(tickets_list())."""
|
|
|
|
|
if not current_user.is_authenticated or not current_user.has_permission("licenses.view"):
|
|
|
|
|
(tickets_list()).
|
|
|
|
|
|
|
|
|
|
@login_required (anders als bei TESM selbst, wo das Geraete-Dashboard
|
|
|
|
|
bewusst auch ohne Login als reines Status-Board sichtbar bleibt): der
|
|
|
|
|
Lizenzserver ist reines internes Verwaltungswerkzeug -- ein anonymer
|
|
|
|
|
Besuch auf '/' soll immer zuerst auf die Login-Seite treffen (via
|
|
|
|
|
login_manager.login_view), nicht auf einen 'bitte anmelden'-Platzhalter
|
|
|
|
|
innerhalb der Dashboard-Huelle."""
|
|
|
|
|
if not current_user.has_permission("licenses.view"):
|
|
|
|
|
return render_template("index.html", buckets=None)
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
|
active_rows = conn.execute("""
|
|
|
|
@@ -3650,7 +3850,7 @@ def tickets_list():
|
|
|
|
|
Dashboards auf eine rein informative Kacheluebersicht (siehe index())
|
|
|
|
|
lebt die eigentliche Verwaltung hier, eigenstaendig ueber die
|
|
|
|
|
Navigation erreichbar."""
|
|
|
|
|
if not current_user.has_permission("licenses.view"):
|
|
|
|
|
if not current_user.has_permission("tickets.view"):
|
|
|
|
|
flash("Keine Berechtigung, Tickets anzusehen.", "danger")
|
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
conn = get_db_connection()
|
|
|
|
@@ -3683,8 +3883,8 @@ def tickets_list():
|
|
|
|
|
return render_template(
|
|
|
|
|
"tickets.html", tickets=tickets_view, type_labels=LICENSE_TYPE_LABELS, module_labels=MODULE_LABELS,
|
|
|
|
|
all_modules=licensing.ALL_MODULES, license_types=licensing.LICENSE_TYPES,
|
|
|
|
|
can_create=current_user.has_permission("licenses.create"),
|
|
|
|
|
can_edit=current_user.has_permission("licenses.edit"),
|
|
|
|
|
can_create=current_user.has_permission("tickets.create"),
|
|
|
|
|
can_edit=current_user.has_permission("tickets.edit"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -4674,7 +4874,7 @@ def settings():
|
|
|
|
|
@app.route("/settings/license")
|
|
|
|
|
@login_required
|
|
|
|
|
def settings_license():
|
|
|
|
|
if not current_user.can_view_settings_system:
|
|
|
|
|
if not current_user.can_view_settings_license:
|
|
|
|
|
flash("Keine Berechtigung, die Lizenz anzusehen.", "danger")
|
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
pending_raw = get_setting("license_pending_request", "")
|
|
|
|
@@ -4697,7 +4897,7 @@ def settings_license():
|
|
|
|
|
@app.route("/settings/license/upload", methods=["POST"])
|
|
|
|
|
@login_required
|
|
|
|
|
def license_upload():
|
|
|
|
|
if not current_user.has_permission("settings_system.edit"):
|
|
|
|
|
if not current_user.has_permission("settings_license.edit"):
|
|
|
|
|
flash("Keine Berechtigung, die Lizenz zu ändern.", "danger")
|
|
|
|
|
return redirect(url_for("settings_license"))
|
|
|
|
|
uploaded = request.files.get("license_file")
|
|
|
|
@@ -4790,7 +4990,7 @@ def _license_handshake_confirm(action, success_setting_fn, success_log_action, s
|
|
|
|
|
@app.route("/settings/license/activate", methods=["POST"])
|
|
|
|
|
@login_required
|
|
|
|
|
def license_activate():
|
|
|
|
|
if not current_user.has_permission("settings_system.edit"):
|
|
|
|
|
if not current_user.has_permission("settings_license.edit"):
|
|
|
|
|
flash("Keine Berechtigung, die Lizenz zu aktivieren.", "danger")
|
|
|
|
|
return redirect(url_for("settings_license"))
|
|
|
|
|
if not _license_state.get("file") or not _license_state.get("valid"):
|
|
|
|
@@ -4807,7 +5007,7 @@ def license_activate():
|
|
|
|
|
@app.route("/settings/license/activate/confirm", methods=["POST"])
|
|
|
|
|
@login_required
|
|
|
|
|
def license_activate_confirm():
|
|
|
|
|
if not current_user.has_permission("settings_system.edit"):
|
|
|
|
|
if not current_user.has_permission("settings_license.edit"):
|
|
|
|
|
flash("Keine Berechtigung, die Lizenz zu aktivieren.", "danger")
|
|
|
|
|
return redirect(url_for("settings_license"))
|
|
|
|
|
|
|
|
|
@@ -4821,7 +5021,7 @@ def license_activate_confirm():
|
|
|
|
|
@app.route("/settings/license/deactivate", methods=["POST"])
|
|
|
|
|
@login_required
|
|
|
|
|
def license_deactivate():
|
|
|
|
|
if not current_user.has_permission("settings_system.edit"):
|
|
|
|
|
if not current_user.has_permission("settings_license.edit"):
|
|
|
|
|
flash("Keine Berechtigung, die Lizenz zu deaktivieren.", "danger")
|
|
|
|
|
return redirect(url_for("settings_license"))
|
|
|
|
|
if not _license_state.get("file") or not license_activated():
|
|
|
|
@@ -4841,7 +5041,7 @@ def license_deactivate():
|
|
|
|
|
@app.route("/settings/license/deactivate/confirm", methods=["POST"])
|
|
|
|
|
@login_required
|
|
|
|
|
def license_deactivate_confirm():
|
|
|
|
|
if not current_user.has_permission("settings_system.edit"):
|
|
|
|
|
if not current_user.has_permission("settings_license.edit"):
|
|
|
|
|
flash("Keine Berechtigung, die Lizenz zu deaktivieren.", "danger")
|
|
|
|
|
return redirect(url_for("settings_license"))
|
|
|
|
|
|
|
|
|
|