fix(auth): aggregate bulk constraint denials into one audit event; ValueTask constraint checks
A bulk command with N denied items wrote N audit events into the bounded, drop-on-full audit channel, so a low-privilege key could evict every other pending audit record. Bulk filters now accumulate a ConstraintDenialSummary (count, constraint names, <=20 sample targets) and write one aggregated constraint-denied event per command. The per-item constraint checks are synchronous, so IConstraintEnforcer's Check* methods now return ValueTask instead of allocating a Task per item.
This commit is contained in:
+50
-2
@@ -87,8 +87,56 @@ public sealed class MxAccessGatewayServiceConstraintTests
|
||||
Assert.True(merged.Results[3].WasSuccessful);
|
||||
Assert.Equal("Tank04.Public", merged.Results[3].TagAddress);
|
||||
Assert.Equal(104, merged.Results[3].ItemHandle);
|
||||
// Both denied tags recorded.
|
||||
Assert.Equal(2, enforcer.RecordedDenials.Count);
|
||||
// Both denied tags recorded in ONE aggregated bulk denial, not one audit call per item.
|
||||
Assert.Empty(enforcer.RecordedDenials);
|
||||
(string commandKind, ConstraintDenialSummary summary, _) = Assert.Single(enforcer.RecordedBulkDenials);
|
||||
Assert.Equal(nameof(MxCommandKind.AddItemBulk), commandKind);
|
||||
Assert.Equal(2, summary.DeniedCount);
|
||||
Assert.Equal(4, summary.TotalCount);
|
||||
Assert.Equal(["Tank01.Locked", "Tank03.Secret"], summary.SampleTargets);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit-flood guard: a bulk write whose every entry is denied records exactly one
|
||||
/// aggregated denial carrying the full denied count, the client correlation id and a sample
|
||||
/// capped at <see cref="ConstraintDenialSummary.MaxSampleTargets"/> — so one low-privilege
|
||||
/// key cannot push thousands of events into the bounded, drop-on-full audit channel.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Invoke_WriteBulk_WithManyDeniedEntries_RecordsOneAggregatedDenial()
|
||||
{
|
||||
const int EntryCount = 500;
|
||||
PredicateConstraintEnforcer enforcer = new() { DenyWriteHandle = (_, _) => true };
|
||||
FakeSessionManager sessionManager = CreateSessionManagerWithSeed();
|
||||
MxAccessGatewayService service = CreateService(sessionManager, enforcer);
|
||||
WriteBulkCommand payload = new() { ServerHandle = 7 };
|
||||
for (int index = 0; index < EntryCount; index++)
|
||||
{
|
||||
payload.Entries.Add(new WriteBulkEntry { ItemHandle = index + 1 });
|
||||
}
|
||||
|
||||
MxCommandRequest request = new()
|
||||
{
|
||||
SessionId = SessionId,
|
||||
ClientCorrelationId = "bulk-flood-1",
|
||||
Command = new MxCommand { Kind = MxCommandKind.WriteBulk, WriteBulk = payload },
|
||||
};
|
||||
|
||||
MxCommandReply reply = await service.Invoke(request, new TestServerCallContext());
|
||||
|
||||
Assert.Equal(0, sessionManager.InvokeCount);
|
||||
Assert.Equal(EntryCount, reply.WriteBulk.Results.Count);
|
||||
Assert.Empty(enforcer.RecordedDenials);
|
||||
(string commandKind, ConstraintDenialSummary summary, string? correlationId) =
|
||||
Assert.Single(enforcer.RecordedBulkDenials);
|
||||
Assert.Equal(nameof(MxCommandKind.WriteBulk), commandKind);
|
||||
Assert.Equal("bulk-flood-1", correlationId);
|
||||
Assert.Equal(EntryCount, summary.DeniedCount);
|
||||
Assert.Equal(EntryCount, summary.TotalCount);
|
||||
Assert.Equal(ConstraintDenialSummary.MaxSampleTargets, summary.SampleTargets.Count);
|
||||
Assert.True(summary.SampleTruncated);
|
||||
Assert.Equal(["write-handle"], summary.ConstraintNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user