Files
MijnBesluit/docs/Identiteit.md
T
2026-09-27 14:21:31 +00:00

16 KiB

Wie is wie: dubbele profielen, claimen en samenvoegen

Dit document legt vast hoe MijnBesluit omgaat met de vraag "is dit dezelfde persoon?". Het is geschreven na een ontwerpgesprek op 15 september 2026, omdat de keuzes hier niet vanzelfsprekend zijn en de redenen erachter over een paar maanden niet meer uit de code te halen zijn.

Het probleem

Jonathan voegt zijn dochter Ysabeau toe als dochter. Zijn ex-partner voegt dezelfde Ysabeau toe, ook als dochter. Straks doen haar grootouders dat ook. Uiteindelijk maakt Ysabeau zelf een account en wil ze haar eigen profiel beheren — en als ze daar een foto zet, horen haar familie en vrienden die te zien.

Zonder maatregelen levert dat vier keer dezelfde persoon op.

De kern van de oplossing

Een niet-geclaimd profiel is een privé-aantekening van degene die hem maakte.

Wat Jonathan heeft aangemaakt is niet "Ysabeau", het is zijn aantekening over zijn dochter. Dat zijn ex haar eigen aantekening heeft is normaal en eerlijk. Dubbele profielen zijn vóór het claimen dus geen fout.

Het wordt pas een probleem op het moment dat Ysabeau zelf zegt: die ben ik. Daarmee verschuift de hele vraag — niet "hoe voorkom ik duplicaten bij het aanmaken", maar "hoe smelten ze samen op het moment van claimen".

Wat we bewust NIET doen

Niet zoeken op naam en geboortedatum. MyHeritage doet dat wel: je typt een naam en geboortedatum en krijgt "bedoelt u deze?". Dat werkt daar, maar het lekt. Wie mag zo'n vraag stellen? Als iemand "Ysabeau Flietstra, 12 maart 1998" intikt en het systeem zegt "die kennen we", dan weet die persoon nu dat ze hier zit. Met genoeg pogingen is dat een zoekmachine op mensen, en dat is precies wat MijnBesluit niet moet zijn.

Niet stilzwijgend koppelen op e-mailadres. (Wél vrágen; zie het hoofdstuk hieronder over de vraag bij het aanmelden. Het verschil is wie er beslist.) Dit zat er wél in, tot 15 september 2026: AddMemberAsync zocht een bestaand profiel op adres en hing dat aan je familie. Gevolg: wie het adres van iemand kende, zag meteen wat een ander over die persoon had ingevuld — naam, geboortedatum, profielfoto. Dat is dezelfde zoekmachine, via een andere deur.

De regel is nu: je hergebruikt alleen je eigen aantekeningen, nooit die van een ander. Voeg je iemand toe aan een tweede familie van jezelf, dan is dat wél hetzelfde profiel — anders zie je je eigen dochter twee keer.

Hoe het dan wel samenkomt

De uitnodiging is de match. Jonathan nodigt Ysabeau uit op haar adres, zijn ex doet dat ook, misschien op een ander adres. Ze neemt beide uitnodigingen aan. Daarmee heeft zíj gezegd dat beide profielen haar zijn — en zij is de enige die dat mag zeggen.

Concreet:

Situatie Wat er gebeurt
Eerste uitnodiging, nog geen account Er wordt een account gemaakt en het profiel wordt geclaimd. Zoals het altijd al ging.
Tweede uitnodiging, ingelogd Het profiel uit de uitnodiging gaat op in het profiel dat je al hebt.
Tweede uitnodiging, niet ingelogd, adres is bekend Geweigerd, met de uitleg dat je eerst moet inloggen. Een tweede account zou juist het probleem veroorzaken.

Dit werkt ook als beide ouders een ander e-mailadres van haar hadden: het adres speelt geen rol bij het koppelen, alleen de link die zij aanklikt.

De vraag bij het aanmelden (17 september 2026)

De uitnodiging is de match, maar hij is niet de enige weg. Wat er in de praktijk gebeurde: iemand voegt een vriend toe met zijn e-mailadres, die vriend maakt zelf een account aan, en dan staan er twee losse dingen naast elkaar die allebei over dezelfde man gaan. De vriend moet dan wachten tot er een uitnodiging komt die de ander misschien nooit verstuurt.

