Das Problem
Zorg Intelligence ist Verwaltungssoftware für selbstständige Pflegekräfte und kleine Pflegeanbieter in den Niederlanden. Alle Organisationen teilen sich eine Anwendung und eine Datenbank. Die Daten sind sensibel: Klientenakten, Pflegepläne und die Bürgerservicenummer (BSN) von Klienten und Mitarbeitenden.
Daraus ergeben sich drei Anforderungen, die nicht davon abhängen dürfen, dass der Anwendungscode überall stimmt. Eine Organisation darf nie Zeilen einer anderen sehen. Eine BSN darf nicht im Klartext in der Datenbank stehen. Und die Frage “Wer hat diese Akte geöffnet?” muss sich im Nachhinein beantworten lassen.
Der Ansatz
Der Stack ist Next.js mit Supabase (Postgres, Auth, Row Level Security und Storage). Die Regeln liegen in der Datenbank, sodass eine Liste, eine Suche, ein Bericht und ein CSV-Export alle durch dieselbe Schleuse gehen.
Row Level Security. Auf jeder Tabelle im öffentlichen Schema ist RLS aktiv, und jede Zeile gehört zu genau einer Organisation. Ein Nutzer hängt über eine Mitgliedschaft mit einer von vier Rollen an einer Organisation: Eigentümer, Verwalter, Mitarbeiter oder Leser. Die Policies wiederholen diese Logik nicht. Sie rufen ein paar kleine Hilfsfunktionen auf (ist dieser Nutzer Mitglied, hat er eine dieser Rollen, darf er schreiben), die die Mitgliedschaften des angemeldeten Nutzers lesen. Ein Leser schreibt nichts.
Details, die sich als wichtig erwiesen haben:
- Fremdschlüssel auf eine Tabelle mit Organisationsspalte sind zusammengesetzt. Ein Verweis kann also nicht auf eine Zeile einer anderen Organisation zeigen.
- In Policies steht der Aufruf, der den aktuellen Nutzer liefert, in einer Unterabfrage. Postgres berechnet ihn dann einmal pro Abfrage statt einmal pro Zeile.
- Eine Organisation kann die Beschränkung auf die eigene Caseload einschalten. Mitarbeiter und Leser sehen dann nur die Klienten, die ihnen zugewiesen sind. Weil das in RLS liegt, gilt es ohne zusätzlichen Code für jeden Bildschirm und jeden Export.
- Der Plattformbetreiber (Roosterspecialisten) sieht keine Pflegedaten. Mitschauen für den Support geht nur mit befristeter Zustimmung der Organisation selbst. Eine solche Sitzung ist nur lesend, zeitlich begrenzt und wird protokolliert.
Damit das nicht unbemerkt bröckelt, liegt im Repository ein Fingerabdruck aller Policies und Funktionen mit einem Test, der dagegen vergleicht, dazu ein wiederholbarer Isolationstest mit 15 Szenarien.
Verschlüsselte BSN. Die BSN wird pro Feld mit pgcrypto verschlüsselt. Der Schlüssel kommt aus Supabase Vault und steht weder im Code noch in git. Ein Trigger verschlüsselt den Wert beim Einfügen und Ändern, behält die letzten vier Ziffern für eine maskierte Anzeige und leert die Klartextspalte. Die Anwendung bekommt die vollständige Nummer nie aus einer normalen Abfrage. Sie muss eine Funktion aufrufen, die die Mitgliedschaft prüft, zuerst die Protokollzeile schreibt und erst dann entschlüsselt.
Zugriffsprotokoll. Die erste Version schrieb eine Zeile “Einsicht” in das allgemeine Änderungsprotokoll. Im aktuellen Schema ist es eine eigene Tabelle mit Zeitpunkt (aus der Datenbank), Organisation, Person, Rolle zu diesem Zeitpunkt, Klient, Art des Objekts, Handlung, einer Anzahl bei Exporten und Suchen sowie der Sitzung. Freitext, ein Suchbegriff, ein Dateiname oder eine IP-Adresse stehen dort nie.
Es gibt einen einzigen Weg hinein: eine Funktion, die selbst prüft, ob der Aufrufer den Datensatz sehen darf, und die Organisation und Klient aus dem Datensatz ableitet statt aus ihren Parametern. Zeilen lassen sich weder ändern noch löschen, auch nicht mit der Service-Rolle. Die einzige Ausnahme ist die Bereinigung nach der Aufbewahrungsfrist von 5 Jahren. Das Protokoll schlägt geschlossen fehl: Kann eine Zeile nicht geschrieben werden, zeigt die App nichts an, und bei einer BSN, einem Download oder einem Export gibt der Server erst Daten zurück, wenn die Zeile existiert. Eigentümer und Verwalter lesen alle Zeilen ihrer Organisation, Mitarbeiter nur ihre eigenen.
Die Abwägungen
- Die App protokolliert, die Datenbank prüft. Wer mit einem eigenen gültigen Token direkt die Datenbank-API liest, bekommt weiterhin nur die Zeilen, die RLS erlaubt, hinterlässt aber keine Protokollzeile. Die BSN ist die Ausnahme, weil sie immer über die Datenbankfunktion läuft. Die Klientenliste selbst (Namen) wird nicht protokolliert.
- Noch keine Kopie des Protokolls außerhalb der Datenbank. Das ist ein offener Punkt.
- Hilfsfunktionen laufen mit erhöhten Rechten, sonst könnte eine Policy die Mitgliedschaften nicht lesen. Jede ist klein und im Fingerabdruck aufgeführt.
- Zwei-Faktor-Anmeldung ist standardmäßig freiwillig. Jeder Nutzer kann eine Authenticator-App einschalten, und ein Eigentümer kann sie für die ganze Organisation zur Pflicht machen. Ist sie Pflicht, sind Support-Sitzungen nicht möglich.
- Kein Zertifikat. Das sind technische Maßnahmen. Das Produkt ist nicht nach NEN 7510 zertifiziert, und ein externer Penetrationstest steht noch aus.
- Policies auf jeder Tabelle kosten Zeit pro Migration: Jede neue Tabelle braucht Policies, einen Probelauf und einen aktualisierten Fingerabdruck.
Mehr zum Produkt steht auf der Projektseite von Zorg Intelligence.