← cd ../
// notities

Multi-tenant zorgsoftware op Supabase: Row Level Security, versleutelde BSN's en een inzagelog

Hoe Zorg Intelligence zorgorganisaties gescheiden houdt in één Postgres-database, het BSN versleuteld opslaat en vastlegt wie welk cliëntdossier heeft geopend.

gepubliceerd
auteur
Nicky Mitnick

Het probleem

Zorg Intelligence is zorgadministratie voor zzp’ers en kleine zorgaanbieders in Nederland. Alle organisaties delen één applicatie en één database. De gegevens zijn gevoelig: cliëntdossiers, zorgplannen en het BSN van cliënten en medewerkers.

Dat levert drie eisen op die niet mogen afhangen van applicatiecode die overal klopt. De ene organisatie mag nooit rijen van de andere zien. Een BSN mag niet als platte tekst in de database staan. En de vraag “wie heeft dit dossier geopend?” moet achteraf te beantwoorden zijn.

De aanpak

De stack is Next.js met Supabase (Postgres, Auth, Row Level Security en Storage). De regels staan in de database, zodat een lijst, een zoekopdracht, een rapport en een CSV-export allemaal door dezelfde poort gaan.

Row Level Security. Op elke tabel in het publieke schema staat RLS aan, en elke rij hoort bij precies één organisatie. Een gebruiker hangt aan een organisatie via een lidmaatschap met één van vier rollen: eigenaar, beheerder, medewerker of lezer. De policies herhalen die logica niet. Ze roepen een paar kleine hulpfuncties aan (is deze gebruiker lid, heeft hij een van deze rollen, mag hij schrijven) die de lidmaatschappen van de ingelogde gebruiker lezen. Een lezer schrijft niets.

Details die ertoe bleken te doen:

  • Verwijzingen naar een tabel met een organisatiekolom zijn samengestelde sleutels. Een verwijzing kan dus niet naar een rij van een andere organisatie wijzen.
  • In policies staat de aanroep die de huidige gebruiker teruggeeft in een subquery. Postgres rekent hem dan één keer per query uit in plaats van één keer per rij.
  • Een organisatie kan caseload-afscherming aanzetten. Medewerkers en lezers zien dan alleen de cliënten die aan hen zijn toegewezen. Omdat dat in RLS zit, geldt het zonder extra code voor elk scherm en elke export.
  • De platformbeheerder (Roosterspecialisten) ziet geen zorgdata. Meekijken voor support kan alleen met tijdelijke toestemming van de organisatie zelf. Zo’n sessie is alleen-lezen, beperkt in tijd en wordt gelogd.

Om te voorkomen dat dit ongemerkt afbrokkelt, staat er in de repository een vingerafdruk van alle policies en functies met een test die daartegen vergelijkt, plus een herhaalbare isolatietest met 15 scenario’s.

Versleuteld BSN. Het BSN wordt per veld versleuteld met pgcrypto. De sleutel komt uit Supabase Vault en staat niet in de code of in git. Een trigger versleutelt de waarde bij invoegen en wijzigen, bewaart de laatste vier cijfers voor een gemaskeerde weergave en maakt de kolom met platte tekst leeg. De applicatie krijgt het volledige nummer nooit uit een gewone query. Ze moet één functie aanroepen die het lidmaatschap controleert, eerst de inzageregel schrijft en pas daarna ontsleutelt.

Inzagelog. De eerste versie schreef een regel “inzage” in het algemene mutatielog. In het huidige schema is het een eigen tabel met het tijdstip (van de database), de organisatie, de persoon, de rol op dat moment, de cliënt, het soort object, de handeling, een aantal bij exports en zoekopdrachten, en de sessie. Er staat nooit vrije tekst, een zoekterm, een bestandsnaam of een IP-adres in.

Er is één weg naar binnen: een functie die zelf controleert of de aanroeper het record mag zien, en die de organisatie en de cliënt uit het record afleidt in plaats van uit de parameters. Regels zijn niet te wijzigen of te verwijderen, ook niet met de service-role. De enige uitzondering is de opschoning na de bewaartermijn van 5 jaar. Het log faalt dicht: kan een regel niet worden geschreven, dan toont de app niets, en bij een BSN, een download of een export geeft de server pas gegevens terug als de regel er staat. Eigenaren en beheerders lezen alle regels van hun organisatie, medewerkers alleen hun eigen regels.

De afwegingen

  • De app logt, de database controleert. Wie met een eigen geldig token rechtstreeks de database-API leest, krijgt nog steeds alleen de rijen die RLS toestaat, maar laat geen inzageregel achter. Het BSN is de uitzondering, want dat loopt altijd via de databasefunctie. De cliëntenlijst zelf (namen) wordt niet gelogd.
  • Nog geen kopie van het log buiten de database. Dat is een open punt.
  • Hulpfuncties draaien met verhoogde rechten, anders kan een policy de lidmaatschappen niet lezen. Ze zijn stuk voor stuk klein en staan in de vingerafdruk.
  • Tweestapsverificatie is standaard optioneel. Elke gebruiker kan een authenticator-app aanzetten, en een eigenaar kan het voor de hele organisatie verplicht stellen. Is het verplicht, dan is meekijken door support niet mogelijk.
  • Geen certificaat. Dit zijn technische maatregelen. Het product is niet NEN 7510-gecertificeerd en een externe penetratietest moet nog gebeuren.
  • Policies op elke tabel kosten tijd per migratie: elke nieuwe tabel vraagt om policies, een proefrun en een bijgewerkte vingerafdruk.

Meer over het product staat op de projectpagina van Zorg Intelligence.