Click here to Skip to main content
14,303,493 members

Custom Roles Based Access Control (RBAC) in ASP.NET MVC Applications - Part 1 (Framework Introduction)

Rate this:
4.95 (161 votes)
Please Sign up or sign in to vote.
4.95 (161 votes)
8 Jun 2015CPOL
An introduction to custom roles based access control in an ASP.NET MVC application using the Entity Framework.

Image 1

Introduction

In this post, I shall cover implementing custom Roles Based Access Control (RBAC) and subsequent roles maintenance in the context of an intranet based ASP.NET MVC web application using Windows Authentication. ASP.NET Roles and Membership provides almost all features required to perform authentication and authorisation but adding a new role and assigning it to a particular user seems to have been lost. This solution forms a self-contained framework independent of default out of the box providers. The framework allows us to focus on which features/areas in our application are restricted to the user, including menus, and what information to make visible/invisible to the user without concerning ourselves with the underlying technicalities. The framework offers RBAC functionality inside the controller action and controller view at a granular level whilst using minimum code syntax and the framework can be extended to incorporate custom RBAC methods. It is especially suited for corporate intranet applications where there is restricted access to the hosting web server once your web application has been deployed or the administration of user roles including role assignment cannot be directly undertaken by the application’s system administrator or owner.

Background

Developing intranet applications within an organization based on Windows Authentication has been around since the dawn of the intranet. In most organizations, it’s typical that intranet based applications not only require to permit access to a subset of users defined in the organization’s Active Directory server, but to also define roles which are assigned to the application’s users thus restricting access to certain features/areas within the application. Again, Roles Based Access Control isn’t a new concept and there are numerous examples posted that exemplify this concept in one form or another. ASP.NET MVC aligns itself well for RBAC and the examples posted on the web in their various guises either over engineer the concept or are too simplistic averting extendibility. It’s for this reason that I wrote this article.

Using the code

The default ASP.NET Roles and Membership classes come in very handy when we want to provide authentication and authorisation in our applications.  This approach requires you to define roles upfront which are then referenced in the MVC application via function attributes; essentially hardcoding values.

[Authorize(Roles="Administrators")]
public class AdminController : Controller
{
    . . .
}

Several problems with this approach become immediately obvious...

  • Without recompiling and re-deploying my application, how do I create new custom roles and bind them dynamically to controller methods once my application has been deployed?

  • How do I dynamically associate users with multiple roles where the highest application permission takes access precedence?

  • How do I dynamically control menus or controller view rendering based on the requesting user’s role(s) and associated application permissions?

When developing corporate solutions, we generally find ourselves in situations where the application’s users’ role data needs to be stored in the application’s own database.  If a database server fails due to hardware failure, restoring an earlier backed-up copy of the application’s database will contain all the role data thus aligning well for database replication for the purpose of a hot standby database server.

Custom Controller/Action Authorisation

Choosing to implement our own custom authentication/authorisation mechanism will entail abandoning the default out of the box ASP.NET Roles and Membership authentication/authorisation mechanism.  However, in doing so enables for finer granularity over user roles and application rights.  Generally speaking, an application once deployed should be self-maintaining and regulating via the application’s system administrator; it becomes a time consuming and costly affair when an application developer inherently becomes the users/roles administrator for that application unless the intention is to capitalize on the reliance of support teams maintaining backend roles.

Authorisation

Role based applications are where users in the system are assigned specific roles.  In our system, each role determines which areas of the application the role can access via application permissions.  Application permissions define MVC controller names and controller action names represented as a string concatenation of the two properties in the format controller-action (eg “admin-index”).  Application permissions are unique which can be traced back to their controller-action references.  It's easy to get confused with the difference between user authentication and user authorisation.  In summary, authentication is verifying that users are who they say they are, using some form of login mechanism (username/password, Windows Authentication, and so on — something that says “this is who I am”).  Authorisation is verifying that they can perform tasks as part of their job role with respect to your site.  This is usually achieved using some type of role-based system.

Roles Based Access Control (RBAC)

Roles Based Access Control is an approach to restricting system access to authorised users.  This mechanism can be used to protect users from accessing parts of the system that they do not need. It also can be used to restrict access to data which they do not need to see.

Roles are created for various job functions and it’s not uncommon for new roles to be introduced into a role-based system long after the application has been deployed.  The permissions to perform certain operations are assigned to specific roles.  Users are assigned particular roles, and through those role assignments acquire application permissions to perform particular computer-system functions.  Since users are not assigned permissions directly, but only acquire them through their role (or roles), management of individual user permissions becomes a matter of simply assigning appropriate roles to the user's account.  Each user in the system can be assigned zero, one or many roles depending on their responsibility within the business processes.

Preparing the Database

In order to form the basis of our authentication/authorisation framework, we will need to add several tables to the application’s existing database, if one currently exists, or create a new application database that will contain the tables.  Theses tables are derived from the following Entity-Relationship (ER) diagram.

RBAC Entity-Relationship Diagram


Image 2

Our Entity-Relationship diagram implies that an application user can be assigned zero or many application roles.  An application role can be assigned zero or many application permissions.  Application permission represents controller action methods.

Subsequently, we derive the following database tables from our Entity-Relationship diagram.

Image 3

We would clearly use more table properties in our real-world application allowing for flexible customization but the illustrated tables provide the minimum properties required to form the basis of an RBAC framework. Integrating our custom authentication/authorisation mechanism into existing MVC applications should be relatively straight forward since no ‘additional’ databases or Identity Management providers (IdM’s) are needed.  It would be highly unlikely that any MVC application wishing to introduce RBAC would be operating without a backend database in the first place therefore we could simply add the above tables to the existing database.  However, we do have the option to separate our RBAC tables away from the main application database since our RBAC tables are independent and based on a loose coupling design.  Any MVC application operating without a backend database will generally not need RBAC, examples being unit/currency conversion websites.

Retrieving User Application Permissions from our RBAC Tables

The following SQL will retrieve a user’s application permissions from our database tables and is used here for illustrative purposes.

