| | | 1 | | using System.Security.Claims; |
| | | 2 | | using Microsoft.AspNetCore.Mvc.Filters; |
| | | 3 | | using Microsoft.Extensions.DependencyInjection; |
| | | 4 | | using OzonDomains.Models.Authorization.Enums; |
| | | 5 | | using Services.Authorization.Services; |
| | | 6 | | |
| | | 7 | | namespace OzonOrdersWeb.Authorization; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// ActionFilter, защищающий от mass assignment: после биндинга для аргументов, помеченных [DiscoverFields], |
| | | 11 | | /// обнуляет поля, на которые у пользователя нет CanWrite. |
| | | 12 | | /// </summary> |
| | | 13 | | public sealed class FieldPermissionSanitizerFilter : IAsyncActionFilter |
| | | 14 | | { |
| | | 15 | | public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) |
| | | 16 | | { |
| | 0 | 17 | | var http = context.HttpContext; |
| | 0 | 18 | | var userId = http.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; |
| | | 19 | | |
| | 0 | 20 | | if (!string.IsNullOrWhiteSpace(userId)) |
| | | 21 | | { |
| | 0 | 22 | | var perms = http.RequestServices.GetRequiredService<IPermissionService>(); |
| | 0 | 23 | | var logger = http.RequestServices.GetRequiredService<ILogger<FieldPermissionSanitizerFilter>>(); |
| | 0 | 24 | | var sanitizedByEntity = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase); |
| | | 25 | | |
| | 0 | 26 | | foreach (var (name, arg) in context.ActionArguments) |
| | | 27 | | { |
| | 0 | 28 | | if (arg is null) continue; |
| | 0 | 29 | | var attr = (DiscoverFieldsAttribute?)Attribute.GetCustomAttribute(arg.GetType(), typeof(DiscoverFieldsAt |
| | 0 | 30 | | if (attr is null) continue; |
| | | 31 | | |
| | 0 | 32 | | if (!sanitizedByEntity.TryGetValue(attr.EntityName, out var readOnly)) |
| | | 33 | | { |
| | 0 | 34 | | var fields = await perms.GetUserFieldPermissionsAsync(userId!, attr.EntityName, http.RequestAborted) |
| | | 35 | | readOnly = fields.Where(f => !f.CanWrite).Select(f => f.FieldName).ToHashSet(StringComparer.OrdinalI |
| | 0 | 36 | | sanitizedByEntity[attr.EntityName] = readOnly; |
| | | 37 | | } |
| | 0 | 38 | | if (readOnly.Count == 0) continue; |
| | | 39 | | |
| | 0 | 40 | | ZeroOutForbiddenProperties(arg, readOnly, logger); |
| | 0 | 41 | | } |
| | 0 | 42 | | } |
| | | 43 | | |
| | 0 | 44 | | await next(); |
| | 0 | 45 | | } |
| | | 46 | | |
| | | 47 | | private static void ZeroOutForbiddenProperties( |
| | | 48 | | object model, HashSet<string> forbiddenNames, ILogger<FieldPermissionSanitizerFilter> logger) |
| | | 49 | | { |
| | 0 | 50 | | var type = model.GetType(); |
| | 0 | 51 | | foreach (var prop in type.GetProperties().Where(p => p.CanWrite && p.CanRead)) |
| | | 52 | | { |
| | 0 | 53 | | if (!forbiddenNames.Contains(prop.Name)) continue; |
| | 0 | 54 | | var pt = prop.PropertyType; |
| | 0 | 55 | | object? def = pt.IsValueType && Nullable.GetUnderlyingType(pt) == null |
| | 0 | 56 | | ? Activator.CreateInstance(pt) |
| | 0 | 57 | | : null; |
| | 0 | 58 | | try { prop.SetValue(model, def); } |
| | 0 | 59 | | catch (Exception exception) |
| | | 60 | | { |
| | 0 | 61 | | logger.LogWarning(exception, "Не удалось очистить запрещённое поле {FieldName} у модели {ModelType}", |
| | 0 | 62 | | prop.Name, type.FullName); |
| | 0 | 63 | | } |
| | | 64 | | } |
| | 0 | 65 | | } |
| | | 66 | | } |