fix(SEC-38,SEC-39): preserve aggregate and fallback throttles
This commit is contained in:
@@ -101,6 +101,37 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
limiter.Check(new ApiKeyThrottlePartition("ipv4:10.0.9.9:1", "other-key")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-38: a valid key id still feeds and consults its cross-peer aggregate when each
|
||||
/// attacking peer has exhausted its 32 dedicated key-id partitions and therefore records
|
||||
/// the peer layer through the fallback bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AggregateLayer_TripsForValidKeyIdCollapsedToPeerFallback()
|
||||
{
|
||||
ManualTimeProvider clock = new(DateTimeOffset.UnixEpoch);
|
||||
ApiKeyFailureLimiter limiter = CreateLimiter(
|
||||
clock,
|
||||
limit: 100,
|
||||
aggregateLimit: 3,
|
||||
maxPartitions: 4096);
|
||||
|
||||
for (int peerIndex = 0; peerIndex < 3; peerIndex++)
|
||||
{
|
||||
string peer = $"ipv4:10.0.0.{peerIndex + 1}:1";
|
||||
for (int keyIndex = 0; keyIndex < ApiKeyFailureLimiter.MaxKeyIdPartitionsPerPeer; keyIndex++)
|
||||
{
|
||||
limiter.RecordFailure(new ApiKeyThrottlePartition(peer, $"prefill-{peerIndex}-{keyIndex}"));
|
||||
}
|
||||
|
||||
limiter.RecordFailure(new ApiKeyThrottlePartition(peer, "victim"));
|
||||
}
|
||||
|
||||
Assert.Equal(
|
||||
ApiKeyThrottleDecision.ThrottledByAggregate,
|
||||
limiter.Check(new ApiKeyThrottlePartition("ipv4:10.0.9.9:1", "victim")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A throttled partition is a valve, not a wall: exactly one request per probe interval is
|
||||
/// admitted to the real verifier, so the holder of the correct secret can always get through.
|
||||
@@ -340,6 +371,29 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
Assert.Equal(ApiKeyThrottleDecision.ThrottledByPeer, limiter.Check(overCap));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-39: a request with no effective key id does not own the peer's shared fallback
|
||||
/// partition. Resetting after such a request must leave other fallback failures tracked.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Reset_WithNullEffectiveKeyId_DoesNotClearSharedFallbackPartition()
|
||||
{
|
||||
ManualTimeProvider clock = new(DateTimeOffset.UnixEpoch);
|
||||
ApiKeyFailureLimiter limiter = CreateLimiter(clock, limit: 3, aggregateLimit: 100);
|
||||
ApiKeyThrottlePartition fallback = new("ipv4:10.0.0.1:1", KeyId: null);
|
||||
|
||||
RecordFailures(limiter, fallback, 3);
|
||||
Assert.Equal(
|
||||
ApiKeyThrottleDecision.ThrottledByPeer,
|
||||
limiter.Check(fallback, out ApiKeyFailureLimiter.PartitionResolution resolution));
|
||||
Assert.Null(resolution.EffectiveKeyId);
|
||||
|
||||
limiter.Reset(fallback, resolution);
|
||||
|
||||
Assert.True(limiter.IsTracked(fallback));
|
||||
Assert.Equal(ApiKeyThrottleDecision.ThrottledByPeer, limiter.Check(fallback));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-32: a spray of unique junk partitions (junk tokens resolve to the sender's fallback
|
||||
/// partition, one per address) must not evict a partition that is currently throttled — the
|
||||
|
||||
Reference in New Issue
Block a user