| | | 1 | | using System.Threading.RateLimiting; |
| | | 2 | | using Microsoft.AspNetCore.Authentication; |
| | | 3 | | using Microsoft.AspNetCore.Authorization; |
| | | 4 | | using Microsoft.AspNetCore.Mvc; |
| | | 5 | | using Microsoft.AspNetCore.RateLimiting; |
| | | 6 | | using Microsoft.Extensions.DependencyInjection; |
| | | 7 | | using Microsoft.Extensions.Options; |
| | | 8 | | using OzonOrdersWeb.Areas.Admin.Services; |
| | | 9 | | using OzonOrdersWeb.Authorization; |
| | | 10 | | using OzonOrdersWeb.Authorization.Discovery; |
| | | 11 | | using OzonOrdersWeb.Authorization.Seed; |
| | | 12 | | using OzonRepositories.Authorization; |
| | | 13 | | using OzonOrdersWeb.Observability; |
| | | 14 | | using Services.Authorization; |
| | | 15 | | using Services.Authorization.Services; |
| | | 16 | | |
| | | 17 | | namespace OzonOrdersWeb.Extensions; |
| | | 18 | | |
| | | 19 | | /// <summary> |
| | | 20 | | /// Регистрация сервисов новой системы авторизации. |
| | | 21 | | /// </summary> |
| | | 22 | | public static class AuthorizationExtensions |
| | | 23 | | { |
| | | 24 | | public static IServiceCollection AddAppAuthorization(this IServiceCollection services, IConfiguration configuration) |
| | | 25 | | { |
| | 2 | 26 | | services.Configure<Services.Authorization.AppAuthorizationOptions>(configuration.GetSection(Services.Authorizati |
| | 2 | 27 | | services.AddMemoryCache(); |
| | 2 | 28 | | services.AddHttpContextAccessor(); |
| | | 29 | | |
| | | 30 | | // Основные сервисы |
| | 2 | 31 | | services.AddScoped<IPermissionService, PermissionService>(); |
| | 2 | 32 | | services.AddScoped<IMenuService, MenuService>(); |
| | 2 | 33 | | services.AddScoped<IDataScopeService, DataScopeService>(); |
| | 2 | 34 | | services.AddScoped<DataScopeFilterSummaryService>(); |
| | 2 | 35 | | services.AddScoped<IAuditService, AuditService>(); |
| | 2 | 36 | | services.AddScoped<IScopeFilterValuesService, ScopeFilterValuesService>(); |
| | 2 | 37 | | services.AddScoped<OzonRepositories.Authorization.IUserScopeProvider, CurrentUserScopeProvider>(); |
| | | 38 | | |
| | | 39 | | // Discovery + seed |
| | 2 | 40 | | services.AddScoped<IPageDiscoveryService, PageDiscoveryService>(); |
| | 2 | 41 | | services.AddScoped<IFieldDiscoveryService, FieldDiscoveryService>(); |
| | 2 | 42 | | services.AddScoped<AuthorizationSeeder>(); |
| | | 43 | | |
| | | 44 | | // Policy + handler |
| | 2 | 45 | | services.AddScoped<IAuthorizationHandler, PageAccessHandler>(); |
| | 2 | 46 | | services.AddAuthorization(options => |
| | 2 | 47 | | { |
| | 0 | 48 | | options.AddPolicy(AuthorizationConstants.PolicyName, policy => |
| | 0 | 49 | | policy.Requirements.Add(new PageAccessRequirement())); |
| | 2 | 50 | | }); |
| | | 51 | | |
| | | 52 | | // Claims transformation |
| | 2 | 53 | | services.AddScoped<IClaimsTransformation, PermissionClaimsTransformation>(); |
| | | 54 | | |
| | | 55 | | // Глобальные фильтры + sanitizer |
| | 2 | 56 | | services.Configure<MvcOptions>(options => |
| | 2 | 57 | | { |
| | 0 | 58 | | options.Filters.Add<AutoPageAuthorizationFilter>(); |
| | 0 | 59 | | options.Filters.Add<FieldPermissionSanitizerFilter>(); |
| | 2 | 60 | | }); |
| | 2 | 61 | | services.AddScoped<AutoPageAuthorizationFilter>(); |
| | 2 | 62 | | services.AddScoped<FieldPermissionSanitizerFilter>(); |
| | | 63 | | |
| | 2 | 64 | | services.AddSingleton<AuthRateLimiterPolicy>(); |
| | | 65 | | |
| | | 66 | | // Rate limiting: политика admin + политика auth (для Login.cshtml.cs) |
| | 2 | 67 | | services.AddRateLimiter(options => |
| | 2 | 68 | | { |
| | 0 | 69 | | options.RejectionStatusCode = 429; |
| | 2 | 70 | | |
| | 0 | 71 | | options.AddPolicy(AdminRateLimitPolicy, httpCtx => |
| | 0 | 72 | | { |
| | 0 | 73 | | var opts = httpCtx.RequestServices.GetRequiredService<IOptions<AppAuthorizationOptions>>().Value; |
| | 0 | 74 | | // Матрица прав шлёт много AJAX подряд — лимит должен быть из конфига (по умолчанию высокий). |
| | 0 | 75 | | var permitLimit = Math.Clamp(opts.AdminRateLimitPerMinute, 120, 10_000); |
| | 0 | 76 | | var key = httpCtx.User?.Identity?.Name |
| | 0 | 77 | | ?? httpCtx.Connection.RemoteIpAddress?.ToString() |
| | 0 | 78 | | ?? "anonymous"; |
| | 0 | 79 | | return RateLimitPartition.GetSlidingWindowLimiter(key, _ => new SlidingWindowRateLimiterOptions |
| | 0 | 80 | | { |
| | 0 | 81 | | PermitLimit = permitLimit, |
| | 0 | 82 | | SegmentsPerWindow = 6, |
| | 0 | 83 | | Window = TimeSpan.FromMinutes(1), |
| | 0 | 84 | | AutoReplenishment = true, |
| | 0 | 85 | | QueueLimit = 0, |
| | 0 | 86 | | }); |
| | 0 | 87 | | }); |
| | 2 | 88 | | |
| | 2 | 89 | | // Политика для страницы входа и других форм аутентификации: |
| | 2 | 90 | | // Строгая защита от брут-форса паролей для мутирующих запросов (POST), |
| | 2 | 91 | | // с комфортным независимым лимитом для чтения/открытия страниц (GET/HEAD/OPTIONS). |
| | 0 | 92 | | options.AddPolicy<string, AuthRateLimiterPolicy>(AuthRateLimitPolicy); |
| | 2 | 93 | | |
| | 2 | 94 | | // Название организации по почте отдаётся без входа, поэтому перебор почт режем отдельным лимитом. |
| | 0 | 95 | | options.AddPolicy(PartnerLookupRateLimitPolicy, httpCtx => |
| | 0 | 96 | | { |
| | 0 | 97 | | var ip = httpCtx.Connection.RemoteIpAddress?.ToString() ?? "anonymous"; |
| | 0 | 98 | | return RateLimitPartition.GetFixedWindowLimiter($"partner-lookup:{ip}", _ => new FixedWindowRateLimiterO |
| | 0 | 99 | | { |
| | 0 | 100 | | PermitLimit = 10, |
| | 0 | 101 | | Window = TimeSpan.FromMinutes(1), |
| | 0 | 102 | | AutoReplenishment = true, |
| | 0 | 103 | | QueueLimit = 0 |
| | 0 | 104 | | }); |
| | 0 | 105 | | }); |
| | 2 | 106 | | |
| | 0 | 107 | | options.AddPolicy(ClientDiagnosticsRateLimitPolicy, httpCtx => |
| | 0 | 108 | | { |
| | 0 | 109 | | var configuredLimit = configuration.GetValue<int?>( |
| | 0 | 110 | | $"{ObservabilityOptions.SectionName}:ClientEventsPerMinute") ?? 60; |
| | 0 | 111 | | var ip = httpCtx.Connection.RemoteIpAddress?.ToString() ?? "anonymous"; |
| | 0 | 112 | | return RateLimitPartition.GetFixedWindowLimiter(ip, _ => new FixedWindowRateLimiterOptions |
| | 0 | 113 | | { |
| | 0 | 114 | | PermitLimit = Math.Clamp(configuredLimit, 10, 600), |
| | 0 | 115 | | Window = TimeSpan.FromMinutes(1), |
| | 0 | 116 | | AutoReplenishment = true, |
| | 0 | 117 | | QueueLimit = 0 |
| | 0 | 118 | | }); |
| | 0 | 119 | | }); |
| | 2 | 120 | | }); |
| | | 121 | | |
| | 2 | 122 | | return services; |
| | | 123 | | } |
| | | 124 | | |
| | | 125 | | public const string AdminRateLimitPolicy = "AdminRateLimit"; |
| | | 126 | | public const string AuthRateLimitPolicy = "auth"; |
| | | 127 | | public const string PartnerLookupRateLimitPolicy = "partner-lookup"; |
| | | 128 | | public const string ClientDiagnosticsRateLimitPolicy = "client-diagnostics"; |
| | | 129 | | |
| | | 130 | | /// <summary> |
| | | 131 | | /// Выполнить seed после запуска приложения. |
| | | 132 | | /// </summary> |
| | | 133 | | public static async Task UseAppAuthorizationSeedAsync(this IApplicationBuilder app) |
| | | 134 | | { |
| | 0 | 135 | | using var scope = app.ApplicationServices.CreateScope(); |
| | 0 | 136 | | var seeder = scope.ServiceProvider.GetRequiredService<AuthorizationSeeder>(); |
| | 0 | 137 | | await seeder.SeedAsync(); |
| | 0 | 138 | | } |
| | | 139 | | } |