SELECT Permission_Id, PermissionDescription 
       FROM PERMISSIONS
       WHERE Permission_Id IN (
             SELECT DISTINCT(Permission_Id) 
                    FROM LNK_ROLE_PERMISSION 
		            WHERE Role_Id IN (
                          SELECT DISTINCT(Role_Id) 
                                 FROM LNK_USER_ROLE ur 
		                         JOIN USERS u ON u.User_Id=ur.User_Id 
		                         WHERE u.Username='swloch'))

User Roles/Permissions Class Mapping

Let us now represent a user’s associated roles and application permissions using a single class encapsulating the necessary worker methods to check the user’s role(s) and associated permissions.  We will take a closer look at the GetDatabaseUserRolesPermissions() method at a later stage.  This class will be used throughout our application to determine user authorisation.

Note: Code snippets presented in this article are minimal intended for illustration purposes only in order to focus on the topic at hand.  The sample project available for download expands on the illustrated code.

public class RBACUser
{
    public int User_Id { get; set; }
    public bool IsSysAdmin { get; set; }
    public string Username { get; set; }
    private List<UserRole> Roles = new List<UserRole>();
 
    public RBACUser(string _username)
    {
        this.Username = _username;
        this.IsSysAdmin = false;
        GetDatabaseUserRolesPermissions();
    }
 
    private void GetDatabaseUserRolesPermissions()
    {
        //Get user roles and permissions from database tables...      
    }
 
    public bool HasPermission(string requiredPermission)
    {
        bool bFound = false;
        foreach (UserRole role in this.Roles)
        {
            bFound = (role.Permissions.Where(
                      p => p.PermissionDescription == requiredPermission).ToList().Count > 0);
            if (bFound)
                break;
        }
        return bFound;
    }
 
    public bool HasRole(string role)
    {
        return (Roles.Where(p => p.RoleName == role).ToList().Count > 0);
    }
    
    public bool HasRoles(string roles)
    {
        bool bFound = false;
        string[] _roles = roles.ToLower().Split(';');
        foreach (UserRole role in this.Roles)
        {
            try
            {
                bFound = _roles.Contains(role.RoleName.ToLower());
                if (bFound)
                    return bFound;
            }
            catch (Exception)
            {
            }
        }
        return bFound;
    }
}
 
public class UserRole
{
    public int Role_Id { get; set; }
    public string RoleName { get; set; }
    public List<RolePermission> Permissions = new List<RolePermission>();
}
 
public class RolePermission
{
    public int Permission_Id { get; set; }
    public string PermissionDescription { get; set; }
}

The RBACUser class encapsulates custom user authentication/authorisation functionality and will be executed in an ‘action filter’ which supports pre-action behaviour to controller action methods.  Action Filters are explained in the following section.

A Basic Overview of MVC Controller Action Methods

MVC controllers are responsible for responding to requests made against an ASP.NET MVC website. MVC attempts to map each browser request to a particular controller action.  If no controller action is specified in the URL, as in the example given below, the default controller action ‘index’ is used.  If the underlying controller or controller action does not exist, a HTTP 404 error will be returned to the browser.  For example, entering the following URL into a web browser will cause MVC to attempt to invoke the ‘Index’ action in the ‘Admin’ controller.

http://localhost/Admin

The ‘Admin’ verb in the URL signifies the controller name due to its position in the URL path.  In this example, the AdminController class is invoked; MVC’s controller class naming convention is to append the keyword ‘Controller’ to the controller’s name.  An MVC controller class is responsible for processing and responding to browser requests.  Every controller class should expose controller action methods that get invoked via URL references or other paths.

The following URL specifies both a controller name and controller action method.

http://localhost/Admin/Create

MVC will attempt to invoke the ‘Create’ controller action method in the AdminController class.  Every controller action returns an action result in response to a browser request even if the referenced controller or controller action doesn’t exist.  Before executing an invoked controller action method, pre-processing can be instructed by using an ‘Action Filter’ where logic can be placed to determine if the action method should be executed or directed to another part of the system instead.  An ‘Action Filter’ is an ideal candidate for checking a user’s authorisation for the invoked functionality.

Roles and MVC Controller/Action Associations

Now that we have a basic understanding on how controller action methods are invoked and that MVC provides a way for us to process logic before an action method is executed in order to evaluate whether the invoked action method should be executed, we need to understand the association between controller action methods and our application roles. In our system, each role will have a number of application ‘controller-action’ associations defined; recall that application permissions define MVC controller names and controller action names formatted as 'controller-action'.

Let’s consider the following example; the ‘Administrator’ role defined in our application must have the ability to create new users via the ‘Admin’ controller using the ‘Create’ action method.  Therefore, we require to associate the application permission ‘admin-create’ with the ‘Administrator’ role.  Likewise, the ‘Standard User’ role defined in our application should not have this ability and therefore must not have the application permission association.  Our example create user action method simply returns a page to the browser containing text fields and a submit button that enables the administrator to create a new user in the backend database. Clearly, we don’t want this page accessible to anybody.

We need to create the application permission ‘admin-create’ and assign the permission to our ‘Administrator’ role since the controller’s name and action method name combination will be passed to our ‘authorisation’ logic as the required application permission to be evaluated against the requesting user’s allowed application permissions.  You can change the format of the application permission stored in the database to a format you prefer but it needs to be consistent throughout the application.

For the time being, we shall manually create and assign the application permission in our database to gain an understanding on how our RBAC tables are structured but the sample project available for download provides an administration menu that enables a system administrator to perform CRUD actions on users/roles/permissions dynamically.

--Create an 'Administrator' Role setting IsSystemRole=1
INSERT INTO ROLES(RoleName, IsSysAdmin) VALUES('Administrator', 1)

--Create a 'Standard User' Role setting IsSystemRole=0
INSERT INTO ROLES(RoleName, IsSysAdmin) VALUES('Standard User', 0)

--Create an Application Permission for the action method 'Create' 
--defined in the 'Admin' controller (ie 'admin-create')
INSERT INTO PERMISSIONS(PermissionDescription) VALUES('admin-create')

