Merge branch 'veyon-schluessel'

This commit is contained in:
admin committed 2026-10-15 14:06:41 +02:00
commit 46ec2f5151
2 files changed
+187 -1

No files matched your search

+168 -1
View File
@@ -1597,11 +1597,15 @@ def fetch_auftragskatalog_state(device_id: str):
aktiv = workspace_default
optionen = {}
optionen = decrypt_sensitive_optionen(optionen)
if key == VEYON_MERKMAL and aktiv:
optionen = veyon_optionen_anreichern(device_id, merkmal_id, optionen)
ergebnis.append({
"merkmal": key,
"ansible_role": ansible_role,
"state": "present" if aktiv else "absent",
"optionen": decrypt_sensitive_optionen(optionen),
"optionen": optionen,
})
# Standardbrowser der Workspace-Version: immer mitschicken, auch als
@@ -1627,6 +1631,169 @@ def fetch_auftragskatalog_state(device_id: str):
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,
# Migration 0062): lokale Dienste einer Organisation, die App-Rollen zum
# Vorbefüllen nutzen. Der Check-in schickt die für ein Gerät geltenden
+19
View File
@@ -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;