Automatisch koppelen op adres is daar niet het antwoord op, om twee redenen. Het lekt (zie hierboven), en een adres is geen identiteit: twee mensen kunnen er één delen - een echtpaar, een ouder die het adres van zijn kind gebruikt, een info@ - en sinds db/15 kunnen er ook meerdere aantekeningen met hetzelfde adres bestaan. Automatisch koppelen moet dan gokken welke, en een verkeerde gok maakt van twee mensen stilletjes één. Dat krijg je er niet meer uit.

Daarom vragen we het aan de enige die het antwoord weet. Wie is aangemeld, ziet bovenaan zijn overzicht staan: "X heeft een profiel voor je aangemaakt op jouw e-mailadres, als vriend. Ben jij dat?" Met twee knoppen, en dat is alles.

Antwoord Wat er gebeurt
Ja, dat ben ik Dezelfde samenvoeging als bij een uitnodiging die je ingelogd aanneemt.
Nee, dat ben ik niet Er verandert niets, en de vraag komt niet terug (persons.link_declined_by_user_id, db/17).
Niets Er verandert niets. De vraag blijft staan tot hij hem beantwoordt.

Waarom dat mag zonder dat het lekt: wie deze vraag krijgt, heeft zojuist met een code in dat postvak bewezen dat het adres van hem is. De enige die iets te zien krijgt is dus de eigenaar van het adres - en over zichzelf. Hij ziet de naam van degene die de aantekening bijhoudt en wat hij van hem zou zijn; dat is precies genoeg om de vraag te kunnen beantwoorden, en niet meer.

Andersom gebeurt er niets: degene die de aantekening maakte krijgt geen melding dat er iets is gevraagd, en geen melding dat het antwoord nee was. Anders zou het weigeren zélf een mededeling worden over wie er wel of niet een account heeft.

De vraag staat op het overzicht en niet als popup bij het aanmelden. Iemand kan je ook toevoegen nádat jij je al had aangemeld, en een popup die je wegklikt is een vraag die je nooit meer krijgt.

Er is hierdoor geen apart goedkeuringsverzoek nodig voor het toevoegen van iemand die al een account heeft. Dat stond eerst in het plan, maar het lost zichzelf op: wie nooit andermans profiel adopteert, geeft ook niets weg. Wil je die persoon er echt bij, dan nodig je uit — en die uitnodiging is de toestemming.

Wat er verhuist bij het samenvoegen

Zie SamenvoegService. Een niet-geclaimd profiel bezit weinig, want kluisitems horen bij de persoon áchter een account. Wat er wel aan hangt:

  • lidmaatschappen — staat het doel al in die familie, dan verdwijnt het dubbele lidmaatschap; de relatie die er al stond blijft leidend, want die is ingevuld door de eigenaar van die familie
  • berichten die voor het duplicaat klaarstonden of ervandaan kwamen
  • foto's waar de persoon op staat; hoogstens één blijft profielfoto
  • uitnodigingen, ook de gebruikte
  • rollen in een nalatenschap — een dubbele aanwijzing of een tweede overlijdensmelding van dezelfde gebruiker verdwijnt, anders vervalst dat het aantal
  • lege velden worden aangevuld vanuit het duplicaat, nooit overschreven: wat bij het doel staat komt van de persoon zelf en weegt zwaarder

Het duplicaat wordt niet verwijderd. Hij blijft staan met merged_into_person_id naar waar hij in is opgegaan — er hangt geschiedenis aan, en een verwijderde rij is niet terug te halen.

Twee grenzen, allebei met een controle erop:

  • Je kunt alleen samenvoegen met je eigen profiel.
  • Twee geclaimde profielen gaan nooit automatisch samen. Dat zijn twee echte accounts met ieder een eigen kluis; daar hoort een mens naar te kijken.

De profielfoto

Hier lag een echte afweging. Zet Ysabeau zelf een foto, dan is die versleuteld met háár sleutel. Haar vader zou hem dan niet kunnen zien — precies het tegenovergestelde van de bedoeling.

Gekozen: de kleine versie is niet versleuteld, de foto op ware grootte wel.

De redenering: een profielfoto is juist het ding dat je aan andere mensen laat zien. Hem behandelen als kluismateriaal is niet eerlijk over wat het is, en zou betekenen dat je voor elke kijker een sleutel moet uitdelen en weer intrekken zodra families veranderen.

De prijs is echt en bewust genomen: wie toegang heeft tot de database, ziet gezichten van 160 pixels. Alles waar het in MijnBesluit om gaat — wachtwoorden, polissen, uitvaartwensen, berichten — blijft versleuteld, net als de foto op ware grootte.

