docs: ADR-0013 Geräte-Lebenszyklus-Aktionen im Kundenportal
Dokumentiert nachträglich eine bereits deployte, live-verifizierte Session-Arbeit: De-/Reprovisionieren (serverseitiger Credential-Entzug, Agent-Self-Heal mit deprovisioned_at-Gate), Löschen als Soft Delete/Archivierung, Hardware-/Log-Leseendpunkte. Bislang komplett undokumentiert in platform-docs gewesen. 09-data-model-v1.md um devices.deprovisioned_at/archived_at ergänzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
94c4ed69fd
commit
a2a34e568f
37
adr/0013-geraete-lebenszyklus-kundenportal.md
Normal file
37
adr/0013-geraete-lebenszyklus-kundenportal.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# ADR-0013: Geräte-Lebenszyklus-Aktionen im Kundenportal
|
||||||
|
|
||||||
|
**Status:** Beschlossen
|
||||||
|
**Datum:** 10.08.2026
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Die Geräteliste im Kundenportal (`/geraete`, siehe ADR-0011) zeigte bislang nur Hostname, Kennung, letzten Check-in und einen Link zum Auftragskatalog. Kund:innen fehlte jede Möglichkeit, Hardware-Informationen oder die Installations-/Aktionshistorie eines Geräts einzusehen, oder ein Gerät zu deaktivieren beziehungsweise loszuwerden, ohne den Betreiber zu kontaktieren.
|
||||||
|
|
||||||
|
Für „De-Provisionieren" und „Löschen" waren zwei Fragen zu klären, die reale Konsequenzen auf im Feld laufende Geräte haben:
|
||||||
|
|
||||||
|
1. Was passiert technisch mit einem deprovisionierten Gerät — wird der laufende Agent aktiv ferngesteuert deinstalliert, oder wird ihm nur serverseitig der Zugriff entzogen?
|
||||||
|
2. Ist „Löschen" ein harter, unwiderruflicher Datenbank-Delete, oder eine Archivierung?
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
**De-Provisionieren ist ein rein serverseitiger Credential-Entzug**, keine aktive Fernabmeldung. Das Agent-Secret des Geräts wird ungültig gemacht (`agent_secret_hash = NULL`, `deprovisioned_at = now()`); der laufende Agent auf dem Gerät selbst wird nicht kontaktiert und deinstalliert sich nicht — er bekommt beim nächsten Check-in schlicht `401 unauthorized` und bleibt bis zur Reprovisionierung untätig liegen. Historie (Hardware-Snapshots, Aktions-Log) bleibt vollständig erhalten. Diese Variante wurde einer aktiven Fernabmeldung vorgezogen, weil letztere eine neue, deutlich invasivere Fähigkeit im `provisioning-agent` gebraucht hätte (Uninstall-Kommando auf echten Kundengeräten), mit entsprechend höherem Test- und Sicherheitsaufwand.
|
||||||
|
|
||||||
|
Damit „Erneut provisionieren" ein deprovisioniertes, aber weiterhin laufendes Gerät ohne physischen Eingriff reaktivieren kann, wurde der Agent (`provisioning-agent/agent.py`) um einen **Self-Heal-Mechanismus** erweitert: bekommt der Agent beim Check-in `401`, ruft er einmalig `/api/v1/agent/bootstrap` erneut auf (dieser Endpoint kannte von Anfang an keine andere Authentifizierung als die `device_id` selbst — bestehendes Design, kein neues Risiko) und übernimmt bei Erfolg das neue Secret. Damit dieser Mechanismus die De-Provisionierung nicht selbst aushebelt — ein deprovisioniertes Gerät würde sich sonst beim nächsten Poll-Zyklus einfach automatisch selbst reaktivieren, unabhängig davon, ob „Erneut provisionieren" im Portal je ausgelöst wurde —, prüft `agent_bootstrap` seit dieser Änderung zusätzlich `deprovisioned_at IS NULL`. Erst das explizite Löschen von `deprovisioned_at` (die Reprovisionieren-Aktion) macht einen erneuten Bootstrap-Aufruf überhaupt erfolgreich. Dieses Gate wurde bei der Live-Verifikation nachträglich ergänzt, nachdem der ursprüngliche Entwurf diese Lücke übersehen hatte.
|
||||||
|
|
||||||
|
**Löschen ist Soft Delete (Archivierung), kein Hard Delete.** Ein „gelöschtes" Gerät bekommt `archived_at = now()` gesetzt, verschwindet aus der aktiven Geräteliste (`fetch_devices_for_organization()`/`fetch_all_devices()` filtern `archived_at IS NULL`), Datensatz und Historie bleiben aber vollständig in der Datenbank erhalten. Kein separater „Wiederherstellen"-Endpunkt in dieser ersten Ausbaustufe — die Zeile bleibt bei Bedarf über die Datenbank erreichbar.
|
||||||
|
|
||||||
|
Zusätzlich neu: Lese-Endpunkte für **Hardware-Informationen** (`GET /api/v1/devices/{id}/hardware`, jüngster `hardware_snapshots`-Eintrag samt Netzwerk-Interfaces/Storage-Devices) und **Installations-/Aktions-Log** (`GET /api/v1/devices/{id}/events`, bestehende `device_merkmal_events`-Tabelle aus ADR-0010, hier erstmals für Kund:innen sichtbar gemacht statt nur intern verwendet).
|
||||||
|
|
||||||
|
Alle Aktionen sind über ein Pico-natives Dropdown (`<details class="dropdown">`) in der Geräteliste erreichbar, ohne zusätzliches JavaScript — konsistent mit dem Rest von `kundenplattform`, das durchgehend serverseitig rendert.
|
||||||
|
|
||||||
|
## Konsequenzen
|
||||||
|
|
||||||
|
`devices` bekommt zwei unabhängige, nullable Zeitstempel-Spalten (`deprovisioned_at`, `archived_at`) statt eines einzelnen Status-Enums — ein Gerät kann deprovisioniert und trotzdem noch in der aktiven Liste sichtbar sein, während „archiviert" es komplett ausblendet; ein einzelnes Enum hätte diese unabhängigen Zustände entweder verboten oder umständlich kombinieren müssen.
|
||||||
|
|
||||||
|
`agent_bootstrap` verweigert seit dieser Änderung Geräte mit gesetztem `deprovisioned_at` — bestehende, funktionierende Erstinstallationen sind davon nicht betroffen, da `deprovisioned_at` bei neu angelegten Geräten immer `NULL` ist.
|
||||||
|
|
||||||
|
Der Self-Heal-Mechanismus im Agenten ist die einzige Änderung dieses ADRs, die tatsächlich auf bereits im Feld laufenden Geräten ausgeführten Code betrifft (nicht nur auf anode) — entsprechend wurde er vor dem Live-Rollout gezielt gegen ein deprovisioniertes/reprovisioniertes Testgerät durchgespielt, nicht nur gegen die API-Antwort geprüft.
|
||||||
|
|
||||||
|
Die Aktionen sind bislang nur im Kundenportal (`/geraete`) umgesetzt, nicht im internen Admin-Bereich (`/admin/geraete`) — letzterer nutzt für den Auftragskatalog bereits denselben Reuse-Pattern (`admin_geraete.py` importiert aus `routers/geraete.py`), eine spätere Erweiterung ist entsprechend günstig.
|
||||||
|
|
||||||
|
Details zum Datenmodell: `09-data-model-v1.md`.
|
||||||
@ -113,6 +113,8 @@ Die interne Geräte-ID ist unabhängig von Hardwaremerkmalen.
|
|||||||
|
|
||||||
Der Hardware-Fingerprint dient der Wiedererkennung während des Provisioning-Handshakes.
|
Der Hardware-Fingerprint dient der Wiedererkennung während des Provisioning-Handshakes.
|
||||||
|
|
||||||
|
Ein Device kann deprovisioniert (Zugriff serverseitig entzogen, aber weiterhin sichtbar) und/oder archiviert (aus der aktiven Geräteliste ausgeblendet, „gelöscht" im Sinne des Kundenportals) sein — beides unabhängig voneinander und ohne den Datensatz oder seine Historie zu entfernen (siehe ADR-0013).
|
||||||
|
|
||||||
Attribute:
|
Attribute:
|
||||||
|
|
||||||
- id
|
- id
|
||||||
@ -122,6 +124,8 @@ Attribute:
|
|||||||
- hostname
|
- hostname
|
||||||
- created_at
|
- created_at
|
||||||
- last_seen
|
- last_seen
|
||||||
|
- deprovisioned_at (optional; NULL bedeutet aktiv provisioniert)
|
||||||
|
- archived_at (optional; NULL bedeutet nicht archiviert/gelöscht)
|
||||||
|
|
||||||
Beziehungen:
|
Beziehungen:
|
||||||
|
|
||||||
@ -570,6 +574,8 @@ Spalten:
|
|||||||
- hostname
|
- hostname
|
||||||
- created_at
|
- created_at
|
||||||
- last_seen
|
- last_seen
|
||||||
|
- deprovisioned_at (nullable)
|
||||||
|
- archived_at (nullable)
|
||||||
|
|
||||||
Beziehungen:
|
Beziehungen:
|
||||||
|
|
||||||
@ -858,6 +864,8 @@ Wird eine Organisationseinheit gelöscht, die noch Unter-OEs oder zugeordnete De
|
|||||||
|
|
||||||
Wird eine Gruppe gelöscht, wird ihre Mitgliedschafts- und Merkmal-Override-Zuordnung mitgelöscht; die zugeordneten Devices selbst bleiben unberührt.
|
Wird eine Gruppe gelöscht, wird ihre Mitgliedschafts- und Merkmal-Override-Zuordnung mitgelöscht; die zugeordneten Devices selbst bleiben unberührt.
|
||||||
|
|
||||||
|
De-/Reprovisionierung und Archivierung eines Devices (siehe ADR-0013) sind reine Spaltenänderungen (`deprovisioned_at`, `archived_at`), kein Löschvorgang — sie lösen keine der hier beschriebenen Kaskaden aus, alle Beziehungen und Historie eines Devices bleiben davon unberührt.
|
||||||
|
|
||||||
Für die relationale Umsetzung gilt daher:
|
Für die relationale Umsetzung gilt daher:
|
||||||
|
|
||||||
- devices.organization_id → ON DELETE RESTRICT
|
- devices.organization_id → ON DELETE RESTRICT
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user