Reputation: 2983
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.
Upvotes: 1
Views: 2662
Reputation: 15683
Some suggestions:
[CheckAccess(user, create)]
Makes it also easier for the frontend to set permissions.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/2477619Upvotes: 1