Aktuelle Datenhaltung über VIN #5

Closed
opened 2026-08-27 22:13:09 +02:00 by erdbruegger · 3 comments
Owner

Derzeit hängen die Daten an der VIN, aber weniger am Benutzer. Ich halte das für problematisch. Sollte irgendwann mal ein Auto seinen Besitzer wechseln, dann würde ja die gesamte Historie der Daten an den neuen User übergehen? Ich denke, die Datensätze sollten an einer Kombi von Benutzer + VIN hängen (falls das noch nicht der fall sein sollte).
Ein Standard-API-KEY mit user rolle sollte dann natürlich auch nur seine eigenen Daten über das Backend abfragen können.

Derzeit hängen die Daten an der VIN, aber weniger am Benutzer. Ich halte das für problematisch. Sollte irgendwann mal ein Auto seinen Besitzer wechseln, dann würde ja die gesamte Historie der Daten an den neuen User übergehen? Ich denke, die Datensätze sollten an einer Kombi von Benutzer + VIN hängen (falls das noch nicht der fall sein sollte). Ein Standard-API-KEY mit user rolle sollte dann natürlich auch nur seine eigenen Daten über das Backend abfragen können.
Collaborator

Umgesetzt in 85b1d67 (release) — CI grün, deployt, live verifiziert.

Du hast recht gehabt, und zwar an der wunden Stelle: bisher hing die Telemetrie nur an der VIN, und jeder eingeloggte Nutzer sah jedes Fahrzeug. Ein API-Key mit Rolle user ebenso.

Eigentum ist jetzt ein Zeitraum, kein Flag

Der entscheidende Punkt ist der, den du selbst genannt hast — beim Besitzerwechsel darf die Historie nicht mitwandern. Eine Spalte vehicles.owner_id hätte genau das getan. Stattdessen gibt es vehicle_owners mit einer Kette halboffener Intervalle [from_ts, to_ts):

Fahrzeug 1 | alice | 2026-01-01 → 2026-07-01
Fahrzeug 1 | bob   | 2026-07-01 → offen

Ein Nutzer sieht einen Record genau dann, wenn dessen ts in eines seiner Intervalle fällt. Verkauft Alice das Auto an Bob, behält Alice ihre Fahrten und Bob fängt bei null an.

Endpunkt neu
GET /vehicles/:id/owners Halter-Historie (Halter selbst + Admin)
POST /vehicles/:id/owner Halterwechsel, Body {userId, at?} — nur Admin

Im Dashboard steckt das unter Fahrzeuge → Halter: Historie als Tabelle, und als Admin ein Formular „Neuer Halter / ab / Halterwechsel eintragen".

API-Keys: eine Lücke, die dabei aufgefallen ist

Schlüssel dürfen nur Admins anlegen, und created_by war der ausstellende Admin — ein Key mit Rolle user hätte also die Reichweite des Admins geerbt. Damit wäre dein zweiter Punkt nicht erfüllt gewesen.

api_keys.owner_id trennt das jetzt: created_by bleibt Revisionsspur, owner_id sagt, für wen der Schlüssel handelt. Bei der Erstellung:

curl -X POST "$BASE/api-keys" -H "Authorization: Bearer $ADMIN_TOKEN" \
     -H 'Content-Type: application/json' -d '{"name":"esp-alice","role":"user","userId":2}'

Mit abgesichert: die Command-Queue

Nicht ausdrücklich gefordert, aber dieselbe Lücke und schwerer als Lesen — ein fremder Nutzer konnte Befehle an dein Auto einreihen. POST /commands geht jetzt nur noch auf eigene Fahrzeuge, GET /commands zeigt nur eigene, GET /commands/:id gibt fremde nicht heraus. Befehle ohne Fahrzeug (global) sind Admins vorbehalten.

Fremdes liefert überall 404, nicht 403 — sonst verrät schon die Fehlermeldung, dass es eine Fahrzeug-ID gibt.

Sichtbarkeit auf einen Blick

Zugang sieht
Login admin alles
Login user eigene Fahrzeuge, nur eigene Besitz-Zeiträume
API-Key admin alles
API-Key user wie sein Inhaber (userId, sonst der ausstellende Admin)

Entscheidungen (proceed-with-default)

  • Bestandsdaten gehen an den dienstältesten Admin, rückwirkend ab -infinity. Damit wird durch die Umstellung keine vorhandene Telemetrie unsichtbar — auf deiner Instanz ändert sich für dich also nichts. Willst du Fahrzeuge auf konkrete Konten verteilen, geht das über die Halter-UI.
  • Neue Fahrzeuge aus dem Ingest bekommen sofort einen Halter: den Inhaber des benutzten API-Keys, beim gemeinsamen DEVICE_TOKEN den dienstältesten Admin. Sonst wäre ein frisch hochgeladenes Auto für jeden Nicht-Admin unsichtbar.
  • Wer ein Auto verkauft hat, findet es weiter in /vehicles — sonst käme er an seine eigene Historie nicht mehr heran. Er sieht davon aber nur seinen Zeitraum.
  • Ein API-Key ohne Inhaber sieht nichts statt alles. Bewusst die sichere Richtung.

