pitaridis
pitaridis

Reputation: 2983

Permissions per page in ASP.NET Core

I have a scenario that roles and claims do not support so I followed a path of implementing this scenario and I would like to ask some questions and tell me if what I do is the right way to do it or suggest a different way to implement it. First of all I want to define the permission of each Controller / Action or Razor page by using an Attribute like this:

[Data.CheckAccess(PermissionsEnum.Users_Create)]
public class PrivacyModel : PageModel
{
    public void OnGet()
    {
    }
}

The PermissionsEnum has the following form:

public enum PermissionsEnum
{
    Users_View = 101,
    Users_Create = 102,
    Users_Edit = 103,
    Users_Delete = 103,
    Users_Details = 104,

    Products_View = 201,
    Products_Create = 202,
    Products_Edit = 203,
    Products_Delete = 204,
    Products_Details = 205
}

I modified the IdentityRole so that I will be able to attach a list of permissions for every role.

public class ApplicationRole : IdentityRole
{
    public string Permissions { get; set; }

    public void SetPermissions(List<PermissionsEnum> permissions)
    {
        Permissions = Newtonsoft.Json.JsonConvert.SerializeObject(permissions);
    }

    public List<PermissionsEnum> GetPermissions()
    {
        return Newtonsoft.Json.JsonConvert.DeserializeObject<List<PermissionsEnum>>(Permissions);
    }
}

EntityFramework can not have properties of type List so I used a string property and I have two helper methods for serializing and deserializing the enum list. Now I can create admin pages that the user can create roles and check permissions grouped by users, and products (these are the two groups that I have in the enumerator) for easier management of the permissions. After creating the roles I will be able to use an other page to assign roles to users. I created the following attribute which will make the authorization:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CheckAccessAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private PermissionsEnum permission;

    public CheckAccessAttribute(PermissionsEnum permission)
    {
        this.permission = permission;
    }

    public async void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.Identity.IsAuthenticated)
        {
            return;
        }

        UserManager<ApplicationUser> userManager = (UserManager<ApplicationUser>)context.HttpContext.RequestServices.GetService(typeof(UserManager<ApplicationUser>));
        var user = await userManager.GetUserAsync(context.HttpContext.User);
        RoleManager<ApplicationRole> roleManager = (RoleManager<ApplicationRole>)context.HttpContext.RequestServices.GetService(typeof(RoleManager<ApplicationRole>));

        var roles = await userManager.GetRolesAsync(user);

        foreach (var role in roles)
        {
            var CurrentRole = await roleManager.FindByNameAsync(role);

            if (CurrentRole.GetPermissions().Contains(permission))
                return;
        }

        // the user has not this permission
        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        return;
    }
}

This works fine but when I start the application, I login and stop the application and after that I start the application, the user is logged in because there is an authentication cookie. This will have as a result the application will crash. The error message is when I try to get the user using the usermanager. The error message is the following.

System.ObjectDisposedException: 'Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'AsyncDisposer'.'

So I changed my code in order to bypass this problem and the code now is the following.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CheckAccessAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private PermissionsEnum permission;

    public CheckAccessAttribute(PermissionsEnum permission)
    {
        this.permission = permission;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.Identity.IsAuthenticated)
        {
            return;
        }

        var userId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

        ApplicationDbContext DbContext = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));

        var user = DbContext.Users.Where(m => m.Id == userId).FirstOrDefault();
        var RoleIDs = DbContext.UserRoles.Where(m => m.UserId == user.Id).Select(m => m.RoleId);
        var Roles = DbContext.Roles.Where(m => RoleIDs.Contains(m.Id)).ToList();
        foreach (var role in Roles)
        {
            if (role.GetPermissions().Contains(permission))
                return;
        }

        // the user has not this permission
        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        return;
    }
}

I have the following questions.

  1. Is there a better way to implement this scenario?
  2. Why the exception System.ObjectDisposedException occurs. Is there a way to solve this problem?
  3. In order to improve the performance, I will try to cache the current user’s permissions so that I will not have to load them each time the attribute is used by a page. I think I will use the Cache in-memory method (https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2). Is this the best way to do it?

Upvotes: 1

Views: 2662

Answers (1)

Felix K.
Felix K.

Reputation: 15683

Some suggestions:

  • Have look at: How do you create a custom AuthorizeAttribute in ASP.NET Core?.
  • Since you are just repeating actions (view, create, edit, delete, details) for every access controlled "zone" (user, product, ...), consider separating these "zones" and permissions in separate files. In your attribute you can then write [CheckAccess(user, create)] Makes it also easier for the frontend to set permissions.
  • Use HasConversion for serialization of permissions.
  • "I think I will use the Cache in-memory method" – If you cache permissions, make sure to remember to invalidate this cache everytime you change permissions of a user or add new permissions.
  • Do not use async void unless for asynchronous event handlers. This is most likely the reason you are experiencing the ObjectDisposedException because your method is executed async and the calling process does not wait for the async operation to finish and disposes the db context. Use the async version of the IAuthorizationFilter instead. Have a look at this answer: https://stackoverflow.com/a/53856127/2477619

Upvotes: 1

Related Questions