--Associate the Application Permission 'admin-create' with the 'Administrator' Role
INSERT INTO LNK_ROLE_PERMISSION VALUES(
	(SELECT Role_Id FROM ROLES WHERE RoleName = 'Administrator'),
	(SELECT Permission_Id FROM PERMISSIONS WHERE PermissionDescription = 'admin-create'))

Image 4

The above tables display the content view of the ‘ROLES’, ‘PERMISSIONS’ and ‘LNK_ROLE_PERMISSION’ tables respectively in relation to the SQL ‘INSERT’ commands.

Now that we have defined an application role and assigned one application permission to the new role, we need to create an application user who is assigned the ‘Administrator’ role.  To keep things simple, we shall assume that our application is running as an intranet based system using Integrated Windows authentication via IIS where the user requesting the resource has already be authenticated.  We simply use the IPrincipal object to identify the requesting user’s name to which we map their unique username to our USERS table.

It goes without saying that only users permitted to access the intranet site would be added to the USERS table unless you are adopting a user ‘registration’ mechanism where users would be assigned ‘standard’ user role. This also works perfectly well for web sites not based on Integrated Windows authentication where authentication is achieved and tracked using authentication tokens which will be covered in the next follow-on article.  In either case, the user is identified via the IPrincipal object.

Let’s assume that the user to which we shall assign the application administration role has the Windows username ‘swloch’ and that the IPrincipal.Identity.Name property for this user evaluates to ‘somedomain\swloch’.

We need to add the user to the USERS table using Windows username (without the prepended domain name) as the table Username field and assign the ‘Administrator’ role to the user.

--Create the user 'swloch'
INSERT INTO USERS(Username) VALUES('swloch')

--Associate the 'Administrator' Role with user
INSERT INTO LNK_USER_ROLE VALUES(
	(SELECT User_Id FROM USERS WHERE Username = 'swloch'),
	(SELECT Role_Id FROM ROLES WHERE RoleName = 'Administrator'))

Image 5

The above tables display the content view of the ‘USERS’ and ‘LNK_USER_ROLE’ tables respectively in relation to the above SQL ‘INSERT’ commands.

Before a controller’s action is executed, the requesting user’s role is determined to check whether the role contains the requested controller/action combination.  If the role does contain the controller/action association then execution of the controller action is permitted resulting in the action result page from that controller/action being returned.  If the role does not contain the required controller/action association, an invalid authorisation page is returned instead of the action result page.

Image 6

Action Filters

In MVC, controllers define action methods that usually have a one-to-one relationship with possible user interactions, such as clicking a link or submitting a form.  Occasionally, you want to perform logic either before an action method is called or after an action method runs.  To support this, MVC provides action filters.  Action filters are custom attributes that provide a declarative means to add pre-action and post-action behaviour to controller action methods.

ASP.NET MVC provides the following types of action filters:

  • Authorisation filter, which makes security decisions about whether to execute an action method, such as performing authentication or validating properties of the request.  The AuthorizeAttribute class is one example of an authorisation filter.

  • Action filter, which wraps the action method execution.  This filter can perform additional processing, such as providing extra data to the action method, inspecting the return value, or cancelling execution of the action method.

  • Result filter, which wraps execution of the ActionResult object.  This filter can perform additional processing of the result, such as modifying the HTTP response.  The OutputCacheAttribute class is one example of a result filter.

  • Exception filter, which executes if there is an unhandled exception thrown somewhere in action method, starting with the authorisation filters and ending with the execution of the result.  Exception filters can be used for tasks such as logging or displaying an error page.  The HandleErrorAttribute class is one example of an exception filter.

Custom Action Filters

Let’s create a custom authorisation filter named RBACAttribute inherited from the AuthorizeAttribute class.  Our custom attribute extracts the requested controller name and controller action name properties from the AuthorizationContext object and checks whether the derived application permission exists as a permitted permission in any of the requesting user’s assigned roles using the RBACUser object.

public class RBACAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {      
        /*Create permission string based on the requested controller 
          name and action name in the format 'controllername-action'*/
        string requiredPermission = String.Format("{0}-{1}",
               filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
               filterContext.ActionDescriptor.ActionName);

        /*Create an instance of our custom user authorisation object passing requesting 
          user's 'Windows Username' into constructor*/
        RBACUser requestingUser = new RBACUser(filterContext.RequestContext
                                               .HttpContext.User.Identity.Name);

        //Check if the requesting user has the permission to run the controller's action
        if (!requestingUser.HasPermission(requiredPermission) & !requestingUser.IsSystemAdmin)
        {
            /*User doesn't have the required permission and is not a SysAdmin, return our 
              custom '401 Unauthorized' access error. Since we are setting 
              filterContext.Result to contain an ActionResult page, the controller's 
              action will not be run.

              The custom '401 Unauthorized' access error will be returned to the 
              browser in response to the initial request.*/
            filterContext.Result = new RedirectToRouteResult(
                                           new RouteValueDictionary { 
                                                { "action", "Index" }, 
                                                { "controller", "Unauthorised" } });
        }
        /*If the user has the permission to run the controller's action, then 
          filterContext.Result will be uninitialized and executing the controller's 
          action is dependant on whether filterContext.Result is uninitialized.*/
    }
}

The RBACUser object exposes the HasPermission method that accepts a permission parameter returning a bool value denoting the existence of that permission in any of the user’s assigned roles.  If you derive a class from the AuthorizeAttribute class, the derived class must be thread safe. Therefore, do not store state information in an instance field in an instance of the class unless that state information is meant to apply to all requests. Instead, store state information per request in the Items property, which is accessible through the context objects passed to AuthorizeAttribute.

In the event where the user’s role(s) do not contain the required application permission, a customized “401 Unauthorized” access error is returned instead of the intended controller’s action result view.  The customised error simply returns a view, as detailed below, via the ‘Index’ controller action defined in the ‘Unauthorised’ controller invoked by the RedirectToRouteResult.