Verifikation (echte Postgres-Instanz, Produktivschema, 360 Tage Telemetrie)

Szenario: Alice ab 01.01., Verkauf an Bob am 01.07.

  • Der Kernfall: Admin 360 Zeilen, Alice 182 (01.01.–30.06.), Bob 178 (01.07.–26.12.). 182 + 178 = 360, keine Überlappung, exakt an der Grenze. Die Historie wandert nicht mit.
  • latest: Alice bekommt ihren letzten Record vom 30.06., Bob den vom 26.12.
  • Carol, die das Auto nie besaß: /vehicles leer, timeseries/latest/owners und der Zugriff über die VIN je 404.
  • API-Keys: alice-key 182, bob-key 178, admin-key 360 — die Schlüssel folgen also exakt ihrem Inhaber.
  • Halterwechsel durch Alice/Carol → 403, nur Admin.
  • Commands: Bob (Halter) darf einreihen, Carol → 404, Carol ohne Fahrzeug → 403, Carol sieht 0 Befehle und Bobs Befehl per ID → 404.
  • Ingest: Fahrzeug über Alice-Key → gehört Alice; über DEVICE_TOKEN → gehört Admin.
  • Migration zweimal hintereinander auf die befüllte DB angewendet: idempotent, Daten unverändert.
  • Regression #1/#4: Filter, Sortierung, Blättern und VIN-Auflösung laufen unverändert. Selbsttests grün, Client-JS des Dashboards parst sauber. Testdatenbank wieder entfernt.
  • Live: /health ok, alle neuen Routen antworten mit 401 statt 404 (existieren also), Dashboard und Sandbox-Doku auf neuem Stand. Der Container startet nur nach erfolgreichem npm run migrate — die Migration ist auf deiner Instanz also durch.

Eine Sache, die du prüfen solltest

Admins sehen weiterhin alles, unabhängig vom Eigentum. Hat eines deiner beiden Konten die Rolle user, sieht es ab jetzt nur noch Fahrzeuge, die ihm ausdrücklich zugeordnet sind — und der Backfill hat alles dem Admin gegeben. Falls dieses Konto etwas sehen soll, trag es einmal über Fahrzeuge → Halter als Halter ein. Bei zwei Admin-Konten ändert sich für dich gar nichts.

