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 SendUserPasswordAsync(MemberRequestDto inDto) { if (_authContext.IsAuthenticated)