Ausgabe fuer Sub-Lizenzserver: kein Weg zur Rolle master

This commit is contained in:
alientim committed 2026-09-09 00:07:34 +02:00
1 parent fd333f60ff
commit 157a244900
9 files changed
+548 -124

No files matched your search

@@ -8,6 +8,7 @@ musste (und ``licensing.py`` byte-identisch gehalten werden *musste*).
from __future__ import annotations
import importlib.util
import logging
from pathlib import Path
from typing import Any
@@ -51,6 +52,18 @@ ENV_PREFIX = "TESM_LICENSE_"
logger = logging.getLogger(__name__)
def is_master_edition() -> bool:
"""Ist das hier die Master-Ausgabe?
Entscheidet die **Anwesenheit** des Teilpakets ``tesm_license.master``, nicht
eine Einstellung: eine Einstellung koennte der lokale Administrator eines
Sub-Lizenzservers umstellen, eine fehlende Datei nicht. Das Release fuer
Sub-Lizenzserver bringt das Teilpaket nicht mit -- dort gibt es also
ueberhaupt keinen Code, der die Rolle ``master`` erzeugen koennte.
"""
return importlib.util.find_spec("tesm_license.master") is not None
def master_key_path(config: Any) -> Path:
return Path(config.data_dir) / "master_signing_key.json"
@@ -32,6 +32,7 @@ from tesm_core.licensing_client import LICENSED_FEATURES, LicenseError
import tesm_licensing as lic
from .. import is_master_edition
from ..services import master_key
from .common import can_issue_license_server_licenses, log_event, signing_key
@@ -62,6 +63,7 @@ def overview():
),
signing_key_health=master_key.health(extension.registry["master_key_path"]),
may_issue_license_server=can_issue_license_server_licenses(),
master_edition=is_master_edition(),
grace_days=lic.GRACE_PERIOD_DAYS,
)
+35 -74
View File
@@ -1,13 +1,14 @@
"""Kommandozeilenwerkzeuge des Lizenzservers.
Enthaelt insbesondere ``bootstrap`` -- die eigene Lizenz der Wurzelinstanz.
``init-key`` erzeugt den Signaturschluessel dieser Installation -- den braucht
**jeder** Lizenzserver, um Kundenlizenzen zu signieren, auch ein
Sub-Lizenzserver.
Warum das ein lokales Skript ist und keine Funktion in der Oberflaeche: die
Master-Lizenz ist die einzige Lizenz, fuer die es **keine hoehere Instanz gibt**,
die sie bestaetigen koennte. Ein Aktivierungs-Handshake gegen sich selbst waere
Theater. Das Skript signiert die Datei mit dem eigenen Schluessel und setzt den
Aktivierungszustand direkt -- und nur so kann eine Lizenzdatei je die Rolle
``master`` tragen. Die Ticket-Oberflaeche bietet diese Rolle an keiner Stelle an.
``bootstrap`` stellt die eigene Lizenz der Wurzelinstanz aus und ist damit der
einzige Weg, auf dem eine Lizenzdatei die Rolle ``master`` bekommt (ueber
Tickets ist sie auf jedem Server abgewiesen). Die Arbeit dazu liegt im
Teilpaket ``tesm_license.master``, das nur die Master-Ausgabe mitbringt; fehlt
es, sagt der Befehl das und tut nichts. Siehe dort fuer die Begruendung.
"""
from __future__ import annotations
@@ -20,7 +21,6 @@ from pathlib import Path
from typing import Any
import tesm_licensing as lic
import tesm_licensing_server as licsrv
from tesm_core import audit
from tesm_core.cli_web import add_web_setup, run_web_setup
from tesm_core.db import Database
@@ -28,7 +28,7 @@ from tesm_core.extension import core
from tesm_core.rbac import service as rbac
from tesm_core.security import passwords
from . import create_app, master_key_path
from . import create_app, is_master_edition, master_key_path
from .services import legacy_import, master_key
@@ -62,73 +62,29 @@ def cmd_init_key(args: argparse.Namespace) -> int:
def cmd_bootstrap(args: argparse.Namespace) -> int:
"""Stellt die eigene Lizenz dieser Wurzelinstanz aus."""
"""Stellt die eigene Lizenz dieser Wurzelinstanz aus -- nur in der Master-Ausgabe."""
if not is_master_edition():
for zeile in (
"Diese Ausgabe ist fuer Sub-Lizenzserver und kann keine Master-Lizenz erzeugen.",
"",
"Ein Sub-Lizenzserver bekommt seine Lizenz vom Master-Lizenzserver und spielt",
"sie unter 'Eigene Lizenz' ein. Danach stellt er selbst Kundenlizenzen aus --",
"mit seinem eigenen Signaturschluessel, der beim Installieren entstanden ist.",
"",
"Die Master-Ausgabe liegt im Repository tesm-license-master.",
):
print(zeile, file=sys.stderr)
return 2
from .master import bootstrap as eigene_lizenz_ausstellen
app, extension = _app_and_core()
key = master_key.ensure(master_key_path(extension.config))
with app.app_context(), extension.database.session() as conn:
vendor = {
"name": str(extension.settings.get(conn, "vendor_name") or "Eigenbetrieb"),
"email": str(extension.settings.get(conn, "vendor_email") or ""),
"phone": str(extension.settings.get(conn, "vendor_phone") or ""),
"address": str(extension.settings.get(conn, "vendor_address") or ""),
"logo_base64": "",
}
bundle, _ = licsrv.issue_license(
customer={"name": args.name, "contact_email": vendor["email"]},
license_type="enterprise",
modules=[],
valid_days=int(args.days),
master_private_key_b64=key.private_key,
master_public_key_b64=key.public_key,
# Bewusst leer: es gibt keinen uebergeordneten Endpunkt. Damit
# laeuft der Handshake fuer diese Lizenz nie ins Netz.
master_endpoint="",
vendor=vendor,
role="master",
)
document, client_key = lic.split_bundle(bundle)
config = extension.config
config.license_path.parent.mkdir(parents=True, exist_ok=True)
config.license_path.write_text(
json.dumps(document, indent=2, sort_keys=True, ensure_ascii=False) + "\n",
encoding="utf-8",
)
try:
config.license_path.chmod(0o640)
except OSError:
pass
from tesm_core.keystore import write_private_file
write_private_file(
config.license_key_path,
json.dumps({"license_id": document["license_id"], "client_key": client_key}) + "\n",
)
with extension.database.transaction(conn):
conn.execute(
"INSERT INTO trusted_issuer_keys (key_id, public_key, label, source, added_at, "
"added_by) VALUES (?,?,?,'bootstrap',datetime('now'),'cli') "
"ON CONFLICT(key_id) DO NOTHING",
(key.key_id, key.public_key, "Eigener Signaturschluessel"),
)
# Es gibt keine hoehere Instanz, die diese Lizenz aktivieren koennte.
extension.settings.set(conn, "license_activation_status", "active", actor="cli")
extension.settings.set(conn, "license_pending_request", "", actor="cli")
audit.log(
conn,
actor="cli",
action="own_license.bootstrapped",
target=document["license_id"],
detail=f"Rolle master, {args.days} Tage, Schluessel {key.key_id}",
severity="warning",
)
state = extension.license.reload(conn) if extension.license else None
document, state = eigene_lizenz_ausstellen(
app, extension, days=int(args.days), name=args.name
)
print(f"Master-Lizenz ausgestellt: {document['license_id']}")
print(f"Gueltig bis: {document['expires_at']}")
print(f"Datei: {config.license_path}")
print(f"Datei: {extension.config.license_path}")
if state is not None:
print(f"Zustand: gueltig={state.valid}, aktiviert={state.activated}, Rolle={state.role}")
return 0
@@ -434,7 +390,12 @@ def build_parser() -> argparse.ArgumentParser:
init_key.set_defaults(func=cmd_init_key)
bootstrap = sub.add_parser(
"bootstrap", help="Eigene Master-Lizenz dieser Wurzelinstanz ausstellen"
"bootstrap",
help=(
"Eigene Master-Lizenz dieser Wurzelinstanz ausstellen"
if is_master_edition()
else "nicht in dieser Ausgabe (nur Master-Lizenzserver)"
),
)
bootstrap.add_argument("--days", type=int, default=3650)
bootstrap.add_argument("--name", default="Lizenzserver (Eigenbetrieb)")
@@ -116,9 +116,16 @@
{% endif %}
</section>
{% else %}
{{ empty_state('Keine gültige Lizenz',
'Spielen Sie eine Lizenzdatei ein oder erzeugen Sie mit "tesm-license-admin bootstrap"
die eigene Lizenz der Wurzelinstanz.', 'key') }}
{% if master_edition %}
{{ empty_state('Keine gültige Lizenz',
'Spielen Sie eine Lizenzdatei ein oder erzeugen Sie mit "tesm-license-admin bootstrap"
die eigene Lizenz der Wurzelinstanz.', 'key') }}
{% else %}
{{ empty_state('Keine gültige Lizenz',
'Diese Ausgabe ist ein Sub-Lizenzserver: fordern Sie beim Master-Lizenzserver eine
Lizenz für diesen Server an und spielen Sie die Datei hier ein. Eine Master-Lizenz
kann diese Ausgabe nicht erzeugen.', 'key') }}
{% endif %}
{% endif %}
{% if pending %}