Browse Source

refactor: update partition key handling to support async resolution in operation rate limiting

pull/25024/head
maliming 7 months ago
parent
commit
46b283e338
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 5
      framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Checker/OperationRateLimitingContext.cs
  2. 15
      framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Policies/OperationRateLimitingRuleBuilder.cs
  3. 3
      framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Policies/OperationRateLimitingRuleDefinition.cs
  4. 21
      framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Rules/FixedWindowOperationRateLimitingRule.cs
  5. 3
      framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/AbpOperationRateLimitingTestModule.cs
  6. 7
      framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/OperationRateLimitingPolicyBuilder_Tests.cs

5
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Checker/OperationRateLimitingContext.cs

@ -8,8 +8,9 @@ public class OperationRateLimitingContext
{ {
/// <summary> /// <summary>
/// Optional parameter passed by the caller. /// Optional parameter passed by the caller.
/// Used by rules configured with PartitionByParameter(). /// Used as the partition key by PartitionByParameter() (required),
/// Can be email, phone number, resource id, or any string. /// and as a fallback by PartitionByEmail() and PartitionByPhoneNumber().
/// Can be email, phone number, user id, resource id, or any string.
/// </summary> /// </summary>
public string? Parameter { get; set; } public string? Parameter { get; set; }

15
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Policies/OperationRateLimitingRuleBuilder.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
namespace Volo.Abp.OperationRateLimiting; namespace Volo.Abp.OperationRateLimiting;
@ -8,7 +9,7 @@ public class OperationRateLimitingRuleBuilder
private TimeSpan _duration; private TimeSpan _duration;
private int _maxCount; private int _maxCount;
private OperationRateLimitingPartitionType? _partitionType; private OperationRateLimitingPartitionType? _partitionType;
private Func<OperationRateLimitingContext, string>? _customPartitionKeyResolver; private Func<OperationRateLimitingContext, Task<string>>? _customPartitionKeyResolver;
private bool _isMultiTenant; private bool _isMultiTenant;
internal bool IsCommitted { get; private set; } internal bool IsCommitted { get; private set; }
@ -43,7 +44,8 @@ public class OperationRateLimitingRuleBuilder
} }
/// <summary> /// <summary>
/// Auto resolve from ICurrentUser.Id. /// Partition by the current authenticated user (ICurrentUser.Id).
/// Use PartitionByParameter() if you need to specify the user ID explicitly.
/// </summary> /// </summary>
public OperationRateLimitingPolicyBuilder PartitionByCurrentUser() public OperationRateLimitingPolicyBuilder PartitionByCurrentUser()
{ {
@ -53,7 +55,7 @@ public class OperationRateLimitingRuleBuilder
} }
/// <summary> /// <summary>
/// Auto resolve from ICurrentTenant.Id. /// Partition by the current tenant (ICurrentTenant.Id). Uses "host" when no tenant is active.
/// </summary> /// </summary>
public OperationRateLimitingPolicyBuilder PartitionByCurrentTenant() public OperationRateLimitingPolicyBuilder PartitionByCurrentTenant()
{ {
@ -63,7 +65,8 @@ public class OperationRateLimitingRuleBuilder
} }
/// <summary> /// <summary>
/// Auto resolve from IWebClientInfoProvider.ClientIpAddress. /// Partition by the client IP address (IWebClientInfoProvider.ClientIpAddress).
/// Use PartitionByParameter() if you need to specify the IP explicitly.
/// </summary> /// </summary>
public OperationRateLimitingPolicyBuilder PartitionByClientIp() public OperationRateLimitingPolicyBuilder PartitionByClientIp()
{ {
@ -95,10 +98,10 @@ public class OperationRateLimitingRuleBuilder
} }
/// <summary> /// <summary>
/// Custom partition key resolver from context. /// Custom async partition key resolver from context.
/// </summary> /// </summary>
public OperationRateLimitingPolicyBuilder PartitionBy( public OperationRateLimitingPolicyBuilder PartitionBy(
Func<OperationRateLimitingContext, string> keyResolver) Func<OperationRateLimitingContext, Task<string>> keyResolver)
{ {
_partitionType = OperationRateLimitingPartitionType.Custom; _partitionType = OperationRateLimitingPartitionType.Custom;
_customPartitionKeyResolver = Check.NotNull(keyResolver, nameof(keyResolver)); _customPartitionKeyResolver = Check.NotNull(keyResolver, nameof(keyResolver));

3
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Policies/OperationRateLimitingRuleDefinition.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
namespace Volo.Abp.OperationRateLimiting; namespace Volo.Abp.OperationRateLimiting;
@ -10,7 +11,7 @@ public class OperationRateLimitingRuleDefinition
public OperationRateLimitingPartitionType PartitionType { get; set; } public OperationRateLimitingPartitionType PartitionType { get; set; }
public Func<OperationRateLimitingContext, string>? CustomPartitionKeyResolver { get; set; } public Func<OperationRateLimitingContext, Task<string>>? CustomPartitionKeyResolver { get; set; }
public bool IsMultiTenant { get; set; } public bool IsMultiTenant { get; set; }
} }

