How can I reuse AuthorizationHandler logic for different AuthorizationRequirements?

64 Views Asked by At

I am trying to find a way to imperatively authorize resources in an asp.net razor pages application. So far, I have followed the guide found in this link: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-8.0. But, I am running into a problem where I want to be able to reuse authentication logic for various different requirements, without having to define a new class which inherits that specific requirement.

For example, I have a location object that I want to be viewable under the following conditions:

  1. If the user is of the role Admin or Master, or
  2. If the user is a part of the Zone which the Location is also a part of.

I have successfully built a requirement, and two handlers to implement this logic as follows:

public class IsAdminOrHigherHandler : AuthorizationHandler<AccessLocationRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AccessLocationRequirement requirement)
    {
        if (context.User.IsInRole(SD.Roles.Admin.ToString()) || context.User.IsInRole(SD.Roles.Master.ToString()))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

public class InLocationsZoneHandler : AuthorizationHandler<AccessLocationRequirement, Location>
{
    private readonly IUserService _userService;
    public InLocationsZoneHandler(IUserService userService)
    {
        _userService = userService;
    }
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AccessLocationRequirement requirement,
        Location resource)
    {
        User? user = await _userService.GetUserFromContextAsync();
        if (user == null)
        {
            context.Fail();
            return;
        }
        if (user.Zones.Any(z => z.Id == resource.Zone.Id))
        {
            context.Succeed(requirement);
            return;
        }
    }
}

I then register a policy that takes this requirement:

    options.AddPolicy("CanAccessLocation",
        policy => policy.AddRequirements(new AccessLocationRequirement()));

This works great, but my question is that when I want to define a new policy for defining rules for editing a location, I want to avoid rewriting this logic just because the requirement has changed. For example, my condition for editing a location are as follows:

  1. If the user is Admin or Master, or
  2. If the user is a part of the Zone which the Location is also a part of AND they have a role of ZoneManager.

As we can see, to edit a location, you may fulfill the requirement simply by fulfilling the logic laid out in the IsAdminOrHigherHandler method, but because the IsAdminOrHigherHandler inherits from AuthorizationHandler<AccessLocationRequirement> and not AuthorizationHandler<EditLocationRequirement>, we would have to rewrite this method.

How can I share Authorization Handler logic for different Requirements? Or is there a better method?

1

There are 1 best solutions below

4
On

You could have BaseRequirement which will be inherited by your AccessLocationRequirement and EditLocationRequirement. Then your IsAdminOrHigherHandler would impelement the BaseRequirement.

That way your every requirement would inherit from your base which handler holds logic for Admin and Master roles.

Something like this:

public class BaseRequirement : IAuthorizationRequirement
{
}

Then you gonna have:

public class AccessLocationRequirement : BaseRequirement 
{

}

and your other requirement:

public class EditAccessLocationRequirement : BaseRequirement 
{

}

Then your handler code would be:

public class IsAdminOrHigherHandler : AuthorizationHandler<BaseRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        BaseRequirement requirement)
    {
        if (context.User.IsInRole(SD.Roles.Admin.ToString()) || context.User.IsInRole(SD.Roles.Master.ToString()))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

public class InLocationsZoneHandler : AuthorizationHandler<AccessLocationRequirement, Location>
{
    private readonly IUserService _userService;
    public InLocationsZoneHandler(IUserService userService)
    {
        _userService = userService;
    }
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AccessLocationRequirement requirement,
        Location resource)
    {
        User? user = await _userService.GetUserFromContextAsync();
        if (user == null)
        {
            context.Fail();
            return;
        }
        if (user.Zones.Any(z => z.Id == resource.Zone.Id))
        {
            context.Succeed(requirement);
            return;
        }
    }
}
// And code of your other handler 

And just register handlers and policy:

services.AddScoped<IAuthorizationHandler, IsAdminOrHigherHandler>();
services.AddScoped<IAuthorizationHandler, InLocationsZoneHandler >();
//and your other handler

and just add your policies with requirements.