feat(domain): refuse non-PDF, infected or unscannable diplomas before storing (refs #192)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
not
2026-10-02 09:53:23 +02:00
co-authored by Claude Opus 5.5
parent eaef19747b
commit 764ae6e1be
@@ -31,7 +31,7 @@ public enum ProvideDocumentsOutcome
/// <summary> /// <summary>
/// The provide-documents use case (S-10a/S-10b): a zorgprofessional uploads the diploma their /// The provide-documents use case (S-10a/S-10b): a zorgprofessional uploads the diploma their
/// registration is parked waiting for. The document is stored in ZGW via the ACL (§8.1), then the /// registration is parked waiting for. Only a PDF that scans clean is accepted (S-29, ADR-0036). The document is stored in ZGW via the ACL (§8.1), then the
/// WachtOpDocumenten task is completed so the registratie process leaves the 30-day wait and continues /// WachtOpDocumenten task is completed so the registratie process leaves the 30-day wait and continues
/// to beoordeling (ADR-0017). Owner-scoped by bsn. Both steps are best-effort about missing preconditions /// to beoordeling (ADR-0017). Owner-scoped by bsn. Both steps are best-effort about missing preconditions
/// (mirroring <see cref="WithdrawRegistration"/>): storage needs an opened zaak, and completion needs a /// (mirroring <see cref="WithdrawRegistration"/>): storage needs an opened zaak, and completion needs a
@@ -49,6 +49,17 @@ public sealed class ProvideDocuments(IRegistrationStore store, IWorkflowClient w
if (registration is null || registration.Bsn != command.Bsn) if (registration is null || registration.Bsn != command.Bsn)
return ProvideDocumentsOutcome.NotFound; return ProvideDocumentsOutcome.NotFound;
// Only a clean PDF goes any further (S-29, ADR-0036): checked after ownership, so a stranger
// learns nothing about the file, and before anything is stored or the wait is completed.
if (!command.Content.AsSpan().StartsWith("%PDF-"u8))
return ProvideDocumentsOutcome.NotAPdf;
switch (await scanner.ScanAsync(command.Content, ct))
{
case ScanVerdict.Infected: return ProvideDocumentsOutcome.Infected;
case ScanVerdict.Unavailable: return ProvideDocumentsOutcome.ScannerUnavailable;
}
// Store the diploma against the zaak (once it is opened) — the ACL is the only ZGW caller (§8.1). // Store the diploma against the zaak (once it is opened) — the ACL is the only ZGW caller (§8.1).
if (registration.ZaakUrl is not null) if (registration.ZaakUrl is not null)
await acl.StoreDiplomaAsync( await acl.StoreDiplomaAsync(