< Summary

Information
Class: OzonOrdersWeb.Authorization.AutoPageAuthorizationFilter
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Authorization/AutoPageAuthorizationFilter.cs
Line coverage
44%
Covered lines: 39
Uncovered lines: 49
Coverable lines: 88
Total lines: 180
Line coverage: 44.3%
Branch coverage
25%
Covered branches: 22
Total branches: 88
Branch coverage: 25%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
OnAuthorizationAsync()40.47%2944247.72%
IsServiceEndpoint(...)66.66%7666.66%
BuildKey(...)0%620%
InferAccessType(...)0%1056320%
GetPageRegistryAsync()0%14414.28%
InvalidatePageRegistry(...)100%210%
get_Id()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Authorization/AutoPageAuthorizationFilter.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Mvc;
 4using Microsoft.AspNetCore.Mvc.Abstractions;
 5using Microsoft.AspNetCore.Mvc.Controllers;
 6using Microsoft.AspNetCore.Mvc.Filters;
 7using Microsoft.EntityFrameworkCore;
 8using Microsoft.Extensions.Caching.Memory;
 9using Microsoft.Extensions.Logging;
 10using Microsoft.Extensions.Options;
 11using OzonDomains.Models.Authorization.Enums;
 12using OzonRepositories.Context;
 13using Services.Authorization;
 14using Services.Authorization.Services;
 15
 16namespace OzonOrdersWeb.Authorization;
 17
 18/// <summary>
 19/// Глобальный фильтр: если действие не помечено [AllowAnonymous] и не имеет [PageAuthorize],
 20/// автоматически определяет код страницы по Area/Controller/Action и проверяет доступ.
 21/// </summary>
 22public sealed class AutoPageAuthorizationFilter : IAsyncAuthorizationFilter
 23{
 24    private readonly ApplicationDbContext _db;
 25    private readonly IPermissionService _permissions;
 26    private readonly IMemoryCache _cache;
 27    private readonly ILogger<AutoPageAuthorizationFilter> _logger;
 28    private readonly AppAuthorizationOptions _options;
 29
 30    private const string PageRegistryCacheKey = "__auth_page_registry";
 31
 332    public AutoPageAuthorizationFilter(
 333        ApplicationDbContext db,
 334        IPermissionService permissions,
 335        IMemoryCache cache,
 336        ILogger<AutoPageAuthorizationFilter> logger,
 337        IOptions<AppAuthorizationOptions> options)
 38    {
 339        _db = db;
 340        _permissions = permissions;
 341        _cache = cache;
 342        _logger = logger;
 343        _options = options.Value;
 344    }
 45
 46    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
 47    {
 348        if (!_options.EnableAutoPageFilter) return;
 49
 350        var endpoint = context.HttpContext.GetEndpoint();
 351        if (endpoint is null) return;
 52
 53        // Корневую страницу не обрабатываем правилами AppPage.
 54        // Иначе при незарегистрированном root роуте можно получить Forbid на "/".
 355        var requestPath = context.HttpContext.Request.Path.Value;
 356        if (string.IsNullOrEmpty(requestPath) || requestPath == "/")
 057            return;
 58
 59        // AllowAnonymous — пропускаем
 360        if (endpoint.Metadata.GetMetadata<AllowAnonymousAttribute>() is not null)
 061            return;
 62
 63        // Служебный AJAX-эндпоинт — не страница, прав на него не выдают. Роли из [Authorize] остаются.
 364        if (IsServiceEndpoint(endpoint, context.ActionDescriptor))
 265            return;
 66
 67        // Явный [PageAuthorize] — записываем в Items и доверяем policy
 168        var explicitPa = endpoint.Metadata.GetMetadata<PageAuthorizeAttribute>();
 169        if (explicitPa is not null)
 70        {
 071            context.HttpContext.Items[PageAuthorizeAttribute.PageCodeItemKey] = explicitPa.PageCode;
 072            context.HttpContext.Items[PageAuthorizeAttribute.AccessTypeItemKey] = explicitPa.Access;
 073            return;
 74        }
 75
 76        // Без атрибута — только для аутентифицированных проверяем авто-страницу
 177        if (context.HttpContext.User?.Identity?.IsAuthenticated != true) return;
 78
 79        // Быстрый bypass для системных администраторов (SuperAdmin AppRole или Identity-роль Admin).
 180        var user = context.HttpContext.User;
 181        if (user.IsInRole(AuthorizationConstants.SuperAdminRoleName)
 182            || user.IsInRole("Admin")
 383            || user.HasClaim(c => c.Type == PermissionClaimsTransformation.SuperAdminClaimType && c.Value == "true"))
 084            return;
 85
 186        var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
 187        if (descriptor is null) return;
 88
 189        var area = descriptor.RouteValues.TryGetValue("area", out var a) ? a ?? string.Empty : string.Empty;
 190        var controller = descriptor.ControllerName;
 191        var action = descriptor.ActionName;
 92
 193        var registry = await GetPageRegistryAsync(context.HttpContext.RequestAborted);
 094        var key = BuildKey(area, controller, action);
 095        if (!registry.TryGetValue(key, out var pageInfo))
 96        {
 097            if (_options.AllowUnregisteredPages) return;
 098            _logger.LogWarning("Доступ запрещён: страница {Key} не зарегистрирована", key);
 099            context.Result = new ForbidResult();
 0100            return;
 101        }
 102
 0103        var userId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 0104        if (string.IsNullOrWhiteSpace(userId))
 105        {
 0106            context.Result = new ForbidResult();
 0107            return;
 108        }
 109
 0110        var accessType = InferAccessType(context.HttpContext.Request.Method, action, controller);
 111
 0112        context.HttpContext.Items[PageAuthorizeAttribute.PageCodeItemKey] = pageInfo.Code;
 0113        context.HttpContext.Items[PageAuthorizeAttribute.AccessTypeItemKey] = accessType;
 114
 0115        var allowed = await _permissions.HasPageAccessAsync(userId, pageInfo.Code, accessType, context.HttpContext.Reque
 0116        if (!allowed)
 117        {
 0118            _logger.LogInformation("Отказ в доступе: {User} -> {Page} ({Access})", userId, pageInfo.Code, accessType);
 0119            context.Result = new ForbidResult();
 120        }
 2121    }
 122
 123    /// <summary>
 124    /// [ServiceEndpoint] на действии или контроллере. Смотрим и метаданные эндпоинта, и сам метод —
 125    /// чтобы не зависеть от того, как атрибут попал в маршрут.
 126    /// </summary>
 127    private static bool IsServiceEndpoint(Endpoint endpoint, ActionDescriptor actionDescriptor)
 128    {
 3129        if (endpoint.Metadata.GetMetadata<ServiceEndpointAttribute>() is not null)
 0130            return true;
 131
 3132        if (actionDescriptor is not ControllerActionDescriptor cad)
 0133            return false;
 134
 3135        return cad.MethodInfo.GetCustomAttributes(typeof(ServiceEndpointAttribute), inherit: true).Length > 0
 3136               || cad.ControllerTypeInfo.GetCustomAttributes(typeof(ServiceEndpointAttribute), inherit: true).Length > 0
 137    }
 138
 139    private static string BuildKey(string? area, string controller, string action)
 0140        => $"{(area ?? string.Empty).ToLowerInvariant()}|{controller.ToLowerInvariant()}|{action.ToLowerInvariant()}";
 141
 142    /// <summary>По HTTP-методу + имени действия догадаться о типе доступа.</summary>
 143    private static AccessType InferAccessType(string httpMethod, string actionName, string controllerName)
 144    {
 0145        var lower = actionName.ToLowerInvariant();
 0146        if (lower.Contains("export") || lower.Contains("download")) return AccessType.Export;
 0147        if (lower.StartsWith("delete") || lower.Contains("remove")) return AccessType.Delete;
 0148        if (lower.StartsWith("create") || lower.Contains("add")) return AccessType.Create;
 0149        if (lower.StartsWith("edit") || lower.StartsWith("update") || lower.Contains("save"))
 0150            return AccessType.Edit;
 151        // Проведение транзакции (POST) в пресетах ролей выдаётся как CanCreate, не CanEdit.
 0152        if (httpMethod == "POST"
 0153            && controllerName.EndsWith("Transaction", StringComparison.OrdinalIgnoreCase)
 0154            && !controllerName.Equals("Transaction", StringComparison.OrdinalIgnoreCase))
 155        {
 0156            return AccessType.Create;
 157        }
 0158        if (httpMethod == "POST" || httpMethod == "PUT" || httpMethod == "PATCH") return AccessType.Edit;
 0159        if (httpMethod == "DELETE") return AccessType.Delete;
 0160        return AccessType.View;
 161    }
 162
 163    private async Task<Dictionary<string, PageInfo>> GetPageRegistryAsync(CancellationToken ct)
 164    {
 1165        if (_cache.TryGetValue(PageRegistryCacheKey, out Dictionary<string, PageInfo>? cached) && cached is not null)
 0166            return cached;
 167
 0168        var pages = await _db.AppPages.AsNoTracking().Where(p => p.IsActive).ToListAsync(ct);
 0169        var dict = pages.ToDictionary(
 0170            p => BuildKey(p.Area, p.Controller, p.Action),
 0171            p => new PageInfo(p.Id, p.Code));
 172
 0173        _cache.Set(PageRegistryCacheKey, dict, TimeSpan.FromMinutes(5));
 0174        return dict;
 0175    }
 176
 0177    public static void InvalidatePageRegistry(IMemoryCache cache) => cache.Remove(PageRegistryCacheKey);
 178
 0179    private sealed record PageInfo(int Id, string Code);
 180}