# RB-03 — mask the owner BSN on the cross-owner case lists Status: **implemented** · 2026-08-27 · Source findings: `07-bio2-compliance.md` BIO-003 · `99-backlog.md` RB-03 ## What was wrong `Mappers.ToAdminSummaryDto` set `Owner = a.Owner` — the raw BSN. Two endpoints consume it, both cross-owner lists read by someone who is **not** the subject: - `GET /admin/cases` (`cases:manage`) - `GET /werkvoorraad` (`aanvraag:beoordelen`) `GET /beoordeling/{id}` — the _detail_ view of the same data — already masked. So the detail screen showed `******782` while the list one click earlier showed the whole BSN. ## What changed | File | Change | | ---------------------- | ---------------------------------------------------------------- | | `Domain/People/Pii.cs` | **new** — `Pii.MaskTail`, moved out of `Program.cs` | | `Contracts/Mappers.cs` | `Owner = Pii.MaskTail(a.Owner, 3)` | | `Program.cs` | local `MaskTail` deleted; two call sites point at `Pii.MaskTail` | | `AdminCasesTests.cs` | asserts the masked value and that `DemoOwner` does not appear | | `WerkvoorraadTests.cs` | same assertion, replacing the `IsNullOrEmpty` one | **Masked in the mapper, not at the endpoints.** The point of the ticket is that both lists _inherit_ it, so a third cross-owner list cannot be added that forgets to mask. **`MaskTail` moved to `Domain/People/Pii.cs`** because it now has three callers across three folders (`Contracts`, `Program.cs`, and `Data` once **RB-04** lands), and a second hand-rolled copy is how one of them drifts into leaking. It is documented as idempotent, which is what lets `/beoordeling/{id}` keep its own call: `IZaakSource` has a second implementation (`OpenZaakZaakSource` → `ZgwZaakMapper`, which maps `Owner` from the zaak `identificatie`), so that endpoint's guarantee should not depend on which source answered. ## Blast radius on the frontend — none Both consumers use the value for display only (`admin-cases.page.ts:101`, `beoordeling-view.ts:40`, `werkvoorraad-item-view.ts:28`); the `parse*` boundaries require a non-empty string, which a masked BSN still is. Nothing keys, filters or looks up by owner. ## Verification `dotnet format --verify-no-changes` clean. `dotnet test`: **251 passed, 1 failed** — the pre-existing `OpenZaakIntegrationTests.Admin_cases_…`, which needs a live container.