Image 7

The error text is defined in the ‘Index.cshtml’ file corresponding to the ‘Unauthorised’ controller and you should modify this file to change the page’s aesthetics.

UnauthorisedController.cs

public class UnauthorisedController : Controller
{
    // GET: Unauthorised
    public ActionResult Index()
    {
        Session.Abandon();
        return View();
    }
}

Index.cshtml corresponding to the ‘Unauthorised’ controller

<body>
@{
    ViewBag.Title = "Unauthorised Request";
}
<div id="title">Error 401 : Unauthorised Request</div>

<div id="error">You do not have permission to access the requested resource due to security restrictions.  In order to gain access, please speak to your system administrator.</div>
</body>

 

Restricting Access to MVC Controller Action Methods using Action Filters

We can now use our custom authorisation filter to restrict access to a controller’s action method in our web application by decorating the action method with our RBACAttribute attribute; MVC allows you to omit the Attribute verb from the authorization attribute when decorating a controller’s action to simply use RBAC for those who prefer this naming convention.  Our authorization attribute instructs MVC to perform logic before the action method is called where logic checks whether the requesting user has been authenticated and has the required application permission to execute the controller’s action method.

[RBAC]
public ActionResult Create()
{
    return View();
}

OR 

[RBACAttribute]
public ActionResult Create()
{
    return View();
}

A good approach to security is to always place the security check as close as possible to the resource you are securing.  You may have additional checks higher up the stack, but ultimately, you need to secure the actual resource.  This way, no matter how the user gets to the resource, there will always be a security check in place. In this case, you don’t want to rely on routing and URL authorisation to secure a controller; you really need to secure the controller itself.

Our custom RBACAttribute authorisation attribute serves this purpose.

  • If you don’t specify any roles or users, the current user must simply be authenticated in order to call the action method. This is an easy way to block unauthenticated users from a particular controller action.

  • If a user attempts to access an action method with this attribute applied and fails the authorisation check, the filter causes the server to return a “401 Unauthorised” HTTP status code.

Performing Conditional Processing using RBAC during Controller Action Execution

Let’s consider that we have a controller action method that returns a page containing employee data.  This time, we are not decorating the controller’s action with our custom RBACAttribute attribute since all users registered with our application may access this page.  However, we do wish to restrict the data returned inside our controller’s action during execution based on the requesting user’s role(s).

For example, a user having the 'Standard' user role will have access to the controller’s action (no authorisation filter applied to the controller’s action) but only be allowed to view the Employee's work related contact details whereas a user having the 'HumanResourcesManager' role will be allowed to view additional information including the employee's salary.  Therefore, we require to check the requesting user’s role both in the controller’s action method and corresponding view to determine the level of employee data returned back to the browser.

Since our RBACUser class encapsulates the required functionality to evaluate a user’s roles/permissions, we need to expose this functionality to the controller’s action methods and views.  The simplest way is to expose our custom methods to the System.Web.Mvc.ControllerBase abstract class using ‘extension methods’.  This enables our user’s roles/permissions functionality to be available to every controller during execution keeping code changes to an absolute minimum, even if you have numerous controllers and corresponding views since there will be no need to change every controller with a new controller class inherited from Controller which exposes our functionality.

Let us extend the System.Web.Mvc.ControllerBase class to expose our RBACUser functionality necessary for roles/permissions management.

public static class RBAC_ExtendedMethods
{
  public static bool HasRole(this ControllerBase controller, string role)
  {
     bool Found = false;
     try
     {
        //Check if the requesting user has the specified role...
        Found = new RBACUser(controller.ControllerContext
                             .HttpContext.User.Identity.Name).HasRole(role);            
     }
     catch { }
     return Found;
  }
  
  public static bool HasRoles(this ControllerBase controller, string roles)
    {
        bool bFound = false;
        try
        {
            //Check if the requesting user has any of the specified roles...
            //Make sure you separate the roles using ';' (ie "Sales Manager;Sales Operator")
            bFound = new RBACUser(controller.ControllerContext
                                  .HttpContext.User.Identity.Name).HasRoles(roles);
        }
        catch { }
        return bFound;
    }
 
  public static bool HasPermission(this ControllerBase controller, string permission)
  {
     bool Found = false;
     try
     {
        //Check if the requesting user has the specified application permission...
        Found = new RBACUser(controller.ControllerContext
                             .HttpContext.User.Identity.Name).HasPermission(permission);
     }
     catch { }
     return Found;
  }
 
  public static bool IsSysAdmin(this ControllerBase controller)
  {        
     bool IsSysAdmin = false;
     try
     {
        //Check if the requesting user has the System Administrator privilege...
        IsSysAdmin = new RBACUser(controller.ControllerContext
                                  .HttpContext.User.Identity.Name).IsSysAdmin;
     }
     catch { }
     return IsSysAdmin;
  }
}

We can now call our exposed functionality in any controller action and/or corresponding view through the controller’s context object as illustrated below.

Controller Action (EmployeeController.cs)

Image 8

RBACUser functionality exposed via our RBAC_ExtendedMethods class can be used in controller actions.

Controller Action View (Index.cshtml)

Image 9

RBACUser functionality exposed via our RBAC_ExtendedMethods class can be used in views.

Using RBAC in a Controller’s Action Method

The following listing illustrates the use of our custom ‘HasRole’ and ‘HasPermission’ methods, exposed in our RBACUser class, in the controller’s action through the controller’s context object.  We have extended these methods to the controller’s context object using extension methods defined in our RBAC_ExtendedMethods class.

public class EmployeeController : Controller
{
    // GET: Employee
    public ActionResult Index()
    {
        if (this.HasRole("HumanResourcesManager"))
        {
            /*This code block is permitted as the requesting user has the 'HumanResourcesManager' 
              role assigned.  Perform additional tasks and/or extract additional data from the 
              database into the controller's view model/viewbag in order to be passed down to the 
              controller's view.*/                            
        }
 
        if (this.HasPermission("ViewRestrictedHRData"))
        {
            /*This code block is permitted as the requesting user has the 'ViewRestrictedHRData' 
              permission assigned.  We can also define role functionality permissions not related 
              to controller-action access.  Extract salary data from database into controller's 
              view model/viewbag...*/
        }
 
        return View();
    }
}

