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