Wie hem mag zien: iedereen met wie je in een familie zit. Dat is de kring die mensen zelf samenstellen door elkaar toe te voegen en uit te nodigen. Die controle staat in VaultService en niet in de schermen: een toegangsregel hoort niet af te hangen van de vraag of elk scherm eraan denkt.

Kleine versies uit de allereerste opzet waren nog wél versleuteld. Die worden bij het eerste gebruik automatisch vervangen; daar is geen migratie voor nodig.

Waar hij staat: overal waar een mens in een lijst staat — Mijn mensen, je beheerders, de nalatenschappen die aan jou zijn toevertrouwd — en in app en website hetzelfde. Dat zit in één onderdeel, Penning.razor. Dat bestand staat twee keer op schijf (de app en de site delen geen project) en een zelfcontrole vergelijkt ze letterlijk, zodat ze niet uit elkaar lopen.

Jezelf sta je niet tussen je eigen mensen — je bent geen familielid van jezelf. Bovenaan Mijn mensen staat daarom een eigen kaart die naar Mijn profiel gaat. Daar wijzig je je naam, je geboortedatum, je foto en je e-mailadres; dat laatste met een code naar het nieuwe adres. Zonder die kaart gingen mensen hun eigen gegevens zoeken in de lijst waar ze juist niet in staan.

Het adres van een persoon is niet uniek

Dat lijkt een detail van de database, maar het is de hele regel hierboven in één index.

Op persons.email stond een UNIQUE-index. Het gevolg: voegde je iemand toe met een adres dat al ergens in de database stond - bij wie dan ook, ook bij een wildvreemde - dan liep het opslaan stuk. Voor de gebruiker was dat een knop die niets deed. Wie een adres het eerst invulde, bepaalde daarmee dat niemand anders die persoon nog kon toevoegen. In een applicatie waar twee ouders allebei hun dochter toevoegen is dat precies de verkeerde kant om.

Sinds db/15_adres_niet_uniek.sql is het een gewone index. Wat dat betekent voor de code:

  • Zoeken op adres kan meer dan één profiel opleveren. ListByEmailAsync geeft ze allemaal; FindByEmailAsync geeft er één, geclaimde eerst, zodat je in elk geval altijd dezelfde terugkrijgt.
  • Wie zijn eigen profiel zoekt, moet daarop filteren en niet op de eerste treffer. Met de eerste de beste kreeg je die van een ander te pakken, viel de eigendomscontrole uit, en maakte je een tweede aantekening aan voor iemand die je zelf al had staan.
  • Bij het wijzigen is alleen een botsing met je eigen profiel een bezwaar. Dat een ander dezelfde persoon kent, is geen fout maar het uitgangspunt.
  • Een account maken koppelt niet meer op adres. Wie zonder uitnodiging een account maakt, krijgt zijn eigen profiel; de aantekening van een ander komt bij hem terecht op het moment dat hij díens uitnodiging aanneemt. Zoeken op adres betekende andermans aantekening adopteren zonder dat die ander er iets over te zeggen had, en met meerdere aantekeningen was het bovendien de vraag welke je dan kreeg.

Bij users.email blijft uniek juist wél gelden: dat is een account, en er kan er maar één per adres zijn. Een zelfcontrole bewaakt allebei die kanten.

Wat hier nog niet is opgelost

  • Mensen zonder e-mailadres kunnen nooit claimen, dus hun duplicaten blijven bestaan. Voor een kluis en berichten maakt dat niet uit. Zodra er ooit een gedeelde stamboom komt, wordt het wel een vraag.
  • Samenvoegingen van vóór 15 september 2026 zijn niet terug te draaien. Daar is geen bonnetje van (zie hieronder); de gegevens om het mee te doen bestaan eenvoudigweg niet. Dat is geen fout die nog te repareren valt.

Samenvoegen terugdraaien

Samenvoegen was lange tijd onomkeerbaar, en dat stond hier ook als open punt. Dat is het nu niet meer.

Waarom het niet zomaar kon: ná een samenvoeging is niet meer te zien wélke lidmaatschappen, berichten en bestanden bij het duplicaat hoorden. Ze staan gewoon bij het doel, tussen alles wat daar al stond. Een gok daarop zou andermans gegevens meesleuren.

