Merge branch 'veyon-schluessel'
This commit is contained in:
commit
46ec2f5151
2 files changed
+187
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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