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); } }