A Role Management System Practice

I made a role management system in our web application. It’s a good practice for using ASP.NET Identity.

Introduction

The role management system is used to manage the user roles and role permissions of Cora application. In addition, it makes control of whether a user have access to the specific action method. See the following figure:

We have a permission table and a role-permission table.

Permission Table

Id      Module

1       Organisation Administration

2       Role Management

3       User Management

4       Emergency Contact

5       Call Centre

6       User Profile

Each permission corresponds to the specific module. E.g. if we have User Management permission, that means we have access to the User Management page to manage the users.

How do we know whether we have the permission to the specific page? Here is the role –permission table.

Role-permission Table

Id      Name                          PermissionId

1       SuperAdmin                       2, 6

2       Admin                                  1, 6

4       CallCenterUser                   5, 6

5       CallCenterManager            3, 6

6       AllPermissions                   1 to 6

From the table, we can see that if we are in Admin role, that means we have permission 1 and permission 6, checking the permission table, we can know that we have Organisation Administration permission and User Profile permission, which means we have access to all of the action methods that Organisation Administration and User Profile will call. Here is the database relationship:

We can use normal CRUD operations on the table to change the role of a user and the permissions of each role. It is just the operation on database, not much to say. I created two pages of the functionality so that the use can do the change by themselves:

The role changing UI:

when clicking the Role button:

The permission changing UI:

Connect the Permission with the Specific Module/Page

A user is in a role, so he has related permissions, and then he has access to the related pages/modules. We need to connect the permission to the specific action methods in the controller.

Filter/attribute mechanism is used to make it. A filter in ASP.NET MVC is a kind of attribute that gives additional logic to an action method or controller. The method in the filter can be run before the action method is called or after, deciding what to do before/after the action method is invoked. In addition, it can decide whether the action method will be called or not by the return value of the filter method, just like a filter. The MVC Framework supports four different types of filters, one of them is Authorization filter using IAuthorizationFilter interface, and the default Implementation is AuthorizeAttribute.

At the beginning of each controller Authorization filter is used to check whether the request is authenticated (i.e. whether the user has logged in):

[Authorize]
public class ManageController : Controller

If the AuthorizeCore method in Authorization filter returns false (the login cookie not exist or expired), then the page will redirect to login page to let the user login.

If we need anonymous access to the specific action method, we can put Anonymous filter on it:

[AllowAnonymous]
public ActionResult Login()

If we want to authorize the method with specific role, we can make the syntax like this:

[Authorize (Roles ='Admin')]

By this way we can make only Admin that can call the method. It is convenient to use the built-in Authorize filter. However, there is a problem that we do not want to connect the roles with the method. Instead we expect to establish a relationship between permissions and methods. What we want to do is to use the role management page to change the permissions of the roles, and make the permissions tightly connected to the methods we can call.

To make the authorization process dynamically, a customized authorize filter is made:

[AuthorizePermission(PermissionNames = "Role Management, Call Centre", ReturnType = ReturnType.Html)]
public ActionResult RoleManagement()

The permission name is exactly the same as that in Permission table. We can pass the permission names and return type to the filter. The filter will firstly find out the role of current user, then check the database whether the role has the permissions that passed in. If not, then it will redirect to /Manage/UnauthorizedRequest or return Json object: {"ErrorName": "Unauthorized Request","StatusCode":403}, depending on whether you pass in ReturnType.Html or ReturnType.Json.

Codes behind the AuthorizationPermission Filter

To make AuthorizationPermission filter run, we need to customize the filter by ourselves. We need to create a new class AuthorizationPermissionFilter which inherits from AuthorizationAttribute and override the core filtering method  AuthorizeCore and the unauthorized request handler HandleUnauthorizedRequest. To use the internal services, we have to pass all the services from the constructor. So the filter should be look like this:

public class AuthorizePermissionFilter : AuthorizeAttribute
{
    private ApplicationRoleManager _roleManager;
    private ApplicationUserManager _userManager;
    private IAccountService _accountService;
    private IRoleService _roleService;

    private string _permissionNames;
    private ReturnType _returnType;