**Umgesetzt** in `85b1d67` (release) — CI grün, deployt, live verifiziert. Du hast recht gehabt, und zwar an der wunden Stelle: bisher hing die Telemetrie nur an der VIN, und **jeder eingeloggte Nutzer sah jedes Fahrzeug**. Ein API-Key mit Rolle `user` ebenso. ### Eigentum ist jetzt ein Zeitraum, kein Flag Der entscheidende Punkt ist der, den du selbst genannt hast — beim Besitzerwechsel darf die Historie *nicht* mitwandern. Eine Spalte `vehicles.owner_id` hätte genau das getan. Stattdessen gibt es `vehicle_owners` mit einer Kette halboffener Intervalle `[from_ts, to_ts)`: ``` Fahrzeug 1 | alice | 2026-01-01 → 2026-07-01 Fahrzeug 1 | bob | 2026-07-01 → offen ``` Ein Nutzer sieht einen Record genau dann, wenn dessen `ts` in eines **seiner** Intervalle fällt. Verkauft Alice das Auto an Bob, behält Alice ihre Fahrten und Bob fängt bei null an. | Endpunkt | neu | |---|---| | `GET /vehicles/:id/owners` | Halter-Historie (Halter selbst + Admin) | | `POST /vehicles/:id/owner` | Halterwechsel, Body `{userId, at?}` — nur Admin | Im Dashboard steckt das unter **Fahrzeuge → Halter**: Historie als Tabelle, und als Admin ein Formular „Neuer Halter / ab / Halterwechsel eintragen". ### API-Keys: eine Lücke, die dabei aufgefallen ist Schlüssel dürfen nur Admins anlegen, und `created_by` war der ausstellende Admin — ein Key mit Rolle `user` hätte also die **Reichweite des Admins** geerbt. Damit wäre dein zweiter Punkt nicht erfüllt gewesen. `api_keys.owner_id` trennt das jetzt: `created_by` bleibt Revisionsspur, `owner_id` sagt, *für wen* der Schlüssel handelt. Bei der Erstellung: ```bash curl -X POST "$BASE/api-keys" -H "Authorization: Bearer $ADMIN_TOKEN" \ -H 'Content-Type: application/json' -d '{"name":"esp-alice","role":"user","userId":2}' ``` ### Mit abgesichert: die Command-Queue Nicht ausdrücklich gefordert, aber dieselbe Lücke und schwerer als Lesen — ein fremder Nutzer konnte Befehle an dein Auto einreihen. `POST /commands` geht jetzt nur noch auf eigene Fahrzeuge, `GET /commands` zeigt nur eigene, `GET /commands/:id` gibt fremde nicht heraus. Befehle ohne Fahrzeug (global) sind Admins vorbehalten. Fremdes liefert überall **404, nicht 403** — sonst verrät schon die Fehlermeldung, dass es eine Fahrzeug-ID gibt. ### Sichtbarkeit auf einen Blick | Zugang | sieht | |---|---| | Login `admin` | alles | | Login `user` | eigene Fahrzeuge, nur eigene Besitz-Zeiträume | | API-Key `admin` | alles | | API-Key `user` | wie sein Inhaber (`userId`, sonst der ausstellende Admin) | ### Entscheidungen (proceed-with-default) - **Bestandsdaten gehen an den dienstältesten Admin**, rückwirkend ab `-infinity`. Damit wird durch die Umstellung **keine vorhandene Telemetrie unsichtbar** — auf deiner Instanz ändert sich für dich also nichts. Willst du Fahrzeuge auf konkrete Konten verteilen, geht das über die Halter-UI. - **Neue Fahrzeuge aus dem Ingest** bekommen sofort einen Halter: den Inhaber des benutzten API-Keys, beim gemeinsamen `DEVICE_TOKEN` den dienstältesten Admin. Sonst wäre ein frisch hochgeladenes Auto für jeden Nicht-Admin unsichtbar. - **Wer ein Auto verkauft hat, findet es weiter in `/vehicles`** — sonst käme er an seine eigene Historie nicht mehr heran. Er sieht davon aber nur seinen Zeitraum. - **Ein API-Key ohne Inhaber sieht nichts** statt alles. Bewusst die sichere Richtung. ### Verifikation (echte Postgres-Instanz, Produktivschema, 360 Tage Telemetrie) Szenario: Alice ab 01.01., Verkauf an Bob am 01.07. - **Der Kernfall:** Admin 360 Zeilen, Alice 182 (01.01.–30.06.), Bob 178 (01.07.–26.12.). 182 + 178 = 360, keine Überlappung, exakt an der Grenze. Die Historie wandert nicht mit. - `latest`: Alice bekommt ihren letzten Record vom 30.06., Bob den vom 26.12. - Carol, die das Auto nie besaß: `/vehicles` leer, `timeseries`/`latest`/`owners` und der Zugriff über die VIN je **404**. - API-Keys: alice-key 182, bob-key 178, admin-key 360 — die Schlüssel folgen also exakt ihrem Inhaber. - Halterwechsel durch Alice/Carol → **403**, nur Admin. - Commands: Bob (Halter) darf einreihen, Carol → 404, Carol ohne Fahrzeug → 403, Carol sieht 0 Befehle und Bobs Befehl per ID → 404. - Ingest: Fahrzeug über Alice-Key → gehört Alice; über `DEVICE_TOKEN` → gehört Admin. - Migration **zweimal** hintereinander auf die befüllte DB angewendet: idempotent, Daten unverändert. - Regression #1/#4: Filter, Sortierung, Blättern und VIN-Auflösung laufen unverändert. Selbsttests grün, Client-JS des Dashboards parst sauber. Testdatenbank wieder entfernt. - Live: `/health` ok, alle neuen Routen antworten mit 401 statt 404 (existieren also), Dashboard und Sandbox-Doku auf neuem Stand. Der Container startet nur nach erfolgreichem `npm run migrate` — die Migration ist auf deiner Instanz also durch. ### Eine Sache, die du prüfen solltest Admins sehen weiterhin alles, unabhängig vom Eigentum. **Hat eines deiner beiden Konten die Rolle `user`**, sieht es ab jetzt nur noch Fahrzeuge, die ihm ausdrücklich zugeordnet sind — und der Backfill hat alles dem Admin gegeben. Falls dieses Konto etwas sehen soll, trag es einmal über **Fahrzeuge → Halter** als Halter ein. Bei zwei Admin-Konten ändert sich für dich gar nichts.
Author
Owner