Without having to alter our EmployeeController class, we now have our RBACUser functionality exposed through the controller’s context object using extension methods.  This is also true for the controller’s view.

Using RBAC in a Controller’s View

The following listing illustrates the use of our custom ‘HasRole’ and ‘HasPermission’ methods.  We have extended these methods to the controller’s context object using extension methods defined in our RBAC_ExtendedMethods class.

<body>
@{
    ViewBag.Title = "Employee Page";
}
@{
    if (ViewContext.Controller.HasRole("HumanResourcesManager"))
    {
            <div id="manager">Use this area to provide additional information and/or display
            additional data provided in the model/viewbag by the controller action
            as the user has the "HumanResourcesManager" role assigned.</div> 
    }
 
    if (ViewContext.Controller.HasPermission("ViewRestrictedHRData"))
    {
            <div id="restricted">Use this area to provide additional information and/or display
            additional data provided in the model/viewbag by the controller action
            as the user has the "ViewRestrictedHRData" permission assigned.</div>
    }
}

<div id="standard">Use this area to provide standard information to all users.</div>
</body>

Our RBAC_ExtendedMethods class provides us with a flexible framework which enables us to extend our custom RBAC functionality with minimal effort.  Our extended RBAC methods are automatically exposed to every controller action and corresponding view in our application through the controller’s context object without having to change the controller classes in anyway (except for the use of our newly exposed functionality).

Using RBAC to Dynamically Control Menus

Our custom ‘HasPermission’, ‘HasRole’ and ‘IsSysAdmin’ methods come in useful when displaying dynamic menu items.  Recall that each role in our system will have a number of application ‘controller-action’ associations defined each representing a controller’s name and controller’s action name.  Consider the application menu items displayed below.

Image 10

If we need to dynamically display menu items based on the requesting user’s role permissions, we can simply refer to the menu’s target controller name and controller action as the permission in the format ‘controller-action’.

For example, if we require the ‘Import Data’ menu to be visible only to users allowed access to the underlying controller’s action, we would simply wrap our custom ‘HasPermission’ method around the menu item definition (generally found in the ‘_Layout.cshtml’ view) as illustrated below passing the menu’s target controller name and controller action as the permission to be checked.

<body>
   <div class="page">
      ...
      <div id="menucontainer">
         <ul>
            @{
            if (ViewContext.Controller.IsSysAdmin())
            {
            <li>
               <a href="#" class="arrow">System Administration</a>
               ...
            </li>
            }
            }

            @{
            if (ViewContext.Controller.HasPermission("data-import"))
            {
            <li>@Html.ActionLink("Import Data", "Import", "Data")</li>
            }
            }

            <li>@Html.ActionLink("About", "About", "Home")</li>
            <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
            <li>@Html.ActionLink("Home", "Index", "Home")</li>
         </ul>
      </div>
   </div>
   ...
</body>

The custom ‘HasPermission’ method will check the requesting user’s role(s) for the permission ‘data-import’ and display accordingly.  Additionally, we can also make use of the ‘IsSysAdmin’ method to check whether the requesting user has a role that has the IsSysAdmin database property enabled.  Therefore, a user that doesn’t have a ‘System Administrator’ role nor a role defining the ‘data-import’ permission will see the following menu items displayed (based on the illustrated code snippet) as opposed to the menu items displayed above.

Image 11

