Как использовать основанную на ресурсах авторизацию ASP.NET Core без дублирования кода if / else везде - PullRequest
7 голосов
/ 27 июня 2019

У меня есть dotnet core 2.2 api с некоторыми контроллерами и методами действий, которые необходимо авторизовать на основании заявки пользователя и ресурса, к которому осуществляется доступ. По сути, каждый пользователь может иметь 0 или несколько «ролей» для каждого ресурса. Все это делается с помощью утверждений ASP.NET Identity.

Итак, я понимаю, что мне нужно использовать Ресурсная авторизация . Но оба примера в основном идентичны и требуют явной необходимости логики if / else для каждого метода действия, чего я и стараюсь избегать.

Я хочу иметь возможность сделать что-то вроде

[Authorize("Admin")] // or something similar
public async Task<IActionResult> GetSomething(int resourceId)
{
   var resource = await SomeRepository.Get(resourceId);

   return Json(resource);
}

И где-то еще определите логику авторизации как политику / фильтр / требование / что угодно и имеют доступ как к заявкам текущего пользователя, так и к параметру resourceId, полученному конечной точкой. Таким образом, я могу видеть, есть ли у пользователя утверждение, которое означает, что у него есть роль «Администратор» для этого конкретного resourceId.

Ответы [ 3 ]

3 голосов
/ 13 июля 2019

Редактировать: на основе обратной связи, чтобы сделать ее динамичной

Главное в RBAC и утверждениях в .NET - это создать свой ClaimsIdentity и затем позволить фреймворку выполнить свою работу.Ниже приведен пример промежуточного программного обеспечения, которое будет смотреть на параметр запроса «пользователь», а затем генерировать ClaimsPrincipal на основе словаря.

Чтобы избежать необходимости фактически подключаться к поставщику удостоверений, я создал промежуточное программное обеспечение, которое устанавливаетдо ClaimsPrincipal:

// **THIS CLASS IS ONLY TO DEMONSTRATE HOW THE ROLES NEED TO BE SETUP **
public class CreateFakeIdentityMiddleware
{
    private readonly RequestDelegate _next;

    public CreateFakeIdentityMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    private readonly Dictionary<string, string[]> _tenantRoles = new Dictionary<string, string[]>
    {
        ["tenant1"] = new string[] { "Admin", "Reader" },
        ["tenant2"] = new string[] { "Reader" },
    };

    public async Task InvokeAsync(HttpContext context)
    {
        // Assume this is the roles
        List<Claim> claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, "John"),
            new Claim(ClaimTypes.Email, "john@someemail.com")
        };

        foreach (KeyValuePair<string, string[]> tenantRole in _tenantRoles)
        {
            claims.AddRange(tenantRole.Value.Select(x => new Claim(ClaimTypes.Role, $"{tenantRole.Key}:{x}".ToLower())));
        }

        // Note: You need these for the AuthorizeAttribute.Roles    
        claims.AddRange(_tenantRoles.SelectMany(x => x.Value)
            .Select(x => new Claim(ClaimTypes.Role, x.ToLower())));

        context.User = new System.Security.Claims.ClaimsPrincipal(new ClaimsIdentity(claims,
            "Bearer"));

        await _next(context);
    }
}

Чтобы подключить это, просто используйте UseMiddleware метод расширения для IApplicationBuilder в вашем классе запуска.

app.UseMiddleware<RBACExampleMiddleware>();

Я создаю AuthorizationHandler, который будет искать параметр запроса «tenant» и либо успешно, либо с ошибкой в ​​зависимости от ролей.

public class SetTenantIdentityHandler : AuthorizationHandler<TenantRoleRequirement>
{
    public const string TENANT_KEY_QUERY_NAME = "tenant";

