feat(domain): scan uploads with clamd over INSTREAM; 422 refused, 503 scanner down (refs #192)
Verified against a real clamd 1.4.6: clean → Clean, EICAR → Infected, closed port → Unavailable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,10 @@ builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
||||
.GetSection("Acl").Get<AclOptions>()
|
||||
?? throw new InvalidOperationException("Missing configuration section 'Acl'"));
|
||||
// clamd defaults to the compose/chart service name; ClamAv__* overrides it (S-29, ADR-0036).
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
||||
.GetSection("ClamAv").Get<ClamAvOptions>() ?? new ClamAvOptions());
|
||||
builder.Services.AddSingleton<IDocumentScanner, ClamdDocumentScanner>();
|
||||
|
||||
// The in-memory registration store is shared between the submit endpoint and the worker (ADR-0009).
|
||||
builder.Services.AddSingleton<IRegistrationStore, InMemoryRegistrationStore>();
|
||||
@@ -158,8 +162,8 @@ app.MapPost("/registrations/{id}/withdraw", async (string id, WithdrawRequest bo
|
||||
// Provide documents (S-10a): the zorgprofessional supplies the documents their registration is parked
|
||||
// waiting for, completing the WachtOpDocumenten task so the process advances to beoordeling (ADR-0017).
|
||||
// Owner-scoped by the caller's bsn (the BFF forwards it from the DigiD token); unknown or not-the-
|
||||
// caller's is 404 (indistinguishable). Idempotent — completing an already-left wait is a no-op. The
|
||||
// real file upload + ZGW storage is S-10b; this endpoint is the trigger that unblocks the process.
|
||||
// caller's is 404 (indistinguishable). Idempotent — completing an already-left wait is a no-op. Only a
|
||||
// PDF that clamd scans clean is stored and unblocks the process (S-29).
|
||||
app.MapPost("/registrations/{id}/documents", async (string id, ProvideDocumentsRequest body, ProvideDocuments provide, CancellationToken ct) =>
|
||||
{
|
||||
if (!Guid.TryParse(id, out var guid))
|
||||
@@ -177,8 +181,16 @@ app.MapPost("/registrations/{id}/documents", async (string id, ProvideDocumentsR
|
||||
var command = new ProvideDocumentsCommand(
|
||||
new RegistrationId(guid), body.Bsn, content,
|
||||
body.FileName ?? "diploma.pdf", body.ContentType ?? "application/pdf");
|
||||
var outcome = await provide.HandleAsync(command, ct);
|
||||
return outcome == ProvideDocumentsOutcome.Accepted ? Results.NoContent() : Results.NotFound();
|
||||
// A refused file is 422 with a machine-readable reason the portal words for the citizen; an
|
||||
// unreachable scanner is 503 — retryable, and nothing was stored (S-29, ADR-0036).
|
||||
return await provide.HandleAsync(command, ct) switch
|
||||
{
|
||||
ProvideDocumentsOutcome.Accepted => Results.NoContent(),
|
||||
ProvideDocumentsOutcome.NotAPdf => Results.UnprocessableEntity(new { reason = "not-a-pdf" }),
|
||||
ProvideDocumentsOutcome.Infected => Results.UnprocessableEntity(new { reason = "infected" }),
|
||||
ProvideDocumentsOutcome.ScannerUnavailable => Results.StatusCode(StatusCodes.Status503ServiceUnavailable),
|
||||
_ => Results.NotFound(),
|
||||
};
|
||||
});
|
||||
|
||||
// The behandelaar's werkbak (S-12): the registrations awaiting beoordeling, read from the open
|
||||
|
||||
Reference in New Issue
Block a user