Veyon: Schluesselpaar je OE, Check-in reichert Optionen an (Migration 0063)
Bauplan Schritt 1 (platform-docs roadmap/veyon-schluesselverteilung- diskussion-2026-10-14.md). Paar beim ersten Bedarf erzeugt (RSA 4096, PKCS#8/SPKI-PEM wie veyon-cli), privat Fernet-verschluesselt. Optionen: veyon_schluesselname, veyon_public_pem, veyon_raum, veyon_lehrer; nur Lehrergeraete zusaetzlich veyon_private_pem und veyon_rechner (<hostname>.local, ohne sich selbst). Lehrer = Haekchen veyon_lehrer an irgendeiner aktiven Gruppe. Fehler brechen den Check-in nicht ab. Getestet gegen eine Wegwerf-Datenbank mit allen Migrationen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed145be3d2
commit
a49252a81a
2 files changed
+187
-1
No files matched your search
@@ -1597,11 +1597,15 @@ def fetch_auftragskatalog_state(device_id: str):
|
|||||||
aktiv = workspace_default
|
aktiv = workspace_default
|
||||||
optionen = {}
|
optionen = {}
|
||||||
|
|
||||||
|
optionen = decrypt_sensitive_optionen(optionen)
|
||||||
|
if key == VEYON_MERKMAL and aktiv:
|
||||||
|
optionen = veyon_optionen_anreichern(device_id, merkmal_id, optionen)
|
||||||
|
|
||||||
ergebnis.append({
|
ergebnis.append({
|
||||||
"merkmal": key,
|
"merkmal": key,
|
||||||
"ansible_role": ansible_role,
|
"ansible_role": ansible_role,
|
||||||
"state": "present" if aktiv else "absent",
|
"state": "present" if aktiv else "absent",
|
||||||
"optionen": decrypt_sensitive_optionen(optionen),
|
"optionen": optionen,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Standardbrowser der Workspace-Version: immer mitschicken, auch als
|
# Standardbrowser der Workspace-Version: immer mitschicken, auch als
|
||||||
@@ -1627,6 +1631,169 @@ def fetch_auftragskatalog_state(device_id: str):
|
|||||||
|
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
|
# Veyon (15.10.2026, platform-docs roadmap/veyon-schluesselverteilung-
|
||||||
|
# diskussion-2026-10-14.md, Bauplan Schritt 1, Migration 0063): ein
|
||||||
|
# Schluesselpaar je OE (= Raum). Der Check-in reichert die Optionen des
|
||||||
|
# Merkmals "veyon" an - wie die Org-Ressourcen serverseitig berechnet, der
|
||||||
|
# Agent reicht sie ungesehen an die Rolle weiter (seit Agent 17d7186 ueber
|
||||||
|
# eine root-Datei, nicht mehr per Kommandozeile):
|
||||||
|
# veyon_schluesselname, veyon_public_pem, veyon_raum, veyon_lehrer
|
||||||
|
# nur Lehrergeraete zusaetzlich: veyon_private_pem, veyon_rechner
|
||||||
|
# Format wie `veyon-cli authkeys create` (RSA 4096, privat PKCS#8-PEM,
|
||||||
|
# oeffentlich SubjectPublicKeyInfo-PEM) - auf Liebherr-83/-35 am 15.10.2026
|
||||||
|
# mit einem so erzeugten Paar geprueft.
|
||||||
|
VEYON_MERKMAL = "veyon"
|
||||||
|
VEYON_SCHLUESSEL_PRAEFIX = "tuxflotte"
|
||||||
|
|
||||||
|
|
||||||
|
def veyon_schluesselname(oe_id) -> str:
|
||||||
|
"""Veyon erlaubt nur Buchstaben: Praefix + erste 12 Hex-Zeichen der
|
||||||
|
OE-ID, Ziffern 0-9 auf g-p abgebildet. Eindeutig genug je
|
||||||
|
Installation (UNIQUE in der Tabelle faengt den Rest ab), stabil."""
|
||||||
|
hexteil = str(oe_id).replace("-", "")[:12].lower()
|
||||||
|
return VEYON_SCHLUESSEL_PRAEFIX + "".join(
|
||||||
|
chr(ord("g") + int(c)) if c.isdigit() else c for c in hexteil
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def erzeuge_veyon_schluesselpaar() -> tuple[str, str]:
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
|
schluessel = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
||||||
|
privat = schluessel.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.PKCS8,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
).decode("ascii")
|
||||||
|
oeffentlich = schluessel.public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
).decode("ascii")
|
||||||
|
return privat, oeffentlich
|
||||||
|
|
||||||
|
|
||||||
|
def hole_oder_erzeuge_veyon_schluessel(cur, oe_id) -> tuple[str, str, str]:
|
||||||
|
"""(name, public_pem, private_pem) der OE; legt das Paar beim ersten
|
||||||
|
Bedarf an. ON CONFLICT DO NOTHING + erneutes Lesen: zwei Geraete
|
||||||
|
derselben OE, die gleichzeitig einchecken, bekommen dasselbe Paar."""
|
||||||
|
cur.execute(
|
||||||
|
"SELECT name, public_pem, private_pem_encrypted FROM veyon_schluessel WHERE oe_id = %s",
|
||||||
|
(oe_id,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
privat, oeffentlich = erzeuge_veyon_schluesselpaar()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO veyon_schluessel (oe_id, name, public_pem, private_pem_encrypted)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON CONFLICT (oe_id) DO NOTHING
|
||||||
|
""",
|
||||||
|
(oe_id, veyon_schluesselname(oe_id), oeffentlich,
|
||||||
|
_fernet().encrypt(privat.encode("ascii")).decode("ascii")),
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"SELECT name, public_pem, private_pem_encrypted FROM veyon_schluessel WHERE oe_id = %s",
|
||||||
|
(oe_id,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
name, oeffentlich, privat_verschluesselt = row
|
||||||
|
privat = _fernet().decrypt(privat_verschluesselt.encode("ascii")).decode("ascii")
|
||||||
|
return name, oeffentlich, privat
|
||||||
|
|
||||||
|
|
||||||
|
def veyon_aktiv(cur, device_id, merkmal_id) -> bool:
|
||||||
|
"""Effektiver Zustand des Merkmals veyon fuer ein (anderes) Geraet -
|
||||||
|
dieselbe Aufloesung wie fetch_auftragskatalog_state(): OE-/Gruppen-
|
||||||
|
Zwischenschicht, sonst der Workspace-Standard."""
|
||||||
|
overrides = resolve_katalog_overrides(str(device_id))
|
||||||
|
if merkmal_id in overrides:
|
||||||
|
return bool(overrides[merkmal_id][0])
|
||||||
|
basis = geraete_basis(cur, str(device_id))
|
||||||
|
if basis is None:
|
||||||
|
return False
|
||||||
|
version_id, _backend_id = basis
|
||||||
|
cur.execute(
|
||||||
|
"SELECT 1 FROM workspace_version_merkmale WHERE workspace_version_id = %s AND merkmal_id = %s",
|
||||||
|
(version_id, merkmal_id),
|
||||||
|
)
|
||||||
|
return cur.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def veyon_ist_lehrer(cur, device_id, merkmal_id, optionen: dict) -> bool:
|
||||||
|
"""Lehrergeraet, wenn IRGENDEINE aktive Quelle das Haekchen setzt -
|
||||||
|
nicht nur die eine, deren Optionen resolve_katalog_overrides() liefert
|
||||||
|
(dort gewinnt die OE, sobald Veyon dort aktiv ist; ein Haekchen an der
|
||||||
|
Gruppe "Veyon-Lehrer" ginge sonst verloren). Entscheidung 15.10.2026:
|
||||||
|
Checkbox veyon_lehrer an der Gruppe."""
|
||||||
|
if optionen.get("veyon_lehrer") is True:
|
||||||
|
return True
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT 1
|
||||||
|
FROM device_gruppen dg
|
||||||
|
JOIN gruppen_merkmale gm ON gm.gruppe_id = dg.gruppe_id
|
||||||
|
WHERE dg.device_id = %s AND gm.merkmal_id = %s AND gm.aktiv
|
||||||
|
AND (gm.optionen ->> 'veyon_lehrer') = 'true'
|
||||||
|
""",
|
||||||
|
(device_id, merkmal_id),
|
||||||
|
)
|
||||||
|
return cur.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def veyon_optionen_anreichern(device_id, merkmal_id, optionen: dict) -> dict:
|
||||||
|
"""Siehe VEYON_MERKMAL. Fehler (z.B. fehlender TUXFLOTTE_ENCRYPTION_KEY)
|
||||||
|
brechen den Check-in nicht ab: die Rolle bekommt dann keinen Schluessel
|
||||||
|
und laesst die Authentifizierung unangetastet."""
|
||||||
|
try:
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT d.oe_id, oe.name
|
||||||
|
FROM devices d JOIN organisationseinheiten oe ON oe.id = d.oe_id
|
||||||
|
WHERE d.id = %s
|
||||||
|
""",
|
||||||
|
(device_id,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
return optionen
|
||||||
|
oe_id, oe_name = row
|
||||||
|
|
||||||
|
name, oeffentlich, privat = hole_oder_erzeuge_veyon_schluessel(cur, oe_id)
|
||||||
|
lehrer = veyon_ist_lehrer(cur, device_id, merkmal_id, optionen)
|
||||||
|
|
||||||
|
ergebnis = dict(optionen)
|
||||||
|
ergebnis.update({
|
||||||
|
"veyon_schluesselname": name,
|
||||||
|
"veyon_public_pem": oeffentlich,
|
||||||
|
"veyon_raum": oe_name,
|
||||||
|
"veyon_lehrer": lehrer,
|
||||||
|
})
|
||||||
|
if lehrer:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, hostname FROM devices
|
||||||
|
WHERE oe_id = %s AND archived_at IS NULL AND deprovisioned_at IS NULL
|
||||||
|
AND hostname IS NOT NULL AND hostname <> ''
|
||||||
|
ORDER BY lower(hostname)
|
||||||
|
""",
|
||||||
|
(oe_id,),
|
||||||
|
)
|
||||||
|
rechner = [
|
||||||
|
{"name": hostname, "host": f"{hostname}.local"}
|
||||||
|
for anderes_id, hostname in cur.fetchall()
|
||||||
|
if str(anderes_id) != str(device_id) and veyon_aktiv(cur, anderes_id, merkmal_id)
|
||||||
|
]
|
||||||
|
ergebnis["veyon_private_pem"] = privat
|
||||||
|
ergebnis["veyon_rechner"] = rechner
|
||||||
|
return ergebnis
|
||||||
|
except Exception as exc: # noqa: BLE001 - Check-in darf nicht scheitern
|
||||||
|
print(f"[veyon] Optionen fuer Geraet {device_id} nicht angereichert: {exc}")
|
||||||
|
return optionen
|
||||||
|
|
||||||
# Org-Ressourcen (15.10.2026, platform-docs roadmap/org-ressourcen-konzept.md,
|
# Org-Ressourcen (15.10.2026, platform-docs roadmap/org-ressourcen-konzept.md,
|
||||||
# Migration 0062): lokale Dienste einer Organisation, die App-Rollen zum
|
# Migration 0062): lokale Dienste einer Organisation, die App-Rollen zum
|
||||||
# Vorbefüllen nutzen. Der Check-in schickt die für ein Gerät geltenden
|
# Vorbefüllen nutzen. Der Check-in schickt die für ein Gerät geltenden
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Veyon-Schluessel je OE (15.10.2026, platform-docs
|
||||||
|
-- roadmap/veyon-schluesselverteilung-diskussion-2026-10-14.md, Bauplan
|
||||||
|
-- Schritt 1). Ein Schluesselpaar je OE (= Raum), erzeugt beim ersten
|
||||||
|
-- Check-in eines Geraets dieser OE mit aktivem Merkmal "veyon". Der private
|
||||||
|
-- Schluessel liegt Fernet-verschluesselt (TUXFLOTTE_ENCRYPTION_KEY, wie die
|
||||||
|
-- _passwort-Optionen) und geht beim Check-in nur an Lehrergeraete.
|
||||||
|
-- name: nur Buchstaben (Veyon-Vorgabe), siehe veyon_schluesselname().
|
||||||
|
|
||||||
|
CREATE TABLE veyon_schluessel (
|
||||||
|
oe_id UUID PRIMARY KEY REFERENCES organisationseinheiten(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL UNIQUE CHECK (name ~ '^[a-z]+$'),
|
||||||
|
public_pem TEXT NOT NULL,
|
||||||
|
private_pem_encrypted TEXT NOT NULL,
|
||||||
|
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in new issue
Block a user