1

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://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2). Is this the best way to do it?
pitaridis
  • 2,192
  • 2
  • 19
  • 33

1 Answers1

1

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
B12Toaster
  • 8,944
  • 5
  • 48
  • 48
  • About separating to zone and action will make things more complex because there will be extra actions for specific zones depending on the requirements. The HasConversion is something that I did not know, and I will read about it to see how it works. I had to use async because I must make an await call in order to get the user and roles. – pitaridis Jan 13 '19 at 10:22
  • I understand that you need it to be async but you cannot simply force a synchronous method called by the dotnet framework to be async. The framework's call to `OnAuthorization` is not `await`ed by the caller it and will simply treat it in a synchronous way. – B12Toaster Jan 13 '19 at 10:27
  • I updated my answer with a link that should clarify this point https://stackoverflow.com/a/53855641/2477619 – B12Toaster Jan 13 '19 at 10:32
  • "About separating to zone and action will make things more complex because there will be extra actions for specific zones depending on the requirements." Okay, that is a valid point. I just wanted to mention that the drawback of this is that you need to create a lot of "constants" (also in the frontend). Morover, the frontend cannot simply generate a CRUD-permission-table to set rights. But use whatever suits you best :) – B12Toaster Jan 13 '19 at 10:51
  • So at the moment that there is no way to use await calls I will have to use the dbcontext and manually get the user and roles. About caching the information so that I will not use the database all the time, is it ok or should I use a different method for better performance? – pitaridis Jan 13 '19 at 12:33
  • (1) "So at the moment that there is no way to use await calls" – there is a way, use the async version of the IAuthorizationFilter (as I wrote in my updated answer). (2) "that I will not use the database all the time, is it ok or should I use a different method" it is okay as long as you make sure your permission cache is cleared if the permissions change (see my updated answer). – B12Toaster Jan 13 '19 at 12:51
  • 1
    The async version of IAuthorizedFilter worked fine. About the caching, I was thinking to expire the cache within the next 10 minutes or something like this but invalidating all the cached information every time that I change a role or permission would be better. I have no experience in caching but I think it would not be such a difficult to implement. – pitaridis Jan 13 '19 at 13:13