using System.Globalization;
namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;
///
/// Accumulates the constraint denials of one bulk command so they are audited as a single
/// aggregated event rather than one event per denied item.
///
///
/// 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.
///
public sealed class ConstraintDenialSummary
{
/// The maximum number of denied targets retained as a sample.
public const int MaxSampleTargets = 20;
private readonly List _constraintNames = [];
private readonly List _sampleTargets = [];
/// Initializes a new instance of the class.
/// The number of items the bulk command carried.
public ConstraintDenialSummary(int totalCount)
{
TotalCount = totalCount;
}
/// Gets the number of items the bulk command carried.
public int TotalCount { get; }
/// Gets the number of items denied.
public int DeniedCount { get; private set; }
/// Gets the distinct constraint names that denied at least one item, in first-seen order.
public IReadOnlyList ConstraintNames => _constraintNames;
/// Gets up to denied targets, in item order.
public IReadOnlyList SampleTargets => _sampleTargets;
/// Gets a value indicating whether more items were denied than the sample retains.
public bool SampleTruncated => DeniedCount > _sampleTargets.Count;
/// Records one denied tag-address item.
/// The denied tag address.
/// The constraint failure that denied it.
public void Add(string target, ConstraintFailure failure)
{
ArgumentNullException.ThrowIfNull(failure);
AddConstraint(failure.ConstraintName);
if (_sampleTargets.Count < MaxSampleTargets)
{
_sampleTargets.Add(target);
}
DeniedCount++;
}
/// Records one denied item-handle item.
/// The denied MXAccess item handle.
/// The constraint failure that denied it.
/// The handle is formatted only while the sample still has room.
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);
}
}