Ja in unser Admin Web Dashboard sollten sich sowieso nur Admins anmelden können, keine User! Die bekommen ja noch ihre eigene App mit ihrer eigenen Ansicht und funktionalität

Ja in unser Admin Web Dashboard sollten sich sowieso nur Admins anmelden können, keine User! Die bekommen ja noch ihre eigene App mit ihrer eigenen Ansicht und funktionalität
Collaborator

Umgesetzt in 3e6e011 (release) — CI grün, deployt, live verifiziert.

Wer sich ohne Admin-Rolle anmeldet, bekommt jetzt „Dieses Dashboard ist Administratoren vorbehalten" und kommt nicht hinein. Das Token wird dabei gar nicht erst im Browser abgelegt — sonst hinge dort eine tote Sitzung. Beim Wiederherstellen einer Sitzung wird die Rolle erneut geprüft, falls ein Konto zwischenzeitlich heruntergestuft wurde. Unter dem Login steht jetzt „Admin-Oberfläche".

/auth/login bleibt bewusst offen für alle Rollen — die Nutzer-App wird sich über denselben Endpunkt anmelden, ein 403 dort hätte sie von vornherein ausgesperrt. Die Abweisung ist eine Frage der Oberfläche, nicht der Sicherheit: die Daten-Endpunkte prüfen Rolle und Eigentum ohnehin jeder für sich (die Verifikation dazu steht im vorigen Kommentar). Selbst wenn jemand das Gate im Browser umginge, bekäme er nur das, was ihm nach #5 zusteht.

Den Hinweis im Fahrzeuge-Tab habe ich passend gedreht: „Als Admin siehst du alle Fahrzeuge. Nutzer sehen in ihrer App nur die eigenen."

Verifikation

  • Gegen eine echte Instanz mit einem Admin- und einem user-Konto: der Admin kommt durch, das Nutzer-Konto wird abgewiesen — die Prüffunktion aus dem ausgelieferten Dashboard direkt gegen die echten Login-Antworten laufen lassen, nicht nachgebaut.
  • Das Nutzer-Konto bekommt weiterhin ein gültiges Token — die kommende App kann sich also anmelden.
  • Live: /health ok, Admin-Gate im ausgelieferten Dashboard vorhanden, Client-JS parst sauber, /auth/login antwortet auf falsche Zugangsdaten mit 401 (Route also offen).

Wenn die Nutzer-App so weit ist und du dafür eigene Endpunkte brauchst, mach am besten ein neues Issue auf — die Sichtbarkeitsregeln aus #5 gelten dort dann automatisch.

**Umgesetzt** in `3e6e011` (release) — CI grün, deployt, live verifiziert. Wer sich ohne Admin-Rolle anmeldet, bekommt jetzt **„Dieses Dashboard ist Administratoren vorbehalten"** und kommt nicht hinein. Das Token wird dabei gar nicht erst im Browser abgelegt — sonst hinge dort eine tote Sitzung. Beim Wiederherstellen einer Sitzung wird die Rolle erneut geprüft, falls ein Konto zwischenzeitlich heruntergestuft wurde. Unter dem Login steht jetzt „Admin-Oberfläche". **`/auth/login` bleibt bewusst offen für alle Rollen** — die Nutzer-App wird sich über denselben Endpunkt anmelden, ein 403 dort hätte sie von vornherein ausgesperrt. Die Abweisung ist eine Frage der Oberfläche, nicht der Sicherheit: die Daten-Endpunkte prüfen Rolle und Eigentum ohnehin jeder für sich (die Verifikation dazu steht im vorigen Kommentar). Selbst wenn jemand das Gate im Browser umginge, bekäme er nur das, was ihm nach #5 zusteht. Den Hinweis im Fahrzeuge-Tab habe ich passend gedreht: „Als Admin siehst du alle Fahrzeuge. Nutzer sehen in ihrer App nur die eigenen." ### Verifikation - Gegen eine echte Instanz mit einem Admin- und einem `user`-Konto: der Admin kommt durch, das Nutzer-Konto wird abgewiesen — die Prüffunktion aus dem ausgelieferten Dashboard direkt gegen die echten Login-Antworten laufen lassen, nicht nachgebaut. - Das Nutzer-Konto **bekommt weiterhin ein gültiges Token** — die kommende App kann sich also anmelden. - Live: `/health` ok, Admin-Gate im ausgelieferten Dashboard vorhanden, Client-JS parst sauber, `/auth/login` antwortet auf falsche Zugangsdaten mit 401 (Route also offen). Wenn die Nutzer-App so weit ist und du dafür eigene Endpunkte brauchst, mach am besten ein neues Issue auf — die Sichtbarkeitsregeln aus #5 gelten dort dann automatisch.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
aiways/Backend#5
No description provided.