However, if a user enters a URL directly (eg ‘http://…/Data/Import’) and does not have the required permission, a customized “401 Unauthorised” access error is returned instead of the intended controller’s action result view providing we have decorated the controller’s action or controller class with our RBACAttribute.  Associating the ‘data-import’ permission to the user’s role will automatically display the corresponding menu.

Persisting RBAC Data via ADO.NET Entity Framework (EF)

Because every request to a controller action decorated with our RBACAttribute authorisation attribute requires verification of the requesting user’s permitted application permissions stored in our RBAC database, the user’s roles/permissions data should be read from the RBAC database once and then stored persistently for maximum performance.  Busy websites can be exposed to thousands of requests per minute and reading the requesting user’s roles/permissions data from the RBAC database for every request would seriously hinder the website’s performance.

ADO.NET Entity Framework (EF) is a component of the .NET Framework which provides a data persistence layer using a conceptual model, called the Entity Data Model (EDM), which sits on top of the database schema. EF applications can run on any computer on which the .NET Framework (starting with version 3.5 SP1) is installed. EF persists data in-memory using a data model that is mapped to underlying database tables; data changes to the underlying tables are automatically detected and automatically refreshed in the persistent data model. This makes EF an ideal candidate for storing our RBAC data (even if your existing application isn’t using EF) since performance will be dramatically increased as the RBAC data isn’t read from the underlying database every time we query the data model. Once our roles/permissions are correctly configured, updates will be infrequent.

Creating an Entity Framework (EF) RBAC Model

Let’s create a new EF model to store our RBAC data which will be used by our RBACUser object. Add a new ‘ADO.NET Entity Data Model’ item to your existing project as detailed below (unless you already have a model in which case you simply need to add the table associations into your model’s OnModelCreating method).

Image 12

Name your database connection string accordingly.  The name specified will be stored in the application’s Web.Config file as a database connection string located in the <connectionstrings><userrole><userrole><rolepermission><rolepermission>configuration section as detailed below.

<connectionStrings>
   <add name="RBAC_Model" connectionString="data source=localhost;initial..." providerName="System.Data.SqlClient" />
</connectionStrings>

Generated Entity Framework (EF) RBAC Model

The following database context model and corresponding database entities are generated from our RBAC database tables; we access the application’s RBAC data through the context model.

namespace RBAC.Models
{
    public partial class RBAC_Model : DbContext
    {
        public RBAC_Model()
            : base("name=RBAC_Model")
        {
        }

        public virtual DbSet<Permission> Permissions { get; set; }
        public virtual DbSet<Role> Roles { get; set; }
        public virtual DbSet<User> Users { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Permission>()
                .HasMany(e => e.Roles)
                .WithMany(e => e.Permissions)
                .Map(m => m.ToTable("LNK_ROLE_PERMISSION")
                .MapLeftKey("Permission_Id").MapRightKey("Role_Id"));

            modelBuilder.Entity<Role>()
                .HasMany(e => e.Users)
                .WithMany(e => e.Roles)
                .Map(m => m.ToTable("LNK_USER_ROLE")
                .MapLeftKey("Role_Id").MapRightKey("User_Id"));
        }
    }
}
[Table("USERS")]
public partial class User
{
    public User()
    {
        Roles = new HashSet<Role>();
    }
 
    [Key]
    public int User_Id { get; set; }   
    public string Username { get; set; } 
    public virtual ICollection<Role> Roles { get; set; }
}

[Table("ROLES")]
public partial class Role
{
    public Role()
    {
        Permissions = new HashSet<Permission>();
        Users = new HashSet<User>();
    }

    [Key]
    public int Role_Id { get; set; }
    public string RoleName { get; set; }
    public string RoleDescription { get; set; }
    public bool IsSysAdmin { get; set; }

    public virtual ICollection<Permission> Permissions { get; set; }
    public virtual ICollection<User> Users { get; set; }
}

[Table("PERMISSIONS")]
public partial class Permission
{
    public Permission()
    {
        Roles = new HashSet<Role>();
    }

    [Key]
    public int Permission_Id { get; set; }
    public string PermissionDescription { get; set; }

    public virtual ICollection<Role> Roles { get; set; }
}

The generated model has identified and defined three entities (User, Role and Permission) from our RBAC database and defined the RBAC_Model context model which links the entity relationships together.  The OnModelCreating method on the RBAC_Model context model class is called during the database context model creation where model entity relationships are created which correspond to the underlying database tables including table index keys and referential integrity constraints.

NOTE: If you already have an Entity Data Model (EDM) in your existing application, simply add the generated entity classes (Users, Roles and Permissions) to your project and define the corresponding DbSet for these entities in your model including the entity relationships defined in the OnModelCreating method. So long as the RBAC tables are added to your existing database, there will be no need to add a database connection string to the application’s Web.Config since your application will already have a connection string defined for your existing EDM.

Extracting Data from out Entity Framework (EF) RBAC Model

To create an instance of our RBAC EF context model, we simply create an instance of the RBAC_Model class from which we then search against the database entities (ie Users, Roles and Permissions).  The following listing illustrates the creation of our context model and a search against the Users table for ‘swloch’.

//Create an instance of the RBAC Entity Data Model (EDM)...
using (RBAC_Model _data = new RBAC_Model())
{            
    User _user = _data.Users.Where(u => u.Username == 'swloch').FirstOrDefault();
    if (_user != null)
    {
        //User found, access roles/permissions via exposed properties...               
        foreach (Role _role in _user.Roles)
        {
            foreach (Permission _permission in _role.Permissions)
            {
                if (_permission.PermissionDescription == 'admin-create')
                {

                }
            }
        }
    }
}

The first time a DbContext is created, is pretty expensive but once the object has been created much of the information is cached so that subsequent instantiations are significantly quicker.  You are more likely to see performance problems from keeping a context object around than you are from instantiating one each time you need access to your database.  If you keep a context object around it will keep track of all the updates, additions, deletes etc and this will slow your application down and may even cause subtle bugs to appear in your application.

The context object should be created per request.  Create the context object, do what you need to do with the object and then get rid of it.  Do not try and have a global context (this is not how web applications work).

Reading User Roles/Permissions from our Database

We will now take a closer look at the GetDatabaseUserRolesPermissions() method used in our RBACUser class.

Code Listing for Entity Framework (EF) RBAC Data Retrieval

The following listing details the GetDatabaseUserRolesPermissions() method which extracts the requesting user’s RBAC data from the underlying database using ADO.NET Entity Framework.  When a new instance of the RBAC context model is created for the very first time, data will be retrieved from the database and stored in-memory.  Therefore, subsequent new instances of the RBAC context model will not require data to be loaded directly from the underlying database since EF will use the data already stored in-memory.  Any underlying changes to the data in the database are detected by EF and reloaded when we next access the data thus ensuring data retrieval from our ‘persistence data layer’ is always up-to-date.

private void GetDatabaseUserRolesPermissions()
{
    using (RBAC_Model _data = new RBAC_Model())
    {            
        User _user = _data.Users.Where(u => u.UserName == this.Username).FirstOrDefault();
        if (_user != null)
        {
            this.User_Id = _user.Id;
            foreach (Role _role in _user.Roles)
            {
                UserRole _userRole = new UserRole { 
                                Role_Id = _role.Id, 
                                RoleName = _role.RoleName };
                foreach (Permission _permission in _role.Permissions)
                {
                    _userRole.Permissions.Add(new RolePermission { 
                                Permission_Id = _permission.Id, 
				                PermissionDescription = _permission.PermissionDescription });
                }
                this.Roles.Add(_userRole);
 
                if (!this.IsSystemAdmin)
                    this.IsSystemAdmin = _role.IsSysAdmin;
            }
        }
    }
}

Extending the RBAC Framework

If we require to extend our RBAC framework with customized methods, we simply add new methods to the RBACUser class; these methods could be associated with new fields added to the USERS database table or associated tables.  For example, we could add a new column to the USERS database table called ‘Title’ which represents the user’s name title (eg ‘Mr’, ‘Prof’, ‘Dr’ etc).  By adding the new property ‘Title’ to our RBACUser class as illustrated below, the property will be automatically loaded from the database into our RBACUser object by Entity Framework.

We can then expose a new method in our RBACUser class called IsDoctor() which checks for the value ‘Dr’ returning a bool (true if value equals ‘Dr’ otherwise false).  Although this is an unlikely real world example, it demonstrates the concept.

public class RBACUser
{
    public int User_Id { get; set; }
    public bool IsSysAdmin { get; set; }
    public string Username { get; set; }
    private List<UserRole> Roles = new List<UserRole>();
    public string Title { get; set; }
          
    public bool IsDoctor()
    {
        return (this.Title == "Dr");
    }

    ...
    ...
}

In order to expose our new functionality to the application’s controller actions and controller views, we must wrap our new functionality in new methods in our RBAC_ExtendedMethods class (ie our extension methods).

public static class RBAC_ExtendedMethods
{
  public static bool IsDoctor(this ControllerBase controller)
  {
     bool IsDoctor = false;
     try
     {
        //Check if the requesting user has the specified role...
        IsDoctor = new RBACUser(controller.ControllerContext
                                .HttpContext.User.Identity.Name).IsDoctor();            
     }
     catch { }
     return IsDoctor;
  }
 
  ...
  ...
}

We have now extended our RBAC framework with our customised method IsDoctor(). We can now use the new method in our controller action via this.IsDoctor() syntax or in our controller view via ViewContext.Controller.IsDoctor() syntax.

Note: Code snippets used in the above examples are minimal highlighting additional code only in order to focus on the topic at hand.

Sample Project

The sample project available for download implements the AdminController which provides the necessary RBAC administration.  Simply adding this controller, accompanying views contained in the Admin folder and RBAC model to any existing MVC project will provide the necessary RBAC administration functionality. Integrating the RBAC administration functionality into an existing ASP.NET MVC project is discussed in the next section.

The RBAC administration is exposed via the ‘System Administration’ menu as displayed below and will only be visible to a user having a role that has the ‘IsSysAdmin’ option enabled; the menu item definition must be contained within the ‘IsSysAdmin’ function to display dynamically as discussed in the ‘Using RBAC to Dynamically Control Menus’ section.  The menu style is driven by CSS and can be easily modified to follow any application theme.

Image 13

Before we create any application roles, we need to create permissions associated with our application. Depending on which areas of your application you need to restrict using role based access, there may be a large number of controller action methods which translate to application permissions.  Entering each controller action as a permission into your application can be a dull and time consuming task.  To aid in the creation of your application permissions, the ‘Permissions’ screen contains a button labeled ‘Import Permissions’.

Application Permissions

The import permissions function uses the .NET Framework’s Reflection API which enables the fetching of assembly type information at runtime.  The function iterates through the assembly’s MVC controller methods and saves each controller-action to the permissions database table.

Image 14

Application Roles

Once the application’s permissions have been defined, we are in a position to create user roles.  User roles are typically associated with one or more application permissions.  Your application business rules should define which roles in your application should access which areas.  Clicking on the ‘Roles’ menu will display your application’s roles (where defined) and enable CRUD actions on the roles to be undertaken.

Image 15

Once an application role has been created, application permissions can then be assigned to the role.  Permissions can be associated and disassociated with a role at any time.  Associating permissions with roles can be a time consuming task.  To aid in the role-permission association process, the ‘Add All Permissions’ button associates all permissions with the role in a single action; unwanted permissions can then be disassociated using the trash icon.  Alternatively, individual permissions can be selected from the dropdown and added via the ‘Add Permission’ button.

Image 16

Application Users

Once the application’s roles have been defined, users can be created and assigned roles.

Image 17

Roles can be associated and disassociated with a user at any time.  Individual roles are assigned to a user by selecting the role from the dropdown and pressing the ‘Add Role’ button; unwanted roles can be unassigned using the trash icon.

Image 18

NOTE: Permanently deleting an application role via the ‘Roles’ screen will automatically remove the role from associated users.

Adding RBAC to Existing MVC Applications

Adding the RBAC functionality to an existing application will require the following steps to be undertaken:-

 
  1. Using Solution Explorer in Visual Studio, create a new folder in your project called ‘Action Filters’ and add the RBACAttribute.cs file by right-clicking on the newly created folder and selecting ‘Add >> Existing Item…’. This file defines the RBACAttribute class used to decorate controller action methods.

    Add the file RBAC_ExtendedMethods.cs to your ‘Action Filters’ folder in the same manner as above. This file defines the RBAC_ExtendedMethods class which defines the extension methods HasRole, HasPermission and IsSysAdmin

    Add the file RBACUser.cs to your ‘Action Filters’ folder in the same manner as above. This file defines the RBACUser class which defines the methods HasRole, HasPermission and IsSysAdmin

  2. Using Solution Explorer in Visual Studio, create a new folder in your project inside the ‘Content’ folder called ‘Images’ and add the grid icons (bin.png and pen.png) and the ‘System Administration’ down arrow menu icon (arrow.gif) unless you prefer to use images already present in your application in which case you will need to alter the image references to point to your preferred images.

  3. Using Solution Explorer in Visual Studio, add both the AdminController.cs and UnauthorisedController.cs files to your ‘Controllers’ folder by right-clicking and selecting ‘Add >> Existing Item…’. These files define the RBAC ‘System Administration’ controller functionality.

  4. Using Solution Explorer in Visual Studio, add the files PERMISSIONS.cs, ROLE.cs, USER.cs and RBAC_Model.cs to your 'Models' folder.  These files define the RBAC_Model data model and corresponding data model types (User, Role, Permission).

  5. Using Solution Explorer in Visual Studio, create a new folder in your application’s ‘Views’ folder called ‘Admin’ and add the view files as detailed in the RBACDemo project by right-clicking on the newly created folder and selecting ‘Add >> Existing Item…’.  These views are associated with the Admin controller actions.  If your project already contains an Admin controller, you can rename the RBAC Admin controller and move the associated views to the new corresponding folder.  Links which reference the Admin controller will need changing to reference the newly named controller.  These links are contained in both the controller itself and the corresponding views.

  6. Using Solution Explorer in Visual Studio, create a new folder in your application’s ‘Views’ folder called ‘Unauthorised’ and add the view file Index.cs by right-clicking on the newly created folder and selecting ‘Add >> Existing Item…’.

  7. Include the ‘System Administration’ menu in your _Layout.cshtml file using your existing menu CSS styling.  If you prefer the illustrated menu styling, copy the necessary CSS from the sample Site.css file to your project’s Site.css file.  This also applies to the views; incorporate your own styling.

  8. The ‘Scripts’ folder references the jquery-ui-1.10.3.custom.min.js file but you can freely download your own custom themed version from http://jqueryui.com and reference accordingly.

 

  Image 19

RBAC Authentication/Authorisation Overview

The IIS User Authentication process is undertaken by IIS to provide an additional layer of security. Since the web application is intranet based and will run within a Windows domain, IIS will check that a user making an inbound request to the web server has been authenticated. If a user is not authenticated, IIS reroutes the inbound request to an Authentication Login dialog box requesting alternative user credentials. If a user is authenticated, the inbound request is routed to the MVC web application where authorisation checks are undertaken as described in earlier sections.

The following diagram illustrates the RBAC Authentication/Authorisation process.

Image 20

An inbound request to our web application is initially handled by IIS which authenticates the user against the active directory group via an Authentication Login dialog box if not authenticated. If the user is authenticated, the request is forwarded to the MVC web application which checks the user’s rights and roles. A user’s right will decide whether the requested controller/action can be processed. A user is stored as an object in the Entity Framework layer which populates the user’s data from the database tables.

Conclusion

This solution forms an ideal framework for any intranet application that requires dynamic self-contained Roles Based Access Control (RBAC) that is specific to the application and independent of ASP.NET Roles and Membership and Identity Management providers (IdM) such as Microsoft Identity Integration Server (MIIS). The framework can be added to existing projects as well as new developments and once deployed will be self-maintaining and regulating via the application’s system administrator with little or no reliance on the application developer.

This solution is particularly suited for corporate intranet applications where limited access to the deployed web server is granted and/or the administration of user roles including role assignment is delegated away from the application system administrator and/or owner.

In the next post, the article will extend the framework to incorporate Roles Based Reporting and refactor RBAC to operate with ASP.NET MVC web applications using username/password authentication via HTTPS.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Stefan Wloch
Software Developer (Senior)
United Kingdom United Kingdom
No Biography provided

Comments and Discussions

 
GeneralImplementation with DB first approach Pin
Member 1386498216-Jul-19 1:44
memberMember 1386498216-Jul-19 1:44 
QuestionUpdate on .net core 2.x Pin
Jigar Panchal12-Mar-19 23:28
memberJigar Panchal12-Mar-19 23:28 
GeneralMy vote of 5 Pin
Member 410934431-Aug-18 2:04
memberMember 410934431-Aug-18 2:04 
QuestionPlease update to .net Core 2.0 Pin
Member 1269327919-Apr-18 19:38
memberMember 1269327919-Apr-18 19:38 
QuestionPlease update to .net Core 2.0 Pin
Member 1269327919-Apr-18 19:38
memberMember 1269327919-Apr-18 19:38 
QuestionAuthentication Required Pin
Member 136893041-Apr-18 6:17
memberMember 136893041-Apr-18 6:17 
QuestionMy Vote of 6 out of 5 Pin
Troy compton30-Jan-18 7:52
memberTroy compton30-Jan-18 7:52 
QuestionImplementation with Database First Pin
Member 229429330-Nov-17 12:23
memberMember 229429330-Nov-17 12:23 
QuestionGreat Article. Awesome. Pin
jigar05085-Nov-17 21:21
memberjigar05085-Nov-17 21:21 
QuestionCustom Roles Based Access Control (RBAC) in WinForm Pin
Sulaiman Danish1-Oct-17 21:12
memberSulaiman Danish1-Oct-17 21:12 
QuestionPurpose of the admin-create permission Pin
podman@guru15-Sep-17 3:33
memberpodman@guru15-Sep-17 3:33 
QuestionNamespace Pin
Member 1301504423-Feb-17 2:21
memberMember 1301504423-Feb-17 2:21 
Questionhow to implement this in asp.net webform. Pin
Member 804879222-Feb-17 22:07
memberMember 804879222-Feb-17 22:07 
PraiseGreat Article Pin
zafirov14-Feb-17 8:54
memberzafirov14-Feb-17 8:54 
GeneralMy vote of 5 Pin
Robert Alexander Parra Ciro6-Feb-17 2:24
memberRobert Alexander Parra Ciro6-Feb-17 2:24 
QuestionHow to create user and logon Pin
Member 804879230-Dec-16 2:56
memberMember 804879230-Dec-16 2:56 
AnswerRe: How to create user and logon Pin
Stefan Wloch13-Jan-17 1:12
memberStefan Wloch13-Jan-17 1:12 
Question[AllowAnonymous] Pin
omid12345622-Oct-16 4:20
memberomid12345622-Oct-16 4:20 
AnswerRe: [AllowAnonymous] Pin
Stefan Wloch13-Jan-17 1:11
memberStefan Wloch13-Jan-17 1:11 
PraiseGreat Work Bro!!! It Helps me a lot!!! Pin
Mohammed Asarudeen R8-Oct-16 21:22
professionalMohammed Asarudeen R8-Oct-16 21:22 
GeneralMy vote of 5 Pin
Raviraj Patel27-Aug-16 21:06
memberRaviraj Patel27-Aug-16 21:06 
Questionport code to asp.net WeBForm Pin
Member 804879214-Aug-16 16:29
memberMember 804879214-Aug-16 16:29 
PraiseProblem Solved Pin
Member 1158049712-Aug-16 7:54
memberMember 1158049712-Aug-16 7:54 
QuestionGreat Job!! Pin
enzomidi23-Jun-16 7:10
memberenzomidi23-Jun-16 7:10 
Questionwithout the prepended domain name) Pin
podman@guru17-Jun-16 8:08
memberpodman@guru17-Jun-16 8:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 13 Feb 2015

Stats

275.1K views
30.3K downloads
205 bookmarked