Daarom schrijft elke samenvoeging nu een bonnetje. In de tabel samenvoegingen komt per samenvoeging één regel te staan met precies wat er is verhuisd, als lijstje stappen in JSON. Terugdraaien is die lijst in omgekeerde volgorde aflopen. Regels die bij het samenvoegen zijn weggegooid — een dubbel lidmaatschap, een dubbele beheerder — staan er als volledige kopie in, want die moeten opnieuw worden aangemaakt.

Drie dingen die daarbij met opzet zo zijn:

  • Wat er ná de samenvoeging is bijgekomen, blijft staan. Wat Ysabeau daarna zelf in haar kluis heeft gelegd, is van haar en hoort bij haar. Alleen wat in het bonnetje staat gaat terug.
  • Een veld dat we hebben aangevuld gaat alleen leeg als het nog steeds is wat wij invulden. Heeft de persoon het daarna zelf aangepast, dan is het van hem en blijft het staan.
  • Terugdraaien mag alleen een beheerder. Samenvoegen doet de persoon zelf — alleen hij mag zeggen dat twee profielen hem zijn — maar het raakt ook de gegevens van degene die het duplicaat ooit aanmaakte. Gaat het mis, dan hoort er een mens naar te kijken.

Is het doelprofiel zelf later ergens in opgegaan, dan weigert het terugdraaien: het bonnetje wijst dan niet meer naar waar de gegevens staan. Die latere samenvoeging gaat eerst terug.

Het bonnetje blijft na afloop staan, met wanneer en waarom het is teruggedraaid. Dát er ooit is samengevoegd, is zelf ook geschiedenis.

De beheerder

Er is één beheerpagina, /beheer, en die is met opzet bijna leeg. Hij bestaat voor de twee situaties waar niemand anders uit komt: een overlijden dat ten onrechte is bevestigd terwijl de bezwaarlink verlopen is — het account staat dan op "overleden", inloggen kan niet meer, en er is geen weg terug — en een samenvoeging die niet had gemoeten.

Wat een beheerder wél kan:

  • een profiel opzoeken op het volledige e-mailadres
  • een bevestigd overlijden terugdraaien, met een verplichte reden die in het logboek komt
  • een samenvoeging terugdraaien, ook met een verplichte reden

Beide handelingen hebben iets gemeen dat ze veilig maakt: ze geven nooit toegang. De ene neemt toegang weg, de andere zet terug wat er stond.

Wat hij niet kan, en ook nooit moet kunnen:

  • een kluis openen. Zijn enige handeling neemt toegang wég en geeft hem nooit. Wie dat verandert, haalt de bodem onder het hele product vandaan.
  • op naam zoeken. Dat zou de zoekmachine op mensen zijn die hierboven wordt afgewezen. Je moet al weten wie je zoekt, omdat diegene contact met je heeft opgenomen.
  • iemand beheerder maken. Dat gaat met de hand in de database: UPDATE users SET is_beheerder = 1 WHERE email = '...'. Wat niet via de applicatie kan, kan ook niet via een lek in de applicatie.

Wie geen beheerder is, krijgt op die pagina "Niet gevonden" te zien — niet "geen toegang". Een pagina waarvan je niet weet dat hij bestaat, ga je ook niet proberen.

Het menu-item Beheer

In het menu staat Beheer alleen voor een beheerder. Of dat zo is, komt uit het inlogcookie (de claim mijnbesluit:beheer) en niet uit de database.

Dat is niet uit zuinigheid. Het menu zit in de layout, en die staat op élke pagina; hij wordt tegelijk met de pagina zelf opgebouwd. Vroeg de layout het op bij de database, dan liep die vraag gelijk op met de vraag van de pagina — over dezelfde DbContext, en die kan er maar één tegelijk aan. Het gevolg was A second operation was started on this context instance: een foutpagina die er soms wel en soms niet was, afhankelijk van welke vraag net eerder klaar was. Een layout hoort niets op te halen; de zelfcontrole "Een layout haalt zelf niets op" houdt dat vast.

De prijs is dat iemand die net beheerder is gemaakt het menu-item pas ziet na opnieuw inloggen. Dat is te overzien, want de claim is geen sleutel maar een lampje: /beheer en /beheer/adverteren kijken zelf nog in de database of je het écht bent, en doen dat bij elke handeling opnieuw. Wordt iemands beheerderschap ingetrokken, dan is hij dus meteen geweerd — alleen blijft het menu-item nog staan tot hij uitlogt.