feat(auditlog): add AuditLog:InboundMaxBytes option (default 1 MiB, [8 KiB, 16 MiB])
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
namespace ScadaLink.AuditLog.Configuration;
|
namespace ScadaLink.AuditLog.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -33,4 +36,14 @@ public sealed class AuditLogOptions
|
|||||||
|
|
||||||
/// <summary>Central retention window in days (default 365, range [30, 3650]).</summary>
|
/// <summary>Central retention window in days (default 365, range [30, 3650]).</summary>
|
||||||
public int RetentionDays { get; set; } = 365;
|
public int RetentionDays { get; set; } = 365;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-body byte ceiling applied to <see cref="AuditEvent.RequestSummary"/> and
|
||||||
|
/// <see cref="AuditEvent.ResponseSummary"/> for <see cref="AuditChannel.ApiInbound"/> rows
|
||||||
|
/// (default 1 MiB). The 8 KiB / 64 KiB default/error caps that apply to other channels
|
||||||
|
/// do not apply here — inbound traffic captures verbatim up to this ceiling and only
|
||||||
|
/// then sets <see cref="AuditEvent.PayloadTruncated"/>. See
|
||||||
|
/// <c>docs/plans/2026-05-23-inbound-api-full-response-audit-design.md</c>.
|
||||||
|
/// </summary>
|
||||||
|
public int InboundMaxBytes { get; set; } = 1_048_576;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ public sealed class AuditLogOptionsValidator : IValidateOptions<AuditLogOptions>
|
|||||||
/// <summary>Inclusive upper bound for <see cref="AuditLogOptions.RetentionDays"/>.</summary>
|
/// <summary>Inclusive upper bound for <see cref="AuditLogOptions.RetentionDays"/>.</summary>
|
||||||
public const int MaxRetentionDays = 3650;
|
public const int MaxRetentionDays = 3650;
|
||||||
|
|
||||||
|
/// <summary>Inclusive lower bound for <see cref="AuditLogOptions.InboundMaxBytes"/> (8 KiB).</summary>
|
||||||
|
public const int MinInboundMaxBytes = 8_192;
|
||||||
|
|
||||||
|
/// <summary>Inclusive upper bound for <see cref="AuditLogOptions.InboundMaxBytes"/> (16 MiB).</summary>
|
||||||
|
public const int MaxInboundMaxBytes = 16_777_216;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ValidateOptionsResult Validate(string? name, AuditLogOptions options)
|
public ValidateOptionsResult Validate(string? name, AuditLogOptions options)
|
||||||
{
|
{
|
||||||
@@ -50,6 +56,13 @@ public sealed class AuditLogOptionsValidator : IValidateOptions<AuditLogOptions>
|
|||||||
$"must be in [{MinRetentionDays}, {MaxRetentionDays}] days.");
|
$"must be in [{MinRetentionDays}, {MaxRetentionDays}] days.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.InboundMaxBytes < MinInboundMaxBytes || options.InboundMaxBytes > MaxInboundMaxBytes)
|
||||||
|
{
|
||||||
|
failures.Add(
|
||||||
|
$"AuditLog:{nameof(AuditLogOptions.InboundMaxBytes)} ({options.InboundMaxBytes}) " +
|
||||||
|
$"must be in [{MinInboundMaxBytes}, {MaxInboundMaxBytes}] bytes.");
|
||||||
|
}
|
||||||
|
|
||||||
return failures.Count == 0
|
return failures.Count == 0
|
||||||
? ValidateOptionsResult.Success
|
? ValidateOptionsResult.Success
|
||||||
: ValidateOptionsResult.Fail(failures);
|
: ValidateOptionsResult.Fail(failures);
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ public class AuditLogOptionsBindingTests
|
|||||||
"RedactSqlParamsMatching": "@token|@secret"
|
"RedactSqlParamsMatching": "@token|@secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RetentionDays": 180
|
"RetentionDays": 180,
|
||||||
|
"InboundMaxBytes": 524288
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
@@ -64,6 +65,7 @@ public class AuditLogOptionsBindingTests
|
|||||||
Assert.Equal(4096, opts.DefaultCapBytes);
|
Assert.Equal(4096, opts.DefaultCapBytes);
|
||||||
Assert.Equal(32768, opts.ErrorCapBytes);
|
Assert.Equal(32768, opts.ErrorCapBytes);
|
||||||
Assert.Equal(180, opts.RetentionDays);
|
Assert.Equal(180, opts.RetentionDays);
|
||||||
|
Assert.Equal(524_288, opts.InboundMaxBytes);
|
||||||
|
|
||||||
// HeaderRedactList: the Microsoft.Extensions.Configuration list binder
|
// HeaderRedactList: the Microsoft.Extensions.Configuration list binder
|
||||||
// APPENDS to the default list, so we assert containment rather than
|
// APPENDS to the default list, so we assert containment rather than
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using ScadaLink.AuditLog.Configuration;
|
||||||
|
|
||||||
|
namespace ScadaLink.AuditLog.Tests.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Task 1 of <c>docs/plans/2026-05-23-inbound-api-full-response-audit.md</c>:
|
||||||
|
/// pins the <see cref="AuditLogOptions.InboundMaxBytes"/> default to 1 MiB and
|
||||||
|
/// the validator bounds to <c>[8 KiB, 16 MiB]</c>. The inbound channel needs a
|
||||||
|
/// much larger ceiling than the 8 KiB / 64 KiB default/error caps that other
|
||||||
|
/// channels use, but unbounded would let any caller flood the central
|
||||||
|
/// <c>AuditLog</c> table with arbitrarily large bodies — hence the upper bound.
|
||||||
|
/// Companion to <see cref="AuditLogOptionsTests"/> which covers the existing
|
||||||
|
/// cap-bytes + retention invariants.
|
||||||
|
/// </summary>
|
||||||
|
public class AuditLogOptionsValidatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Validate_InboundMaxBytes_DefaultOptions_IsOneMebibyte()
|
||||||
|
{
|
||||||
|
// The doc'd default per docs/plans/2026-05-23-inbound-api-full-response-audit-design.md
|
||||||
|
// is 1 048 576 bytes (1 MiB). Pin it so a config drift is a test failure,
|
||||||
|
// not a silent operational surprise.
|
||||||
|
var opts = new AuditLogOptions();
|
||||||
|
Assert.Equal(1_048_576, opts.InboundMaxBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(8_192)] // documented min
|
||||||
|
[InlineData(1_048_576)] // default
|
||||||
|
[InlineData(16_777_216)] // documented max
|
||||||
|
public void Validate_InboundMaxBytes_InRange_Passes(int value)
|
||||||
|
{
|
||||||
|
var validator = new AuditLogOptionsValidator();
|
||||||
|
var opts = new AuditLogOptions { InboundMaxBytes = value };
|
||||||
|
Assert.True(validator.Validate(null, opts).Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(8_191)]
|
||||||
|
[InlineData(16_777_217)]
|
||||||
|
[InlineData(int.MaxValue)]
|
||||||
|
public void Validate_InboundMaxBytes_OutOfRange_Fails(int value)
|
||||||
|
{
|
||||||
|
var validator = new AuditLogOptionsValidator();
|
||||||
|
var opts = new AuditLogOptions { InboundMaxBytes = value };
|
||||||
|
var result = validator.Validate(null, opts);
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(
|
||||||
|
result.Failures!,
|
||||||
|
f => f.Contains(nameof(AuditLogOptions.InboundMaxBytes), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user