Using resource-based authorization on a list in ASP.NET Core/Entity Framework Core 3.1

Viewed 551

Premise

The documented method to apply resource-based authorization in ASP.Net Core is to register an AuthorizationHandler, define each OperationAuthorizationRequirement, then check access to resources using the AuthorizeAsync() method of an injected IAuthorizationHandler. (Reference docs)

This is all well and good for checking operations against individual records, but my question is how best to authorize against many resources at once (e.g. checking read permission against a list of records for an index page)?

Example

Let's say we have a list of orders, and we want to provide users with a list of the ones they have created. To do this with the practice defined by Microsoft's docs, we would first create some static OperationAuthorizationRequirement objects:

public static class CrudOperations
{
    public static OperationAuthorizationRequirement Create =
        new OperationAuthorizationRequirement { Name = nameof(Create) };
    public static OperationAuthorizationRequirement Read =
        new OperationAuthorizationRequirement { Name = nameof(Read) };
    public static OperationAuthorizationRequirement Update =
        new OperationAuthorizationRequirement { Name = nameof(Update) };
    public static OperationAuthorizationRequirement Delete =
        new OperationAuthorizationRequirement { Name = nameof(Delete) };
}

..and then create our AuthorizationHandler:

public class OrderCreatorAuthorizationHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Order>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        OperationAuthorizationRequirement requirement, 
        InspectionManagementUser resource)
    {
        if (context.User == null || resource == null)
        {
            return Task.CompletedTask;
        }

        var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (resource.CreatedById == currentUserId
            && requirement.Name == CrudOperations.Read.Name) {
                context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

This is registered as a service in Startup.cs, and is ready to go. In our view logic, we can use our new handler to get a filtered list of orders as such:

//_context is an injected instance of the application's DatabaseContext
//_authorizationService is an injected instance of IAuthorizationService

var allOrders = await _context.Orders.ToListAsync();

var filteredOrders = allOrders
    .Where(o => _authorizationService.AuthorizeAsync(User, o, CrudOperations.Read).Result.Succeeded);

This will work just fine, but to me seems extremely computationally expensive as each record is checked separately in memory. This would increase even further as the logic for the authorization handler got more complex (for example, if it involved a database call).

It would presumably be far more efficient to have the database engine filter the list for us as follows:

var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier);

var filteredOrders = await _context.Orders
    .Where(o => o.CreatedById == currentUserId)
    .ToListAsync();

This will execute faster, but we've now bypassed our authorization logic completely. If we later decide to change the restrictions in our AuthorizationHandler we must also remember to change it here and anywhere else we use this method. If you ask me that rather seems to defeat the purpose of separating this authorization code out in the first place.

Is there a neat solution to this problem that I'm missing? Any advice or guidance on best practice would be much appreciated.

0 Answers
Related