diff --git a/backend/src/BigRegister.Api/Stamdata/DocumentConfidentialiteit.cs b/backend/src/BigRegister.Api/Stamdata/DocumentConfidentialiteit.cs new file mode 100644 index 0000000..dbed90a --- /dev/null +++ b/backend/src/BigRegister.Api/Stamdata/DocumentConfidentialiteit.cs @@ -0,0 +1,12 @@ +namespace BigRegister.Stamdata; + +/// +/// One row of the document-confidentialiteit stamdata (config-as-code, ADR-0004): the ZGW +/// vertrouwelijkheidaanduiding to register a DRC document with, per upload category +/// (). The first +/// property () is the table key by convention (see +/// StamdataTable). Non-temporal — a category's sensitivity doesn't change over time. +/// A category absent from this table falls back to "openbaar" (see +/// OpenZaakDocumentSource) rather than failing the upload. +/// +public sealed record DocumentConfidentialiteit(string CategoryId, string Vertrouwelijkheidaanduiding); diff --git a/backend/src/BigRegister.Api/Stamdata/StamdataCatalog.cs b/backend/src/BigRegister.Api/Stamdata/StamdataCatalog.cs index 7b73de0..57db6bf 100644 --- a/backend/src/BigRegister.Api/Stamdata/StamdataCatalog.cs +++ b/backend/src/BigRegister.Api/Stamdata/StamdataCatalog.cs @@ -14,6 +14,7 @@ public static class StamdataCatalog StamdataTable.Of("beroepen", "Beroepen (BIG)"), StamdataTable.Of("opleidingen", "Opleidingen → beroep"), StamdataTable.Of("specialismen", "Specialismen → beroep"), + StamdataTable.Of("documentconfidentialiteit", "Documenttype → vertrouwelijkheidaanduiding"), // PolicyQuestions and future tables migrate here, same one-liner each. }; diff --git a/backend/src/BigRegister.Api/Stamdata/documentconfidentialiteit.json b/backend/src/BigRegister.Api/Stamdata/documentconfidentialiteit.json new file mode 100644 index 0000000..18aed4c --- /dev/null +++ b/backend/src/BigRegister.Api/Stamdata/documentconfidentialiteit.json @@ -0,0 +1,7 @@ +[ + { "categoryId": "identiteit", "vertrouwelijkheidaanduiding": "vertrouwelijk" }, + { "categoryId": "diploma", "vertrouwelijkheidaanduiding": "openbaar" }, + { "categoryId": "taalvaardigheid", "vertrouwelijkheidaanduiding": "openbaar" }, + { "categoryId": "werkervaring", "vertrouwelijkheidaanduiding": "openbaar" }, + { "categoryId": "nascholing", "vertrouwelijkheidaanduiding": "openbaar" } +] diff --git a/backend/src/BigRegister.Api/Zgw/OpenZaakDocumentSource.cs b/backend/src/BigRegister.Api/Zgw/OpenZaakDocumentSource.cs index 8cd6afe..6a8f63d 100644 --- a/backend/src/BigRegister.Api/Zgw/OpenZaakDocumentSource.cs +++ b/backend/src/BigRegister.Api/Zgw/OpenZaakDocumentSource.cs @@ -3,6 +3,7 @@ using System.Text.Json.Serialization; using BigRegister.Api.Contracts; using BigRegister.Api.Data; using BigRegister.Domain.Authorization; +using BigRegister.Stamdata; namespace BigRegister.Api.Zgw; @@ -23,6 +24,15 @@ public sealed class OpenZaakDocumentSource(HttpClient http, ZgwTokenProvider tok { private readonly ZgwHttpClient zgw = new(http, tokens); + // WP-59: per-document-type confidentiality (stamdata, ADR-0004) — "openbaar" if the + // category isn't in the table, so an unconfigured category never fails the upload. + private static readonly IReadOnlyDictionary ConfidentialiteitByCategory = + StamdataFile.Load("documentconfidentialiteit") + .ToDictionary(r => r.CategoryId, r => r.Vertrouwelijkheidaanduiding); + + private static string ConfidentialiteitFor(string categoryId) => + ConfidentialiteitByCategory.GetValueOrDefault(categoryId, "openbaar"); + // ponytail: sync-over-async — IDocumentSource is sync to match the local store + the // existing sync upload/submit endpoints, same reasoning as OpenZaakZaakSource. public UploadResponse Upload( @@ -52,10 +62,7 @@ public sealed class OpenZaakDocumentSource(HttpClient http, ZgwTokenProvider tok Inhoud: Convert.ToBase64String(content), Informatieobjecttype: informatieobjecttypeUrl, Identificatie: doc.DocumentId, - // ponytail: hardcoded "openbaar" (public) — real usage would likely vary the - // confidentiality level per category (e.g. an identity document is more sensitive - // than a diploma); a fixed value is enough to prove the seam end-to-end. - Vertrouwelijkheidaanduiding: "openbaar"), caller); + Vertrouwelijkheidaanduiding: ConfidentialiteitFor(categoryId)), caller); DocumentStore.SetDrcUrl(doc.DocumentId, eio.Url); return new UploadResponse(doc.DocumentId, doc.LocalId); diff --git a/backend/tests/BigRegister.Tests/OpenZaakDocumentSourceTests.cs b/backend/tests/BigRegister.Tests/OpenZaakDocumentSourceTests.cs index 9e9614e..9960ce4 100644 --- a/backend/tests/BigRegister.Tests/OpenZaakDocumentSourceTests.cs +++ b/backend/tests/BigRegister.Tests/OpenZaakDocumentSourceTests.cs @@ -58,6 +58,23 @@ public class OpenZaakDocumentSourceTests Assert.Contains("123443210", body); // bronorganisatie Assert.Contains("paspoort.pdf", body); Assert.Contains(Convert.ToBase64String("%PDF-1.4 fake"u8.ToArray()), body); // inhoud + // WP-59: "identiteit" is mapped to "vertrouwelijk" in the confidentialiteit stamdata. + Assert.Contains("\"vertrouwelijkheidaanduiding\":\"vertrouwelijk\"", body); + } + + [Fact] + public void Upload_falls_back_to_openbaar_for_a_category_absent_from_the_confidentialiteit_table() + { + var options = Options(); + options.InformatieobjecttypeUrls["org-logo"] = InformatieobjecttypeUrl; + var handler = new ZgwStubHandler(url => + """{ "url": "https://oz.example/documenten/api/v1/enkelvoudiginformatieobjecten/eio-2" }"""); + var source = new OpenZaakDocumentSource(new HttpClient(handler), new ZgwTokenProvider(options), options); + + source.Upload("local-2", "org-logo", "org-template", "logo.png", "image/png", [1, 2, 3], Caller); + + var body = handler.BodyOf($"{DrcBase}/enkelvoudiginformatieobjecten"); + Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", body); } [Fact] diff --git a/backend/tests/BigRegister.Tests/StamdataValidationTests.cs b/backend/tests/BigRegister.Tests/StamdataValidationTests.cs index 9ffd4b2..715c150 100644 --- a/backend/tests/BigRegister.Tests/StamdataValidationTests.cs +++ b/backend/tests/BigRegister.Tests/StamdataValidationTests.cs @@ -1,5 +1,6 @@ using BigRegister.Api.Data; using BigRegister.Domain.Diplomas; +using BigRegister.Domain.Documents; using BigRegister.Stamdata; namespace BigRegister.Tests; @@ -22,6 +23,14 @@ public class StamdataValidationTests private static readonly IReadOnlySet BeroepCodes = StamdataFile.Load("beroepen").Select(b => b.Code).ToHashSet(StringComparer.Ordinal); + // Every document category id that exists across any wizard (WP-59's confidentialiteit + // table points at these) — "org-logo" resolves too, even though it's deliberately absent + // from the confidentialiteit table itself (falls back to "openbaar"). + private static readonly IReadOnlySet DocumentCategoryIds = new[] { "registratie", "herregistratie", "org-template" } + .SelectMany(DocumentRules.AllCategoriesFor) + .Select(c => c.CategoryId) + .ToHashSet(StringComparer.Ordinal); + private static readonly IReadOnlyList References = new[] { new StamdataRef( @@ -38,6 +47,12 @@ public class StamdataValidationTests "Specialisme.beroep → beroepen.code", StamdataFile.Load("specialismen").Select(s => s.Beroep), key => BeroepCodes.Contains(key)), + // WP-59: a confidentialiteit row for a category that no wizard ever asks for is dead + // config — fail the build rather than let it silently rot. + new StamdataRef( + "DocumentConfidentialiteit.CategoryId → a real document category", + StamdataFile.Load("documentconfidentialiteit").Select(d => d.CategoryId), + key => DocumentCategoryIds.Contains(key)), }; [Fact] diff --git a/docs/project/backlog/README.md b/docs/project/backlog/README.md index 4eae109..f469c69 100644 --- a/docs/project/backlog/README.md +++ b/docs/project/backlog/README.md @@ -109,7 +109,7 @@ for its existing violations, so every WP ends green. | [WP-56](WP-56-openzaak-catalogus-provisioning.md) | Idempotent catalogus provisioning | 10 · OpenZaak hardening | done | | [WP-57](WP-57-openzaak-least-privilege-scopes.md) | Least-privilege client scopes | 10 · OpenZaak hardening | done | | [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | done | -| [WP-59](WP-59-document-confidentialiteit-config.md) | Per-document-type confidentialiteit config | 10 · OpenZaak hardening | todo | +| [WP-59](WP-59-document-confidentialiteit-config.md) | Per-document-type confidentialiteit config | 10 · OpenZaak hardening | done | | [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | todo | | [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | todo | | [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | todo | diff --git a/docs/project/backlog/WP-59-document-confidentialiteit-config.md b/docs/project/backlog/WP-59-document-confidentialiteit-config.md index 6023c21..6988048 100644 --- a/docs/project/backlog/WP-59-document-confidentialiteit-config.md +++ b/docs/project/backlog/WP-59-document-confidentialiteit-config.md @@ -1,6 +1,6 @@ # WP-59 — Per-document-type confidentialiteit config -Status: todo +Status: done Phase: 10 — OpenZaak production hardening ## Why @@ -43,17 +43,35 @@ this slice is "apply the existing pattern," not invent a new one. ## Acceptance criteria -- [ ] Confidentiality level for a real upload varies by document type per the new - stamdata table. -- [ ] `StamdataValidationTests` cover the new table (a bad edit fails CI, per ADR-0004). -- [ ] `/beheer/stamdata` can edit the new table without a code change (existing generic - editor). +- [x] Confidentiality level for a real upload varies by document type per the new + stamdata table (`identiteit` → `vertrouwelijk`; everything else → `openbaar`). +- [x] `StamdataValidationTests` cover the new table (a bad edit fails CI, per ADR-0004). +- [x] `/beheer/stamdata` can edit the new table without a code change (existing generic + editor — the `StamdataCatalog` registration is the only wiring needed). + +## What actually happened + +Implemented mostly as planned — one gap found and closed: the diff as first written +registered `DocumentConfidentialiteit` in `StamdataCatalog` and wired the lookup into +`OpenZaakDocumentSource`, plus a positive test (`identiteit` → `vertrouwelijk`) and a +fallback test (an unmapped category, `org-logo`, → `openbaar`), but had **no** +`StamdataValidationTests` reference-integrity entry for the new table — the second +acceptance box was unchecked. Added one: a `StamdataRef` resolving every +`documentconfidentialiteit.json` `categoryId` against the real set of document category +ids (`DocumentRules.AllCategoriesFor` across `registratie`/`herregistratie`/`org-template`), +so a typo'd or stale `categoryId` now fails the build instead of silently never matching +(`OpenZaakDocumentSource.ConfidentialiteitFor`'s dictionary lookup would otherwise just +fall back to `"openbaar"` forever with no signal). `org-logo` deliberately stays absent +from the confidentialiteit table (falls back to `"openbaar"`) and correctly still +resolves as a known category — the reference check validates "is this a real category", +not "must every category be configured." ## Verification -`cd backend && dotnet test`; manual: `/beheer/stamdata` shows and edits the new table; an -upload for a mapped document type carries the mapped confidentiality level (test -asserted). +`cd backend && dotnet test` (161/161 green, incl. the 2 new `OpenZaakDocumentSourceTests` ++ the new `StamdataValidationTests` reference entry); `dotnet format --verify-no-changes` +clean. Manual: `/beheer/stamdata` shows and edits the new table; an upload for a mapped +document type carries the mapped confidentiality level (test asserted). ## Out of scope