21
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Rules/FixedWindowOperationRateLimitingRule.cs

@ -38,7 +38,7 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
public virtual async Task<OperationRateLimitingRuleResult> AcquireAsync( public virtual async Task<OperationRateLimitingRuleResult> AcquireAsync(
OperationRateLimitingContext context) OperationRateLimitingContext context)
{ {
var partitionKey = ResolvePartitionKey(context); var partitionKey = await ResolvePartitionKeyAsync(context);
var storeKey = BuildStoreKey(partitionKey); var storeKey = BuildStoreKey(partitionKey);
var storeResult = await Store.IncrementAsync(storeKey, Definition.Duration, Definition.MaxCount); var storeResult = await Store.IncrementAsync(storeKey, Definition.Duration, Definition.MaxCount);
@ -48,7 +48,7 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
public virtual async Task<OperationRateLimitingRuleResult> CheckAsync( public virtual async Task<OperationRateLimitingRuleResult> CheckAsync(
OperationRateLimitingContext context) OperationRateLimitingContext context)
{ {
var partitionKey = ResolvePartitionKey(context); var partitionKey = await ResolvePartitionKeyAsync(context);
var storeKey = BuildStoreKey(partitionKey); var storeKey = BuildStoreKey(partitionKey);
var storeResult = await Store.GetAsync(storeKey, Definition.Duration, Definition.MaxCount); var storeResult = await Store.GetAsync(storeKey, Definition.Duration, Definition.MaxCount);
@ -57,12 +57,12 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
public virtual async Task ResetAsync(OperationRateLimitingContext context) public virtual async Task ResetAsync(OperationRateLimitingContext context)
{ {
var partitionKey = ResolvePartitionKey(context); var partitionKey = await ResolvePartitionKeyAsync(context);
var storeKey = BuildStoreKey(partitionKey); var storeKey = BuildStoreKey(partitionKey);
await Store.ResetAsync(storeKey); await Store.ResetAsync(storeKey);
} }
protected virtual string ResolvePartitionKey(OperationRateLimitingContext context) protected virtual async Task<string> ResolvePartitionKeyAsync(OperationRateLimitingContext context)
{ {
return Definition.PartitionType switch return Definition.PartitionType switch
{ {
@ -71,17 +71,20 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
$"OperationRateLimitingContext.Parameter is required for policy '{PolicyName}' (PartitionByParameter)."), $"OperationRateLimitingContext.Parameter is required for policy '{PolicyName}' (PartitionByParameter)."),
OperationRateLimitingPartitionType.CurrentUser => OperationRateLimitingPartitionType.CurrentUser =>
CurrentUser.Id?.ToString() ?? throw new AbpException( CurrentUser.Id?.ToString()
$"Current user is not authenticated. Policy '{PolicyName}' requires PartitionByCurrentUser."), ?? throw new AbpException(
$"Current user is not authenticated. Policy '{PolicyName}' requires PartitionByCurrentUser. " +
"Use PartitionByParameter() if you need to specify the user ID explicitly."),
OperationRateLimitingPartitionType.CurrentTenant => OperationRateLimitingPartitionType.CurrentTenant =>
CurrentTenant.Id?.ToString() ?? HostTenantKey, CurrentTenant.Id?.ToString()
?? HostTenantKey,
OperationRateLimitingPartitionType.ClientIp => OperationRateLimitingPartitionType.ClientIp =>
WebClientInfoProvider.ClientIpAddress WebClientInfoProvider.ClientIpAddress
?? throw new AbpException( ?? throw new AbpException(
$"Client IP address could not be determined. Policy '{PolicyName}' requires PartitionByClientIp. " + $"Client IP address could not be determined. Policy '{PolicyName}' requires PartitionByClientIp. " +
"Ensure IWebClientInfoProvider is properly configured."), "Ensure IWebClientInfoProvider is properly configured or use PartitionByParameter() to pass the IP explicitly."),
OperationRateLimitingPartitionType.Email => OperationRateLimitingPartitionType.Email =>
context.Parameter context.Parameter
@ -96,7 +99,7 @@ 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."), $"Phone number is required for policy '{PolicyName}' (PartitionByPhoneNumber). Provide it via context.Parameter or ensure the user has a phone number."),
OperationRateLimitingPartitionType.Custom => OperationRateLimitingPartitionType.Custom =>
Definition.CustomPartitionKeyResolver!(context), await Definition.CustomPartitionKeyResolver!(context),
_ => throw new AbpException($"Unknown partition type: {Definition.PartitionType}") _ => throw new AbpException($"Unknown partition type: {Definition.PartitionType}")
}; };

3
framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/AbpOperationRateLimitingTestModule.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NSubstitute; using NSubstitute;
using Volo.Abp.AspNetCore.WebClientInfo; using Volo.Abp.AspNetCore.WebClientInfo;
@ -157,7 +158,7 @@ public class AbpOperationRateLimitingTestModule : AbpModule
options.AddPolicy("TestCustomResolver", policy => options.AddPolicy("TestCustomResolver", policy =>
{ {
policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 2) policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 2)
.PartitionBy(ctx => $"action:{ctx.Parameter}"); .PartitionBy(ctx => Task.FromResult($"action:{ctx.Parameter}"));
}); });
// Multi-tenant: ByParameter with tenant isolation - same param, different tenants = different counters // Multi-tenant: ByParameter with tenant isolation - same param, different tenants = different counters

7
framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/OperationRateLimitingPolicyBuilder_Tests.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
@ -74,7 +75,7 @@ public class OperationRateLimitingPolicyBuilder_Tests
{ {
policy.AddRule(rule => rule policy.AddRule(rule => rule
.WithFixedWindow(TimeSpan.FromMinutes(30), maxCount: 5) .WithFixedWindow(TimeSpan.FromMinutes(30), maxCount: 5)
.PartitionBy(ctx => $"custom:{ctx.Parameter}")); .PartitionBy(ctx => Task.FromResult($"custom:{ctx.Parameter}")));
}); });
var policy = options.Policies["CustomPolicy"]; var policy = options.Policies["CustomPolicy"];
@ -242,11 +243,11 @@ public class OperationRateLimitingPolicyBuilder_Tests
{ {
policy.AddRule(rule => rule policy.AddRule(rule => rule
.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 5) .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 5)
.PartitionBy(ctx => $"by-ip:{ctx.Parameter}")); .PartitionBy(ctx => Task.FromResult($"by-ip:{ctx.Parameter}")));
policy.AddRule(rule => rule policy.AddRule(rule => rule
.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 5) .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 5)
.PartitionBy(ctx => $"by-device:{ctx.Parameter}")); .PartitionBy(ctx => Task.FromResult($"by-device:{ctx.Parameter}")));
}); });
var policy = options.Policies["MultiCustomPolicy"]; var policy = options.Policies["MultiCustomPolicy"];

Loading…
Cancel
Save