    public AuthorizePermissionFilter(ApplicationRoleManager roleManager, ApplicationUserManager userManager,
        IAccountService accountService, IRoleService roleService, string permissionNames, ReturnType returnType)
    {
        _roleManager = roleManager;
        _userManager = userManager;
        _accountService = accountService;
        _roleService = roleService;
        _permissionNames = permissionNames;
        _returnType = returnType;
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var userId = Convert.ToInt32(httpContext.User.Identity.GetUserId());
        if(_permissionNames != null)
        {
            var permissionNameArray = _permissionNames.Split(',');
            foreach (var _permissionName in permissionNameArray)
            {
                var permission = _roleService.GetPermissionList().Where(x => x.ModuleName == _permissionName.Trim()).FirstOrDefault();
                if (permission != null)
                {
                    var roleList = permission.RolePermissions.Select(y => y.Role);
                    if (roleList != null)
                    {
                        foreach (var role in roleList)
                        {
                            if (_userManager.IsInRole(userId, role.RoleName))
                            {
                                return true;
                            }
                        }
                    }
                }
            }
        }
            
        return false;
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        //filterContext.HttpContext.Response.StatusCode = 403;
        switch (_returnType)
        {
            case ReturnType.Html:
                filterContext.RouteData.Values["controller"] = "Manage";
                filterContext.RouteData.Values["action"] = "UnauthorizedRequest";
                break;
            case ReturnType.Json:
                filterContext.Result = new JsonResult {
                    Data = new { ErrorName = "Unauthorized Request", StatusCode = 403 },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };
                break;
        }     
    }
}

The AuthorizeCore method is used to find out the role of current user, then check the database whether the role has the permissions that passed in from the attribute. The HandleUnauthorizedRequest method handles what to do when AuthorizeCore method returns false (i.e. have no access). It will set properties of filterContext object to return HTML/JSON.

Issues about Dependency Injection

Here comes a big problem with the code above: how can we instantiate the filter? What we want to do is to just pass in the PermissionNames and ReturnType parameter to the constructor of the filter, like this:

[AuthorizePermission(PermissionNames = "Role Management, Call Centre", ReturnType = ReturnType.Html)]

But the constructor needs more:

public AuthorizePermissionFilter(ApplicationRoleManager roleManager, ApplicationUserManager userManager,
        IAccountService accountService, IRoleService roleService, string permissionNames, ReturnType returnType)

It needs all the services to do the process. To make the filter like the style above, we have to create an attribute to fit the format:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizePermissionAttribute : AuthorizeAttribute
{
    public string PermissionNames { get; set; }    
    public ReturnType ReturnType { get; set; }
}

And then we need to connect AuthorizePermissionAttribute with AuthorizePermissionFilter. What catch my mind immediately is the dependency injection. We can use Ninject to bind them together and pass some of the parameters to the constructor of AuthorizePermissionFilter from AuthorizePermissionAttribute. It means when  [AuthorizePermission(PermissionNames = "Role Management, Call Centre", ReturnType = ReturnType.Html)] is called, the parameters will be passed into the properties of  AuthorizePermissionAttribute instance. Then AuthorizePermissionFilter will also be called by Ninject and the PermissionNames and ReturnType parameter will be passed into the constructor of AuthorizePermissionFilter. The other parameters that the constructor needs will be instantiated automatically by Ninject. Here is the code of dependency injection in Global.asax.cs:

kernel.BindFilter<AuthorizePermissionFilter>(FilterScope.Action, 0)
.WhenActionMethodHas<AuthorizePermissionAttribute>().InRequestScope()
.WithConstructorArgumentFromActionAttribute<AuthorizePermissionAttribute>("permissionNames", att => att.PermissionNames)
.WithConstructorArgumentFromActionAttribute<AuthorizePermissionAttribute>("returnType", att => att.ReturnType);

By this way, when AuthorizePermission Attribute is being instantiating, AuthorizePermission Filter’s constructor will be called. The services going to be used will be instantiated automatically and the parameters PermissionNames and ReturnType will be passed to the constructor at the same time. At last Ninject instantiate  AuthorizePermission Filter instead of us.

 

Leave a Reply

Your email address will not be published. Required fields are marked *