An introduction to custom roles based access control in an ASP.NET MVC application using the Entity Framework.
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.
Hide Copy Code
[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.
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
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.
Subsequently, we derive the following database tables from our Entity-Relationship diagram.
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.
Hide Copy Code
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.
Hide Shrink Copy Code
public class RBACUser
{
public int User_Id { get; set; }
public bool IsSysAdmin { get; set; }
public string Username { get; set; }
private List Roles = new List();
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.RoleDescripton == role).ToList().Count > 0);
}
}
public class UserRole
{
public int Role_Id { get; set; }
public string RoleDescripton { get; set; }
public List Permissions = new List();
}
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.
Hide Copy Code
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.
Hide Copy Code
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.
Hide Copy Code
--Create an 'Administrator' Role setting IsSystemRole=1
INSERT INTO ROLES(RoleDescription, IsSysAdmin) VALUES('Administrator', 1)
--Create a 'Standard User' Role setting IsSystemRole=0
INSERT INTO ROLES(RoleDescription, 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 RoleDescription = 'Administrator'),
(SELECT Permission_Id FROM PERMISSIONS WHERE PermissionDescription = 'admin-create'))
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.
Hide Copy Code
--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 RoleDescription = 'Administrator'))
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.
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.
Hide Shrink Copy Code
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 theAuthorizeAttribute
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
.
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
Hide Copy Code
public class UnauthorisedController : Controller
{
// GET: Unauthorised
public ActionResult Index()
{
Session.Abandon();
return View();
}
}
Index.cshtml corresponding to the ‘Unauthorised’ controller
Hide Copy Code
<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 theAttribute
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.
Hide Copy Code
[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.
Hide Shrink Copy Code
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 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)
RBACUser
functionality exposed via our RBAC_ExtendedMethods
class can be used in controller actions.
Controller Action View (Index.cshtml)
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.
Hide Copy Code
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.
Hide Copy Code
<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.
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.
Hide Shrink Copy Code
<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.
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).
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 configuration section as detailed below.
Hide Copy Code
"
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.
Hide Shrink Copy Code
namespace RBAC.Models
{
public partial class RBAC_Model : DbContext
{
public RBAC_Model()
: base("name=RBAC_Model")
{
}
public virtual DbSet Permissions { get; set; }
public virtual DbSet Roles { get; set; }
public virtual DbSet Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasMany(e => e.Roles)
.WithMany(e => e.Permissions)
.Map(m => m.ToTable("LNK_ROLE_PERMISSION")
.MapLeftKey("Permission_Id").MapRightKey("Role_Id"));
modelBuilder.Entity()
.HasMany(e => e.Users)
.WithMany(e => e.Roles)
.Map(m => m.ToTable("LNK_USER_ROLE")
.MapLeftKey("Role_Id").MapRightKey("User_Id"));
}
}
}
Hide Shrink Copy Code
[Table("USERS")]
public partial class User
{
public User()
{
Roles = new HashSet();
}
[Key]
public int User_Id { get; set; }
public string Username { get; set; }
public virtual ICollection Roles { get; set; }
}
[Table("ROLES")]
public partial class Role
{
public Role()
{
Permissions = new HashSet();
Users = new HashSet();
}
[Key]
public int Role_Id { get; set; }
public string RoleDescription { get; set; }
public bool IsSysAdmin { get; set; }
public virtual ICollection Permissions { get; set; }
public virtual ICollection Users { get; set; }
}
[Table("PERMISSIONS")]
public partial class Permission
{
public Permission()
{
Roles = new HashSet();
}
[Key]
public int Permission_Id { get; set; }
public string PermissionDescription { get; set; }
public virtual ICollection 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. TheOnModelCreating
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’.
Hide Copy Code
//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.
Hide Copy Code
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,
RoleDescripton = _role.RoleDescription };
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.
Hide Copy Code
public class RBACUser
{
public int User_Id { get; set; }
public bool IsSysAdmin { get; set; }
public string Username { get; set; }
private List Roles = new List();
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).
Hide Copy Code
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 viaViewContext.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.
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.
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.
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.
Application Users
Once the application’s roles have been defined, users can be created and assigned roles.
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.
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:-
|
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.
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.
No comments:
Post a Comment