diff --git a/common/ASC.Api.Core/ASC.Api.Core.csproj b/common/ASC.Api.Core/ASC.Api.Core.csproj
index b9bace788a..43e65541d4 100644
--- a/common/ASC.Api.Core/ASC.Api.Core.csproj
+++ b/common/ASC.Api.Core/ASC.Api.Core.csproj
@@ -26,6 +26,8 @@
+
+
diff --git a/common/ASC.Api.Core/Auth/AuthHandler.cs b/common/ASC.Api.Core/Auth/AuthHandler.cs
index ca6dcc5856..9029d65e2a 100644
--- a/common/ASC.Api.Core/Auth/AuthHandler.cs
+++ b/common/ASC.Api.Core/Auth/AuthHandler.cs
@@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
+using Role = ASC.Common.Security.Authorizing.Role;
+
namespace ASC.Api.Core.Auth;
[Scope]
diff --git a/common/ASC.Api.Core/Core/BaseStartup.cs b/common/ASC.Api.Core/Core/BaseStartup.cs
index 6ab1c9a96c..66b6f72a8f 100644
--- a/common/ASC.Api.Core/Core/BaseStartup.cs
+++ b/common/ASC.Api.Core/Core/BaseStartup.cs
@@ -100,6 +100,88 @@ public abstract class BaseStartup
}
});
+ var redisOptions = _configuration.GetSection("Redis").Get().ConfigurationOptions;
+ var connectionMultiplexer = ConnectionMultiplexer.Connect(redisOptions);
+
+ services.AddRateLimiter(options =>
+ {
+ options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
+ PartitionedRateLimiter.Create(httpContext =>
+ {
+ var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
+
+ if (userId == null)
+ {
+ return RateLimitPartition.GetNoLimiter("no_limiter");
+ }
+
+ var permitLimit = 1500;
+
+ string partitionKey;
+
+ partitionKey = $"fw_{userId}";
+
+ return RedisRateLimitPartition.GetFixedWindowRateLimiter(partitionKey, key => new RedisFixedWindowRateLimiterOptions
+ {
+ PermitLimit = permitLimit,
+ Window = TimeSpan.FromMinutes(1),
+ ConnectionMultiplexerFactory = () => connectionMultiplexer
+ });
+ }),
+ PartitionedRateLimiter.Create(httpContext =>
+ {
+ var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
+ string partitionKey;
+ int permitLimit;
+
+ if (userId == null)
+ {
+ return RateLimitPartition.GetNoLimiter("no_limiter");
+ }
+
+ if (String.Compare(httpContext.Request.Method, "GET", true) == 0)
+ {
+ permitLimit = 50;
+ partitionKey = $"cr_read_{userId}";
+
+ }
+ else
+ {
+ permitLimit = 15;
+ partitionKey = $"cr_write_{userId}";
+ }
+
+ return RedisRateLimitPartition.GetConcurrencyRateLimiter(partitionKey, key => new RedisConcurrencyRateLimiterOptions
+ {
+ PermitLimit = permitLimit,
+ QueueLimit = 0,
+ ConnectionMultiplexerFactory = () => connectionMultiplexer
+ });
+ }
+ ));
+
+ options.AddPolicy("sensitive_api", httpContext => {
+ var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
+
+ if (userId == null)
+ {
+ return RateLimitPartition.GetNoLimiter("no_limiter");
+ }
+
+ var permitLimit = 5;
+ var partitionKey = $"sensitive_api_{userId}";
+
+ return RedisRateLimitPartition.GetFixedWindowRateLimiter(partitionKey, key => new RedisFixedWindowRateLimiterOptions
+ {
+ PermitLimit = permitLimit,
+ Window = TimeSpan.FromMinutes(1),
+ ConnectionMultiplexerFactory = () => connectionMultiplexer
+ });
+ });
+
+ options.OnRejected = (context, ct) => RateLimitMetadata.OnRejected(context.HttpContext, context.Lease, ct);
+ });
+
services.AddScoped();
services.AddBaseDbContextPool()
@@ -311,6 +393,8 @@ public abstract class BaseStartup
app.UseAuthentication();
+ app.UseRateLimiter();
+
app.UseAuthorization();
app.UseCultureMiddleware();
@@ -346,6 +430,8 @@ public abstract class BaseStartup
await context.Response.WriteAsync($"{Environment.MachineName} running {CustomHealthCheck.Running}");
});
});
+
+
}
public void ConfigureContainer(ContainerBuilder builder)
diff --git a/common/ASC.Api.Core/GlobalUsings.cs b/common/ASC.Api.Core/GlobalUsings.cs
index 8fa7cc301f..dd9f820332 100644
--- a/common/ASC.Api.Core/GlobalUsings.cs
+++ b/common/ASC.Api.Core/GlobalUsings.cs
@@ -41,6 +41,7 @@ global using System.Text.Encodings.Web;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
+global using System.Threading.RateLimiting;
global using System.Web;
global using System.Xml.Linq;
@@ -150,6 +151,10 @@ global using NLog.Web;
global using RabbitMQ.Client;
+global using RedisRateLimiting;
+global using RedisRateLimiting.AspNetCore;
+
+global using StackExchange.Redis;
global using StackExchange.Redis.Extensions.Core.Configuration;
global using StackExchange.Redis.Extensions.Newtonsoft;
diff --git a/products/ASC.People/Server/Api/UserController.cs b/products/ASC.People/Server/Api/UserController.cs
index cd1373cd02..bc5fffa717 100644
--- a/products/ASC.People/Server/Api/UserController.cs
+++ b/products/ASC.People/Server/Api/UserController.cs
@@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
+using Microsoft.AspNetCore.RateLimiting;
+
namespace ASC.People.Api;
public class UserController : PeopleControllerBase
@@ -1221,7 +1223,8 @@ public class UserController : PeopleControllerBase
/// false
[AllowNotPayment]
[AllowAnonymous]
- [HttpPost("password")]
+ [HttpPost("password")]
+ [EnableRateLimiting("sensitive_api")]
public async Task