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:
@@ -0,0 +1,90 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Accumulates the constraint denials of one bulk command so they are audited as a single
|
||||
/// aggregated event rather than one event per denied item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A bulk command can carry thousands of items, and the audit writer is a bounded, drop-on-full
|
||||
/// channel. One audit event per denied item let a single low-privilege key flood that channel and
|
||||
/// push every other audit record out of it. The summary keeps the forensic signal — how many items
|
||||
/// were denied, which constraints denied them, and a bounded sample of the targets — at a fixed
|
||||
/// cost of one audit event per command.
|
||||
/// </remarks>
|
||||
public sealed class ConstraintDenialSummary
|
||||
{
|
||||
/// <summary>The maximum number of denied targets retained as a sample.</summary>
|
||||
public const int MaxSampleTargets = 20;
|
||||
|
||||
private readonly List<string> _constraintNames = [];
|
||||
private readonly List<string> _sampleTargets = [];
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ConstraintDenialSummary"/> class.</summary>
|
||||
/// <param name="totalCount">The number of items the bulk command carried.</param>
|
||||
public ConstraintDenialSummary(int totalCount)
|
||||
{
|
||||
TotalCount = totalCount;
|
||||
}
|
||||
|
||||
/// <summary>Gets the number of items the bulk command carried.</summary>
|
||||
public int TotalCount { get; }
|
||||
|
||||
/// <summary>Gets the number of items denied.</summary>
|
||||
public int DeniedCount { get; private set; }
|
||||
|
||||
/// <summary>Gets the distinct constraint names that denied at least one item, in first-seen order.</summary>
|
||||
public IReadOnlyList<string> ConstraintNames => _constraintNames;
|
||||
|
||||
/// <summary>Gets up to <see cref="MaxSampleTargets"/> denied targets, in item order.</summary>
|
||||
public IReadOnlyList<string> SampleTargets => _sampleTargets;
|
||||
|
||||
/// <summary>Gets a value indicating whether more items were denied than the sample retains.</summary>
|
||||
public bool SampleTruncated => DeniedCount > _sampleTargets.Count;
|
||||
|
||||
/// <summary>Records one denied tag-address item.</summary>
|
||||
/// <param name="target">The denied tag address.</param>
|
||||
/// <param name="failure">The constraint failure that denied it.</param>
|
||||
public void Add(string target, ConstraintFailure failure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(failure);
|
||||
AddConstraint(failure.ConstraintName);
|
||||
if (_sampleTargets.Count < MaxSampleTargets)
|
||||
{
|
||||
_sampleTargets.Add(target);
|
||||
}
|
||||
|
||||
DeniedCount++;
|
||||
}
|
||||
|
||||
/// <summary>Records one denied item-handle item.</summary>
|
||||
/// <param name="itemHandle">The denied MXAccess item handle.</param>
|
||||
/// <param name="failure">The constraint failure that denied it.</param>
|
||||
/// <remarks>The handle is formatted only while the sample still has room.</remarks>
|
||||
public void Add(int itemHandle, ConstraintFailure failure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(failure);
|
||||
AddConstraint(failure.ConstraintName);
|
||||
if (_sampleTargets.Count < MaxSampleTargets)
|
||||
{
|
||||
_sampleTargets.Add(itemHandle.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
DeniedCount++;
|
||||
}
|
||||
|
||||
private void AddConstraint(string constraintName)
|
||||
{
|
||||
// The constraint vocabulary is a handful of fixed names, so a linear scan beats a set.
|
||||
for (int index = 0; index < _constraintNames.Count; index++)
|
||||
{
|
||||
if (string.Equals(_constraintNames[index], constraintName, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_constraintNames.Add(constraintName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user