feat(inboundapi): bound audit capture at InboundMaxBytes (memory safety)
AuditWriteMiddleware previously buffered the FULL request and response bodies into memory and only let DefaultAuditPayloadFilter trim them after persistence. A 500 MiB upload allocated 500 MiB of MemoryStream plus 1 GiB of UTF-16 string transiently before the filter pulled it back to the 1 MiB inbound ceiling — the cap was real on the persisted row but not at the capture site. Inject IOptionsMonitor<AuditLogOptions> and read InboundMaxBytes per-request (same convention as DefaultAuditPayloadFilter so a live config change picks up the next request). The request reader now pulls at most cap + 1 bytes into a UTF-8 byte-safe-truncated string and rewinds the stream so the endpoint handler still sees the full body. The response wrap is a new CapturedResponseStream that forwards every Write / WriteAsync to the real sink (the client still receives all bytes) while capturing at most cap + 1 bytes for the audit copy. The middleware now sets PayloadTruncated itself when either body hit the cap; the filter still OR's its own determination on top. Adds a project reference from ScadaLink.InboundAPI to ScadaLink.AuditLog so AuditLogOptions resolves. AuditLog does NOT reference InboundAPI back, so no cycle is introduced. Tests: - All 21 existing AuditWriteMiddlewareTests still pass (the helper gains an optional AuditLogOptions argument; default is the standard 1 MiB ceiling so existing small-body tests are unaffected). - MiddlewareOrderTests' construction site updated for the new ctor arg; a StaticAuditLogOptionsMonitor file-local double mirrors the InboundChannelCapTests pattern. - New RequestBody_AboveInboundMaxBytes_TruncatedToCap_PayloadTruncatedTrue pins a 4 KiB cap against a 20 KB body: audit copy <= 4 KiB, PayloadTruncated = true, downstream handler reads the full 20 KB. - New ResponseBody_AboveInboundMaxBytes_TruncatedToCap_ClientStillReceivesAllBytes_PayloadTruncatedTrue pins the same shape on the response side: client sink receives 20 KB, audit copy <= 4 KiB, PayloadTruncated = true. InboundAPI test count: 133 -> 135.
This commit is contained in:
@@ -6,6 +6,8 @@ using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.AuditLog.Configuration;
|
||||
using ScadaLink.Commons.Entities.Audit;
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
@@ -145,7 +147,8 @@ public class MiddlewareOrderTests
|
||||
// instantiates the type correctly.
|
||||
_ => Task.CompletedTask,
|
||||
writer,
|
||||
NullLogger<AuditWriteMiddleware>.Instance));
|
||||
NullLogger<AuditWriteMiddleware>.Instance,
|
||||
new StaticAuditLogOptionsMonitor(new AuditLogOptions())));
|
||||
services.AddRouting();
|
||||
services.AddAuthorization();
|
||||
services.AddAuthentication("TestScheme")
|
||||
@@ -233,4 +236,22 @@ public class MiddlewareOrderTests
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File-local <see cref="IOptionsMonitor{TOptions}"/> test double — returns the
|
||||
/// same snapshot on every read. Mirrors the helper in
|
||||
/// <c>AuditWriteMiddlewareTests</c>.
|
||||
/// </summary>
|
||||
private sealed class StaticAuditLogOptionsMonitor : IOptionsMonitor<AuditLogOptions>
|
||||
{
|
||||
private readonly AuditLogOptions _value;
|
||||
|
||||
public StaticAuditLogOptionsMonitor(AuditLogOptions value) => _value = value;
|
||||
|
||||
public AuditLogOptions CurrentValue => _value;
|
||||
|
||||
public AuditLogOptions Get(string? name) => _value;
|
||||
|
||||
public IDisposable? OnChange(Action<AuditLogOptions, string?> listener) => null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user