Personalisierung Schritt 3: Konten im Check-in, Rückmeldung des Agents, Sperren, Neuinstallation
Check-in liefert persoenliche_konten (anlegen mit Hash, aktiv, sperren, entfernen). /api/v1/agent/persoenliche-konten nimmt die Ergebnisse an: angelegt löscht den Hash und merkt ohne "Bestehenden Zugang behalten" die übrigen Konten zum Entfernen vor. Ein Code ohne Kästchen sperrt bestehende Konten sofort. Sperren/Entsperren je Konto. Eine Neuinstallation beendet alle Personalisierungen des Geräts. Migration 0065 um gesperrt, entfernen, geraet_meldung und Status fehlgeschlagen ergänzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e12f2206a4
commit
b4c77689fd
2 files changed
+197
-5
No files matched your search
@@ -491,6 +491,7 @@ def load_or_create_device(
|
||||
|
||||
row = cur.fetchone()
|
||||
created = False
|
||||
personalisierungen_beenden_bei_neuinstallation(cur, row[0])
|
||||
else:
|
||||
device_id = uuid4()
|
||||
|
||||
@@ -5914,7 +5915,7 @@ def personalisierung_verfallene_markieren(cur) -> None:
|
||||
PERSONALISIERUNG_SPALTEN = (
|
||||
"p.id, p.device_id, p.status, p.code_gueltig_bis, p.bestehenden_behalten, p.erstellt_von, "
|
||||
"p.erstellt_am, p.email, p.anmeldename, p.eingereicht_am, p.bestaetigung_bis, p.bestaetigt_am, "
|
||||
"p.angelegt_am, p.beendet_am"
|
||||
"p.angelegt_am, p.beendet_am, p.gesperrt, p.entfernen, p.geraet_meldung"
|
||||
)
|
||||
|
||||
|
||||
@@ -5937,6 +5938,9 @@ def personalisierung_als_dict(row) -> dict:
|
||||
"bestaetigt_am": zeit(row[11]),
|
||||
"angelegt_am": zeit(row[12]),
|
||||
"beendet_am": zeit(row[13]),
|
||||
"gesperrt": row[14],
|
||||
"entfernen": row[15],
|
||||
"geraet_meldung": row[16],
|
||||
}
|
||||
|
||||
|
||||
@@ -6000,6 +6004,14 @@ def create_personalisierung_code(device_id: str, bestehenden_behalten: bool, ers
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
# Ohne "Bestehenden Zugang behalten" wird das bisherige Konto
|
||||
# sofort gesperrt; entfernt wird es erst, wenn das neue angelegt
|
||||
# ist (siehe record_persoenliche_konten_ergebnisse()).
|
||||
if not bestehenden_behalten:
|
||||
cur.execute(
|
||||
"UPDATE personalisierungen SET gesperrt = TRUE WHERE device_id = %s AND status = 'angelegt'",
|
||||
(device_id,),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO personalisierungen
|
||||
@@ -6155,6 +6167,137 @@ def set_email_domaenen(organization_id: str, domaenen: list[str]) -> bool:
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
def fetch_persoenliche_konten_fuer_agent(device_id: str) -> list[dict]:
|
||||
"""
|
||||
Soll-Zustand der persönlichen Konten für den Check-in (Schritt 3). Je
|
||||
Konto genau eine Aktion: anlegen (mit Passwort-Hash), aktiv, sperren
|
||||
oder entfernen. Konten, die hier fehlen, fasst die Rolle nicht an.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
personalisierung_verfallene_markieren(cur)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, status, anmeldename, passwort_hash, gesperrt, entfernen
|
||||
FROM personalisierungen
|
||||
WHERE device_id = %s AND status IN ('bestaetigt', 'angelegt')
|
||||
ORDER BY erstellt_am
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
konten = []
|
||||
for pid, status, anmeldename, passwort_hash, gesperrt, entfernen in rows:
|
||||
if status == "bestaetigt":
|
||||
if not passwort_hash:
|
||||
continue
|
||||
konten.append({"id": str(pid), "anmeldename": anmeldename, "aktion": "anlegen", "passwort_hash": passwort_hash})
|
||||
else:
|
||||
aktion = "entfernen" if entfernen else "sperren" if gesperrt else "aktiv"
|
||||
konten.append({"id": str(pid), "anmeldename": anmeldename, "aktion": aktion})
|
||||
return konten
|
||||
|
||||
|
||||
PERSOENLICHE_KONTEN_ERGEBNISSE = ("angelegt", "aktiv", "gesperrt", "entfernt", "fehler")
|
||||
|
||||
|
||||
def record_persoenliche_konten_ergebnisse(device_id: str, ergebnisse: list) -> None:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
for eintrag in ergebnisse:
|
||||
meldung = (eintrag.meldung or "")[:1000] or None
|
||||
cur.execute(
|
||||
"SELECT status, bestehenden_behalten FROM personalisierungen WHERE id = %s AND device_id = %s FOR UPDATE",
|
||||
(eintrag.id, device_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
continue
|
||||
status, behalten = row
|
||||
|
||||
if eintrag.ergebnis == "angelegt" and status == "bestaetigt":
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'angelegt', angelegt_am = now(), passwort_hash = NULL, geraet_meldung = NULL
|
||||
WHERE id = %s
|
||||
""",
|
||||
(eintrag.id,),
|
||||
)
|
||||
if not behalten:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen SET entfernen = TRUE, gesperrt = TRUE
|
||||
WHERE device_id = %s AND status = 'angelegt' AND id <> %s
|
||||
""",
|
||||
(device_id, eintrag.id),
|
||||
)
|
||||
elif eintrag.ergebnis == "entfernt" and status == "angelegt":
|
||||
cur.execute(
|
||||
"UPDATE personalisierungen SET status = 'beendet', beendet_am = now(), geraet_meldung = NULL WHERE id = %s",
|
||||
(eintrag.id,),
|
||||
)
|
||||
elif eintrag.ergebnis == "fehler" and status == "bestaetigt":
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'fehlgeschlagen', passwort_hash = NULL, geraet_meldung = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(meldung, eintrag.id),
|
||||
)
|
||||
elif eintrag.ergebnis == "fehler":
|
||||
cur.execute("UPDATE personalisierungen SET geraet_meldung = %s WHERE id = %s", (meldung, eintrag.id))
|
||||
elif eintrag.ergebnis in ("aktiv", "gesperrt"):
|
||||
cur.execute("UPDATE personalisierungen SET geraet_meldung = NULL WHERE id = %s", (eintrag.id,))
|
||||
|
||||
|
||||
def set_personalisierung_gesperrt(device_id: str, personalisierung_id: str, gesperrt: bool) -> bool:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen SET gesperrt = %s
|
||||
WHERE id = %s AND device_id = %s AND status = 'angelegt' AND NOT entfernen
|
||||
""",
|
||||
(gesperrt, personalisierung_id, device_id),
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
def personalisierungen_beenden_bei_neuinstallation(cur, device_id) -> None:
|
||||
"""
|
||||
ADR-0041: Der Fingerprint ist hardwarebasiert, ein neu installiertes
|
||||
Gerät kommt als dasselbe zurück - ohne die Konten von vorher.
|
||||
"""
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'beendet', beendet_am = now(), passwort_hash = NULL, bestaetigung_token_hash = NULL
|
||||
WHERE device_id = %s AND status IN ('code_offen', 'eingereicht', 'bestaetigt', 'angelegt')
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
|
||||
|
||||
class PersoenlichesKontoErgebnis(BaseModel):
|
||||
id: str
|
||||
ergebnis: str
|
||||
meldung: str | None = None
|
||||
|
||||
|
||||
class AgentPersoenlicheKontenRequest(BaseModel):
|
||||
device_id: str
|
||||
ergebnisse: list[PersoenlichesKontoErgebnis]
|
||||
|
||||
|
||||
class SetGesperrtRequest(BaseModel):
|
||||
gesperrt: bool
|
||||
|
||||
|
||||
class CreatePersonalisierungRequest(BaseModel):
|
||||
bestehenden_behalten: bool = False
|
||||
erstellt_von: str | None = None
|
||||
@@ -6215,6 +6358,42 @@ def get_personalisierungen_organisation(organization_id: str, authorization: str
|
||||
return {"success": True, "geraete": fetch_personalisierungen_organisation(organization_id)}
|
||||
|
||||
|
||||
@app.post("/api/v1/devices/{device_id}/personalisierung/{personalisierung_id}/gesperrt")
|
||||
def post_personalisierung_gesperrt(
|
||||
device_id: str,
|
||||
personalisierung_id: str,
|
||||
payload: SetGesperrtRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": set_personalisierung_gesperrt(device_id, personalisierung_id, payload.gesperrt)}
|
||||
|
||||
|
||||
@app.post("/api/v1/agent/persoenliche-konten")
|
||||
def agent_persoenliche_konten(
|
||||
payload: AgentPersoenlicheKontenRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if authorization is None or not authorization.startswith("Bearer "):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Authorization-Header."}
|
||||
|
||||
if not verify_agent_secret(payload.device_id, authorization.removeprefix("Bearer ")):
|
||||
return {"success": False, "error": "unauthorized", "message": "Ungültiges Agent-Secret."}
|
||||
|
||||
for eintrag in payload.ergebnisse:
|
||||
if eintrag.ergebnis not in PERSOENLICHE_KONTEN_ERGEBNISSE:
|
||||
return {"success": False, "error": "invalid_request", "message": f"Unbekanntes Ergebnis: {eintrag.ergebnis}"}
|
||||
try:
|
||||
UUID(eintrag.id)
|
||||
except ValueError:
|
||||
return {"success": False, "error": "invalid_request", "message": "Ungültige Konto-ID."}
|
||||
|
||||
record_persoenliche_konten_ergebnisse(payload.device_id, payload.ergebnisse)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
class CheckPersonalisierungRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
@@ -6736,6 +6915,9 @@ def agent_checkin(
|
||||
# wenn er sich vom lokal tatsächlich gesetzten unterscheidet.
|
||||
"hostname": hostname,
|
||||
"ansible_repo": ansible_repo_fuer_geraet(payload.device_id),
|
||||
# Persönliche Konten (ADR-0041, Schritt 3). Ältere Agenten ignorieren
|
||||
# das Feld; die Rolle fasst nur hier genannte Konten an.
|
||||
"persoenliche_konten": fetch_persoenliche_konten_fuer_agent(payload.device_id),
|
||||
"poll_interval_seconds": effektives_intervall,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user