using System.Collections.Concurrent; using ZB.MOM.WW.MxGateway.Server.Configuration; namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization; /// /// Cheap, in-process per-peer sliding-window failure counter for the gRPC auth path (SEC-11). It is /// checked BEFORE the API-key verification store read and short-circuits a peer that has exceeded /// failed attempts within /// ; a successful verification resets the /// peer's counter. /// /// /// /// The peer key is the API key id when the presented token parses, falling back to the transport /// peer address otherwise. Keying on key id (per the NAT caveat in glauth.md) means a single /// abusive credential behind a shared NAT is throttled without locking out unrelated clients on the /// same address. /// /// /// The tracked-peer set is a bounded LRU () so /// a spray of unique peer keys cannot grow memory without limit. Successful peers are removed on /// reset, so in steady state the map holds only peers with recent failures — the common success path /// is a lock-free dictionary miss. /// /// public sealed class ApiKeyFailureLimiter { private readonly int _limit; private readonly long _windowTicks; private readonly int _maxPeers; private readonly TimeProvider _clock; private readonly ConcurrentDictionary _peers = new(StringComparer.Ordinal); /// Initializes a new instance of the class. /// Security options carrying the failure-limit knobs. /// The time provider. public ApiKeyFailureLimiter(SecurityOptions security, TimeProvider clock) : this( (security ?? throw new ArgumentNullException(nameof(security))).ApiKeyFailureLimit, TimeSpan.FromSeconds(security.ApiKeyFailureWindowSeconds), security.ApiKeyFailureTrackedPeers, clock) { } // Test/explicit seam. internal ApiKeyFailureLimiter(int limit, TimeSpan window, int maxPeers, TimeProvider clock) { ArgumentNullException.ThrowIfNull(clock); _limit = limit; _windowTicks = window.Ticks; _maxPeers = maxPeers; _clock = clock; } /// Returns whether the peer has reached the failure limit within the current window. /// The peer key (key id or peer address). /// when the peer should be short-circuited. public bool IsBlocked(string peer) { ArgumentNullException.ThrowIfNull(peer); if (_limit <= 0) { return false; } if (!_peers.TryGetValue(peer, out PeerState? state)) { return false; } long now = _clock.GetUtcNow().UtcTicks; lock (state) { Prune(state, now); return state.FailureTicks.Count >= _limit; } } /// Records a failed verification attempt for the peer. /// The peer key (key id or peer address). public void RecordFailure(string peer) { ArgumentNullException.ThrowIfNull(peer); if (_limit <= 0) { return; } long now = _clock.GetUtcNow().UtcTicks; PeerState state = _peers.GetOrAdd(peer, static _ => new PeerState()); lock (state) { Prune(state, now); state.FailureTicks.Enqueue(now); state.LastActivityTicks = now; } EvictIfOverCapacity(); } /// Clears the peer's failure count after a successful verification. /// The peer key (key id or peer address). public void Reset(string peer) { ArgumentNullException.ThrowIfNull(peer); _peers.TryRemove(peer, out _); } private void Prune(PeerState state, long now) { while (state.FailureTicks.Count > 0 && now - state.FailureTicks.Peek() >= _windowTicks) { state.FailureTicks.Dequeue(); } } private void EvictIfOverCapacity() { // Best-effort eviction: only runs when the map exceeds the cap (rare, since only peers with // recent failures are tracked). Removes the least-recently-active peer. Racy under // concurrency, which is acceptable for a bound rather than an exact policy. while (_peers.Count > _maxPeers) { string? oldest = null; long oldestTicks = long.MaxValue; foreach (KeyValuePair entry in _peers) { long activity = Volatile.Read(ref entry.Value.LastActivityTicks); if (activity < oldestTicks) { oldestTicks = activity; oldest = entry.Key; } } if (oldest is null || !_peers.TryRemove(oldest, out _)) { break; } } } private sealed class PeerState { public Queue FailureTicks { get; } = new(); public long LastActivityTicks; } }