    private static readonly ConcurrentDictionary<string, string[]> _methodRoles = new ConcurrentDictionary<string, string[]>();

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TenantRoleRequirement requirement)
    {
        if (HasRoleInTenant(context))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }

    private bool HasRoleInTenant(AuthorizationHandlerContext context)
    {
        if (context.Resource is AuthorizationFilterContext authorizationFilterContext)
        {
            if (authorizationFilterContext.HttpContext
                .Request
                .Query
                .TryGetValue(TENANT_KEY_QUERY_NAME, out StringValues tenant)
                && !string.IsNullOrWhiteSpace(tenant))
            {
                if (TryGetRoles(authorizationFilterContext, tenant.ToString().ToLower(), out string[] roles))
                {
                    if (context.User.HasClaim(x => roles.Any(r => x.Value == r)))
                    {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private bool TryGetRoles(AuthorizationFilterContext authorizationFilterContext,
        string tenantId,
        out string[] roles)
    {
        string actionId = authorizationFilterContext.ActionDescriptor.Id;
        roles = null;

        if (!_methodRoles.TryGetValue(actionId, out roles))
        {
            roles = authorizationFilterContext.Filters
                .Where(x => x.GetType() == typeof(AuthorizeFilter))
                .Select(x => x as AuthorizeFilter)
                .Where(x => x != null)
                .Select(x => x.Policy)
                .SelectMany(x => x.Requirements)
                .Where(x => x.GetType() == typeof(RolesAuthorizationRequirement))
                .Select(x => x as RolesAuthorizationRequirement)
                .SelectMany(x => x.AllowedRoles)
                .ToArray();

            _methodRoles.TryAdd(actionId, roles);
        }

        roles = roles?.Select(x => $"{tenantId}:{x}".ToLower())
            .ToArray();

        return roles != null;
    }
}

TenantRoleRequirement - очень простой класс:

public class TenantRoleRequirement : IAuthorizationRequirement { }

Затем вы подключаете все в файле startup.cs следующим образом:

services.AddTransient<IAuthorizationHandler, SetTenantIdentityHandler>();

// Although this isn't used to generate the identity, it is needed
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.Audience = "https://localhost:5000/";
    options.Authority = "https://localhost:5000/identity/";
});

services.AddAuthorization(authConfig =>
{
    authConfig.AddPolicy(Policies.HasRoleInTenant, policyBuilder => {
        policyBuilder.RequireAuthenticatedUser();
        policyBuilder.AddRequirements(new TenantRoleRequirement());
    });
});

Метод выглядит следующим образом:

// TOOD: Move roles to a constants/globals
[Authorize(Policy = Policies.HasRoleInTenant, Roles = "admin")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { "value1", "value2" };
}

Ниже приведены тестовые сценарии:

  1. Положительный: https://localhost:44337/api/values?tenant=tenant1

  2. Отрицательный: https://localhost:44337/api/values?tenant=tenant2

  3. Отрицательный: https://localhost:44337/api/values

Ключевым моментом такого подхода является то, что я никогда неLy возвращает 403. Код устанавливает идентичность, а затем позволяет платформе обрабатывать результат.Это гарантирует, что аутентификация отделена от авторизации.

~ Ура

3 голосов
/ 10 июля 2019

Вы можете создать свой собственный атрибут, который будет проверять роль пользователя.Я сделал это в одном из моих приложений:

public sealed class RoleValidator : Attribute, IAuthorizationFilter
{
    private readonly IEnumerable<string> _roles;

    public RoleValidator(params string[] roles) => _roles = roles;

    public RoleValidator(string role) => _roles = new List<string> { role };

    public void OnAuthorization(AuthorizationFilterContext filterContext)
    {
        if (filterContext.HttpContext.User.Claims == null || filterContext.HttpContext.User.Claims?.Count() <= 0)
        {
            filterContext.Result = new UnauthorizedResult();
            return;
        }

        if (CheckUserRoles(filterContext.HttpContext.User.Claims))
            return;

        filterContext.Result = new ForbidResult();
    }

    private bool CheckUserRoles(IEnumerable<Claim> claims) =>
        JsonConvert.DeserializeObject<List<RoleDto>>(claims.FirstOrDefault(x => x.Type.Equals(ClaimType.Roles.ToString()))?.Value)
            .Any(x => _roles.Contains(x.Name));
}

Он получает роль пользователя из утверждений и проверяет, есть ли у пользователя надлежащая роль для получения этого ресурса.Вы можете использовать его следующим образом:

[RoleValidator("Admin")]

или лучше подходить с enum:

[RoleValidator(RoleType.Admin)]

или вы можете передать несколько ролей:

[RoleValidator(RoleType.User, RoleType.Admin)]

С этимВ решении также необходимо использовать стандартный атрибут Authorize

2 голосов
/ 10 июля 2019

Отредактировано на основе комментариев

Насколько я понимаю, вы хотите получить доступ к текущему пользователю (всю информацию, связанную с ним), роли (ролям), которые вы хотите указать для контроллера (или действия), и параметрам, полученным конечной точкой. Не пробовал для веб-API, но для ядра MVC asp.net. Вы можете достичь этого, используя AuthorizationHandler в авторизации на основе политик, и в сочетании с внедренным сервисом, специально созданным для определения доступа к ресурсам ролей.

Для этого сначала настройте политику в Startup.ConfigureServices:

services.AddAuthorization(options =>
{
    options.AddPolicy("UserResource", policy => policy.Requirements.Add( new UserResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IRoleResourceService, RoleResourceService>();

затем создайте UserResourceHandler:

public class UserResourceHandler : AuthorizationHandler<UserResourceRequirement>
{
    readonly IRoleResourceService _roleResourceService;

    public UserResourceHandler (IRoleResourceService r)
    {
        _roleResourceService = r;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement requirement)
    {
        if (context.Resource is AuthorizationFilterContext filterContext)
        {
            var area = (filterContext.RouteData.Values["area"] as string)?.ToLower();
            var controller = (filterContext.RouteData.Values["controller"] as string)?.ToLower();
            var action = (filterContext.RouteData.Values["action"] as string)?.ToLower();
            var id = (filterContext.RouteData.Values["id"] as string)?.ToLower();
            if (_roleResourceService.IsAuthorize(area, controller, action, id))
            {
                context.Succeed(requirement);
            }               
        }            
    }
}

Доступ к параметрам, полученным конечной точкой, достигается путем приведения context.Resource к AuthorizationFilterContext, чтобы мы могли получить к нему доступ RouteData. Что касается UserResourceRequirement, мы можем оставить его пустым.

public class UserResourceRequirement : IAuthorizationRequirement { }

Что касается IRoleResourceService, это простой класс обслуживания, поэтому мы можем внедрить в него все, что угодно. Этот сервис является заменой сопряжения Роли с действием в коде, поэтому нам не нужно указывать его в атрибуте действия. Таким образом, у нас может быть свобода выбора реализации, например: из базы данных, из файла конфигурации или в жестком коде.

Доступ к пользователю в RoleResourceService достигается путем введения IHttpContextAccessor. Обратите внимание: чтобы сделать IHttpContextAccessor инъекционным, добавьте services.AddHttpContextAccessor() в Startup.ConfigurationServices теле метода.

Вот пример получения информации из файла конфигурации:

public class RoleResourceService : IRoleResourceService
{
    readonly IConfiguration _config;
    readonly IHttpContextAccessor _accessor;
    readonly UserManager<AppUser> _userManager;

    public class RoleResourceService(IConfiguration c, IHttpContextAccessor a, UserManager<AppUser> u) 
    {
        _config = c;
        _accessor = a;
        _userManager = u;
    }

    public bool IsAuthorize(string area, string controller, string action, string id)
    {
        var roleConfig = _config.GetValue<string>($"RoleSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
        var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
        var userRoles = await _userManager.GetRolesAsync(appUser);
        // all of needed data are available now, do the logic of authorization
        return result;
    } 
}

Конечно, получить настройку из базы данных немного сложнее, но это можно сделать, поскольку мы можем ввести AppDbContext. Для жестко закодированного подхода существует множество способов сделать это.

После того, как все сделано, используйте политику для действия:

[Authorize(Policy = "UserResource")] //dont need Role name because of the RoleResourceService
public ActionResult<IActionResult> GetSomething(int resourceId)
{
    //existing code
}

Фактически, мы можем использовать политику «UserResource» для любых действий, которые мы хотим применить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...