Browse Source
fix: update RetryAfter handling to return null for ban policies and add custom resolver null check
pull/25024/head
maliming
3 weeks ago
No known key found for this signature in database
GPG Key ID: A646B9CB645ECEA4
6 changed files with
43 additions and
7 deletions
docs/en/framework/infrastructure/operation-rate-limiting.md
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Rules/FixedWindowOperationRateLimitingRule.cs
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Store/DistributedCacheOperationRateLimitingStore.cs
framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/AbpOperationRateLimitingTestModule.cs
framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/DistributedCacheOperationRateLimitingStore_Tests.cs
framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/OperationRateLimitingChecker_Tests.cs
@ -205,7 +205,9 @@ policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100)
## Multi-Tenancy
By default, rate limit counters are **shared across all tenants** . You can enable tenant isolation for a rule by calling `WithMultiTenancy()` :
By default, partition keys do not include tenant information — for partition types like `PartitionByParameter` , `PartitionByCurrentUser` , `PartitionByClientIp` , etc., counters are shared across tenants unless you call `WithMultiTenancy()` . Note that `PartitionByCurrentTenant()` is inherently per-tenant since the partition key is the tenant ID itself, and `PartitionByClientIp()` is typically kept global since the same IP should share a counter regardless of tenant.
You can enable tenant isolation for a rule by calling `WithMultiTenancy()` :
````csharp
policy.AddRule(rule => rule
@ -326,7 +328,7 @@ public override void ConfigureServices(ServiceConfigurationContext context)
### Ban Policy (maxCount: 0)
Setting `maxCount` to `0` creates a ban policy that blocks all requests for the specified duration :
Setting `maxCount` to `0` creates a ban policy that permanently denies all requests regardless of the window duration. The `RetryAfter` value will be `null` since there is no window to wait for :
````csharp
options.AddPolicy("BlockedUser", policy =>
@ -99,12 +99,24 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
$"Phone number is required for policy '{PolicyName}' (PartitionByPhoneNumber). Provide it via context.Parameter or ensure the user has a phone number." ) ,
OperationRateLimitingPartitionType . Custom = >
await Definition . CustomPartitionKeyResolver ! ( context ) ,
await ResolveCustomPartitionKeyAsync ( context ) ,
_ = > throw new AbpException ( $"Unknown partition type: {Definition.PartitionType}" )
} ;
}
protected virtual async Task < string > ResolveCustomPartitionKeyAsync ( OperationRateLimitingContext context )
{
var key = await Definition . CustomPartitionKeyResolver ! ( context ) ;
if ( string . IsNullOrEmpty ( key ) )
{
throw new AbpException (
$"Custom partition key resolver returned null or empty for policy '{PolicyName}'. " +
"The resolver must return a non-empty string." ) ;
}
return key ;
}
protected virtual string BuildStoreKey ( string partitionKey )
{
// Stable rule descriptor based on content so reordering rules does not change the key.
@ -38,7 +38,7 @@ public class DistributedCacheOperationRateLimitingStore : IOperationRateLimiting
IsAllowed = false ,
CurrentCount = 0 ,
MaxCount = maxCount ,
RetryAfter = duration
RetryAfter = null
} ;
}
@ -111,7 +111,7 @@ public class DistributedCacheOperationRateLimitingStore : IOperationRateLimiting
IsAllowed = false ,
CurrentCount = 0 ,
MaxCount = maxCount ,
RetryAfter = duration
RetryAfter = null
} ;
}
@ -161,6 +161,13 @@ public class AbpOperationRateLimitingTestModule : AbpModule
. PartitionBy ( ctx = > Task . FromResult ( $"action:{ctx.Parameter}" ) ) ;
} ) ;
// Custom resolver returning null - should throw
options . AddPolicy ( "TestCustomResolverNull" , policy = >
{
policy . WithFixedWindow ( TimeSpan . FromHours ( 1 ) , maxCount : 2 )
. PartitionBy ( ctx = > Task . FromResult < string > ( null ! ) ) ;
} ) ;
// Multi-tenant: ByParameter with tenant isolation - same param, different tenants = different counters
options . AddPolicy ( "TestMultiTenantByParameter" , policy = >
{
@ -118,7 +118,7 @@ public class DistributedCacheOperationRateLimitingStore_Tests : OperationRateLim
result . IsAllowed . ShouldBeFalse ( ) ;
result . CurrentCount . ShouldBe ( 0 ) ;
result . MaxCount . ShouldBe ( 0 ) ;
result . RetryAfter . ShouldNot BeNull ( ) ;
result . RetryAfter . ShouldBeNull ( ) ;
}
[Fact]
@ -130,6 +130,6 @@ public class DistributedCacheOperationRateLimitingStore_Tests : OperationRateLim
result . IsAllowed . ShouldBeFalse ( ) ;
result . CurrentCount . ShouldBe ( 0 ) ;
result . MaxCount . ShouldBe ( 0 ) ;
result . RetryAfter . ShouldNot BeNull ( ) ;
result . RetryAfter . ShouldBeNull ( ) ;
}
}
@ -653,6 +653,7 @@ public class OperationRateLimitingChecker_Tests : OperationRateLimitingTestBase
exception . Result . IsAllowed . ShouldBeFalse ( ) ;
exception . Result . MaxCount . ShouldBe ( 0 ) ;
exception . Result . RetryAfter . ShouldBeNull ( ) ;
exception . HttpStatusCode . ShouldBe ( 4 2 9 ) ;
}
@ -674,6 +675,7 @@ public class OperationRateLimitingChecker_Tests : OperationRateLimitingTestBase
status . IsAllowed . ShouldBeFalse ( ) ;
status . MaxCount . ShouldBe ( 0 ) ;
status . RemainingCount . ShouldBe ( 0 ) ;
status . RetryAfter . ShouldBeNull ( ) ;
}
[Fact]
@ -701,6 +703,19 @@ public class OperationRateLimitingChecker_Tests : OperationRateLimitingTestBase
( await _ checker . IsAllowedAsync ( "TestCustomResolver" , ctx2 ) ) . ShouldBeTrue ( ) ;
}
[Fact]
public async Task Should_Throw_When_Custom_Resolver_Returns_Null ( )
{
var context = new OperationRateLimitingContext { Parameter = "test" } ;
var exception = await Assert . ThrowsAsync < AbpException > ( async ( ) = >
{
await _ checker . CheckAsync ( "TestCustomResolverNull" , context ) ;
} ) ;
exception . Message . ShouldContain ( "Custom partition key resolver returned null or empty" ) ;
}
[Fact]
public void Should_Throw_When_Policy_Has_Duplicate_Rules ( )
{