feat(openzaak): per-document-type confidentialiteit config (WP-59)
Drives the DRC upload's vertrouwelijkheidaanduiding from a new stamdata table instead of the hardcoded "openbaar", following the existing config-as-code pattern (ADR-0004). Adds the referential-integrity check StamdataValidationTests was missing for the new table.
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
namespace BigRegister.Stamdata;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row of the document-confidentialiteit stamdata (config-as-code, ADR-0004): the ZGW
|
||||||
|
/// <c>vertrouwelijkheidaanduiding</c> to register a DRC document with, per upload category
|
||||||
|
/// (<see cref="BigRegister.Domain.Documents.DocumentCategory.CategoryId"/>). The first
|
||||||
|
/// property (<see cref="CategoryId"/>) is the table key by convention (see
|
||||||
|
/// <c>StamdataTable</c>). Non-temporal — a category's sensitivity doesn't change over time.
|
||||||
|
/// A category absent from this table falls back to <c>"openbaar"</c> (see
|
||||||
|
/// <c>OpenZaakDocumentSource</c>) rather than failing the upload.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record DocumentConfidentialiteit(string CategoryId, string Vertrouwelijkheidaanduiding);
|
||||||
@@ -14,6 +14,7 @@ public static class StamdataCatalog
|
|||||||
StamdataTable.Of<Beroep>("beroepen", "Beroepen (BIG)"),
|
StamdataTable.Of<Beroep>("beroepen", "Beroepen (BIG)"),
|
||||||
StamdataTable.Of<Opleiding>("opleidingen", "Opleidingen → beroep"),
|
StamdataTable.Of<Opleiding>("opleidingen", "Opleidingen → beroep"),
|
||||||
StamdataTable.Of<Specialisme>("specialismen", "Specialismen → beroep"),
|
StamdataTable.Of<Specialisme>("specialismen", "Specialismen → beroep"),
|
||||||
|
StamdataTable.Of<DocumentConfidentialiteit>("documentconfidentialiteit", "Documenttype → vertrouwelijkheidaanduiding"),
|
||||||
// PolicyQuestions and future tables migrate here, same one-liner each.
|
// PolicyQuestions and future tables migrate here, same one-liner each.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{ "categoryId": "identiteit", "vertrouwelijkheidaanduiding": "vertrouwelijk" },
|
||||||
|
{ "categoryId": "diploma", "vertrouwelijkheidaanduiding": "openbaar" },
|
||||||
|
{ "categoryId": "taalvaardigheid", "vertrouwelijkheidaanduiding": "openbaar" },
|
||||||
|
{ "categoryId": "werkervaring", "vertrouwelijkheidaanduiding": "openbaar" },
|
||||||
|
{ "categoryId": "nascholing", "vertrouwelijkheidaanduiding": "openbaar" }
|
||||||
|
]
|
||||||
@@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
|
|||||||
using BigRegister.Api.Contracts;
|
using BigRegister.Api.Contracts;
|
||||||
using BigRegister.Api.Data;
|
using BigRegister.Api.Data;
|
||||||
using BigRegister.Domain.Authorization;
|
using BigRegister.Domain.Authorization;
|
||||||
|
using BigRegister.Stamdata;
|
||||||
|
|
||||||
namespace BigRegister.Api.Zgw;
|
namespace BigRegister.Api.Zgw;
|
||||||
|
|
||||||
@@ -23,6 +24,15 @@ public sealed class OpenZaakDocumentSource(HttpClient http, ZgwTokenProvider tok
|
|||||||
{
|
{
|
||||||
private readonly ZgwHttpClient zgw = new(http, tokens);
|
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<string, string> ConfidentialiteitByCategory =
|
||||||
|
StamdataFile.Load<DocumentConfidentialiteit>("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
|
// ponytail: sync-over-async — IDocumentSource is sync to match the local store + the
|
||||||
// existing sync upload/submit endpoints, same reasoning as OpenZaakZaakSource.
|
// existing sync upload/submit endpoints, same reasoning as OpenZaakZaakSource.
|
||||||
public UploadResponse Upload(
|
public UploadResponse Upload(
|
||||||
@@ -52,10 +62,7 @@ public sealed class OpenZaakDocumentSource(HttpClient http, ZgwTokenProvider tok
|
|||||||
Inhoud: Convert.ToBase64String(content),
|
Inhoud: Convert.ToBase64String(content),
|
||||||
Informatieobjecttype: informatieobjecttypeUrl,
|
Informatieobjecttype: informatieobjecttypeUrl,
|
||||||
Identificatie: doc.DocumentId,
|
Identificatie: doc.DocumentId,
|
||||||
// ponytail: hardcoded "openbaar" (public) — real usage would likely vary the
|
Vertrouwelijkheidaanduiding: ConfidentialiteitFor(categoryId)), caller);
|
||||||
// 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);
|
|
||||||
|
|
||||||
DocumentStore.SetDrcUrl(doc.DocumentId, eio.Url);
|
DocumentStore.SetDrcUrl(doc.DocumentId, eio.Url);
|
||||||
return new UploadResponse(doc.DocumentId, doc.LocalId);
|
return new UploadResponse(doc.DocumentId, doc.LocalId);
|
||||||
|
|||||||
@@ -58,6 +58,23 @@ public class OpenZaakDocumentSourceTests
|
|||||||
Assert.Contains("123443210", body); // bronorganisatie
|
Assert.Contains("123443210", body); // bronorganisatie
|
||||||
Assert.Contains("paspoort.pdf", body);
|
Assert.Contains("paspoort.pdf", body);
|
||||||
Assert.Contains(Convert.ToBase64String("%PDF-1.4 fake"u8.ToArray()), body); // inhoud
|
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]
|
[Fact]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using BigRegister.Api.Data;
|
using BigRegister.Api.Data;
|
||||||
using BigRegister.Domain.Diplomas;
|
using BigRegister.Domain.Diplomas;
|
||||||
|
using BigRegister.Domain.Documents;
|
||||||
using BigRegister.Stamdata;
|
using BigRegister.Stamdata;
|
||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
@@ -22,6 +23,14 @@ public class StamdataValidationTests
|
|||||||
private static readonly IReadOnlySet<string> BeroepCodes =
|
private static readonly IReadOnlySet<string> BeroepCodes =
|
||||||
StamdataFile.Load<Beroep>("beroepen").Select(b => b.Code).ToHashSet(StringComparer.Ordinal);
|
StamdataFile.Load<Beroep>("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<string> DocumentCategoryIds = new[] { "registratie", "herregistratie", "org-template" }
|
||||||
|
.SelectMany(DocumentRules.AllCategoriesFor)
|
||||||
|
.Select(c => c.CategoryId)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
private static readonly IReadOnlyList<StamdataRef> References = new[]
|
private static readonly IReadOnlyList<StamdataRef> References = new[]
|
||||||
{
|
{
|
||||||
new StamdataRef(
|
new StamdataRef(
|
||||||
@@ -38,6 +47,12 @@ public class StamdataValidationTests
|
|||||||
"Specialisme.beroep → beroepen.code",
|
"Specialisme.beroep → beroepen.code",
|
||||||
StamdataFile.Load<Specialisme>("specialismen").Select(s => s.Beroep),
|
StamdataFile.Load<Specialisme>("specialismen").Select(s => s.Beroep),
|
||||||
key => BeroepCodes.Contains(key)),
|
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>("documentconfidentialiteit").Select(d => d.CategoryId),
|
||||||
|
key => DocumentCategoryIds.Contains(key)),
|
||||||
};
|
};
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -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-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-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-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-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-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 |
|
| [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | todo |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WP-59 — Per-document-type confidentialiteit config
|
# WP-59 — Per-document-type confidentialiteit config
|
||||||
|
|
||||||
Status: todo
|
Status: done
|
||||||
Phase: 10 — OpenZaak production hardening
|
Phase: 10 — OpenZaak production hardening
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
@@ -43,17 +43,35 @@ this slice is "apply the existing pattern," not invent a new one.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Confidentiality level for a real upload varies by document type per the new
|
- [x] Confidentiality level for a real upload varies by document type per the new
|
||||||
stamdata table.
|
stamdata table (`identiteit` → `vertrouwelijk`; everything else → `openbaar`).
|
||||||
- [ ] `StamdataValidationTests` cover the new table (a bad edit fails CI, per ADR-0004).
|
- [x] `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
|
- [x] `/beheer/stamdata` can edit the new table without a code change (existing generic
|
||||||
editor).
|
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
|
## Verification
|
||||||
|
|
||||||
`cd backend && dotnet test`; manual: `/beheer/stamdata` shows and edits the new table; an
|
`cd backend && dotnet test` (161/161 green, incl. the 2 new `OpenZaakDocumentSourceTests`
|
||||||
upload for a mapped document type carries the mapped confidentiality level (test
|
+ the new `StamdataValidationTests` reference entry); `dotnet format --verify-no-changes`
|
||||||
asserted).
|
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
|
## Out of scope
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user