2

We're implementing a new web application in Asp.net core 2.0, and I'd like to be able to restrict actions based on a combination of things, rather than one particular user role (like admin, power user, etc). The problem space looks like this:

  • Each User can have a particular 'home' facility that they have default permissions at based on their job function.
  • CRUD actions each have a particular permission associated with them.
  • Each permission can be granted at any number of facilities, just one, or none at all (or some combination thereof).
  • A particular user could have different permissions at different facilities. For example, a regional manager could have View and Order permissions at all of the facilities they work with, but only have the View permission to facilities in neighboring regions.

Currently, we use a home-grown solution that's getting out of hand to limit permissions, but it only works with a users 'home' facility. We can't grant someone that orders inventory from another facility, for example, to view a different facility's inventory.

We've attempted to just apply roles for each action in each facility (Yikes!) that are generated on the fly, but this lead to some users getting permissions they shouldn't have. Not to mention, its a nightmare to maintain.

How can I extend the Roles Functionality in ASP.NET Core 2.0 to allow my users to have different permissions in different facilities without having to create roles for each action at each facility?

Adam Wells
  • 545
  • 1
  • 5
  • 15

2 Answers2

3

I'd recommend using policies. They give you much finer-grained control. Basically, you start with one or more "requirements". For example, you might start with something like:

public class ViewFacilitiesRequirement : IAuthorizationRequirement
{
}

public class OrderFacilitiesRequirement : IAuthorizationRequirement
{
}

These mostly function as attachments for authorization handlers, so they're pretty basic. The meat comes in those authorization handlers, where you define what meeting the requirement actually means.

public class ViewFacilitiesHandler : AuthorizationHandler<ViewFacilitiesRequirement>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ViewFacilitiesRequirement requirement)
    {
        // logic here, if user is authorized:
        context.Succeed(requirement);
    }
}

Authorization handlers are dependency injected, so you can inject things like your DbContext, UserManager<TUser>, etc. into them in the normal way and then query those sources to determine whether or not the user is authorized.

Once you've got some requirements and handlers, you need to register them:

services.AddAuthorization(o =>
{
    o.AddPolicy("ViewFacilities", p =>
        p.Requirements.Add(new ViewFacilitiesRequirement()));
});

services.AddScoped<IAuthorizationHandler, ViewFacilitiesHandler>();

In case it's not obvious, a policy can utilize multiple requirements. All will have to pass for the policy to pass. The handlers just need to be registered with the DI container. They are applied automatically based on the type(s) of requirements they apply to.

Then, on the controller or action that needs this permission:

 [Authorize(Policy = "ViewFacilities")]

This is a very basic example, of course. You can make handlers than can work with multiple different requirements. You can build out your requirements a bit more, so you don't need as many of those either. Or you may prefer to be more explicit, and have requirements/handlers for each specific scenario. It's entirely up to you.

For more detail, see the documentation.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
1

You could create an AuthorizationFilterAttribute and assign it to each API endpoint or Controller class. This will allow you to assign case-by-case permissions to each of your users, then you just need a table containing specific permission IDs.

Here's an implementation that pulls username from basic authentication. You can change authentication to your implementation by changing the OnAuthorization method to retrieve user details from wherever you store it.

/// <summary>
/// Generic Basic Authentication filter that checks if user is allowed 
/// to access a specific permssion.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class BasicAuthenticationFilter: AuthorizationFilterAttribute
{
    private readonly bool _active = true;
    private readonly int _permissionId;

    public BasicAuthenticationFilter(int permissionId)
    {
        private _permissionId = permissionId;
    }

    /// <summary>
    /// Overriden constructor to allow explicit disabling of this
    /// filter's behavior. Pass false to disable (same as no filter
    /// but declarative)
    /// </summary>
    public BasicAuthenticationFilter(bool active) => _active = active;

    /// <summary>
    /// Override to Web API filter method to handle Basic Authentication.
    /// </summary>
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (!_active) return;

        BasicAuthenticationIdentity identity = ParseAuthorizationHeader(actionContext);
        if (identity == null && !OnAuthorizeUser(identity.Name, identity.Password, actionContext))
        {
            Challenge(actionContext);
            return;
        }

        Thread.CurrentPrincipal = new GenericPrincipal(identity, null);
        base.OnAuthorization(actionContext);

    }

    /// <summary>
    /// Base implementation for user authentication you'll want to override this implementing
    /// requirements on a case-by-case basis.
    /// </summary>
    protected virtual bool OnAuthorizeUser(string username, string password, HttpActionContext actionContext)
    {
        if (!Authorizer.Validate(username,password)) // check if user is authentic
            return false;

        using (var db = new DbContext())
        {
            var userPermissions = _context.UserPermissions
                .Where(user => user.UserName == username);

            if (userPermissions.Permission.Contains(_permissionId))
                return true;
            else
                return false;
            return true;
        }
    }

    /// <summary>
    /// Parses the Authorization header and creates user credentials
    /// </summary>
    protected virtual BasicAuthenticationIdentity ParseAuthorizationHeader(HttpActionContext actionContext)
    {
        string authHeader = null;
        System.Net.Http.Headers.AuthenticationHeaderValue auth = actionContext.Request.Headers.Authorization;
        if (auth?.Scheme == "Basic")
            authHeader = auth.Parameter;

        if (String.IsNullOrEmpty(authHeader))
            return null;

        authHeader = Encoding.Default.GetString(Convert.FromBase64String(authHeader));

        string[] tokens = authHeader.Split(':');
        if (tokens.Length < 2)
            return null;

        return new BasicAuthenticationIdentity(tokens[0], tokens[1]);
    }

    /// <summary>
    /// Send the Authentication Challenge request
    /// </summary>
    private void Challenge(HttpActionContext actionContext)
    {
        var host = actionContext.Request.RequestUri.DnsSafeHost;
        actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        actionContext.Response.Headers.Add("WWW-Authenticate", String.Format("Basic realm=\"{0}\"", host));
    }
}

Then you just add the filter to your API methods:

    private const int _orderPermission = 450;
    /// <summary>
    /// Submit a new order.
    /// </summary>
    [HttpPost, BasicAuthenticationFilter(_orderPermission)]
    public void Submit([FromBody]OrderData value)
    {
        Task.Run(()=> ProcessInbound());

        return Ok();
    }
nelsontruran
  • 514
  • 4
  • 18