Introduction
This article presents an architecture for role based access for components/features (e.g., Edit User, Add User, Save Changes) on per page basis. It is a generic solution for role based authorization on both server and client side using .NET MVC and JavaScript.
The application is built as a web application which authenticates and authorizes the user.
How It Works
- On Login, user details including the features assigned are stored as
session
object. - On request for a page thereon:
ServerSide
: PermissionFilter
authenticates the request; If unauthenticated, user is redirected to Login Page. PermissionFilter
also authorizes the requests against the features/permissions assigned; If unauthorized, NotAuthorized
page is rendered. ClientSide
: PermissionChecker
script gets the elementControlID
s user is authorized for and removes the rest elements from the DOM.
Behind the Scenes
- Database: Each feature has a unique
controlID
. - ClientSide: Each feature is assigned unique
controlID
using attribute data-feature="<FeatureControlID>
". - ServerSide: Feature access is checked by the
controllerName
and actionName
associated with a feature from the controller and action name in the request.
Tools & Technologies
- Visual Studio 2012 - .NET MVC
Code:
The code could be found at:
https://github.com/gtush24/MVCAndJSSecurityApp.git
Background
Database Schema
Database Schema
- Each user has a role.
- Each role is assigned 1 or more features/actions (Example: Add/Edit/Delete, etc.)
- Each feature/permission is an action recognized by
controllerName
and actionName
having unique controlID(Name)
to recognize it on the webpage.
Examples
- User - XYZ has a role Admin
- Role - Admin has assigned features - Edit, Delete
- Feature - Edit is recognized as
ControllerName
: Home
, ActionName
: EditUser
Using the Code
There are 2 main components here for authorization:
Server Side
- An action filter which authorizes for the actions called against features/permissions assigned to role of
loggedIn
User. - It also validates for authentication; access
AfterLogin
only pages. - It returns a
NotAuthorizedToUseThisContent
page for any unauthorized activity.
Client Side
- A script that removes the features from the DOM that are not assigned to the role of
loggedIn
user from the page on page-load and after completion of any Ajax request.
Server Side - PermissionFilter.cs
It is an action filter and runs on every request to action in a controller. It authenticates and authorizes (using action and controller name from features/permissions allowed for loggedin
User role).
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
HttpRequestBase request = filterContext.HttpContext.Request;
HttpResponseBase response = filterContext.HttpContext.Response;
if ((request.AcceptTypes.Contains("application/json")) == false)
{
if (request.IsAjaxRequest())
{
#region Preventing caching of ajax request in IE browser
response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
response.Cache.SetValidUntilExpires(false);
response.Cache.SetCacheability(HttpCacheability.NoCache);
response.Cache.SetNoStore();
#endregion
}
string currentActionName = filterContext.ActionDescriptor.ActionName;
string currentControllerName =
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
PermissionManager userPermissions = PermissionManager.getPermissions();
if (userPermissions != null)
{
filterContext.Controller.ViewBag.LoggedInAdministrator =
PermissionManager.GetLoggedInUser();
if (PermissionManager.enablePermissioningSystem == true)
{
bool isCurrentActionAFeature = Service.isFeaturePresentInList
(userPermissions.allFeatures, currentControllerName, currentActionName);
if (isCurrentActionAFeature)
{
bool hasPermission = false;
hasPermission = Service.isFeaturePresentInList
(userPermissions.accessibleFeatures, currentControllerName,
currentActionName);
if (!hasPermission)
{
filterContext.Result = new ViewResult
{
ViewName = "~/Views/shared/unauthorizedactivity.cshtml"
};
}
}
}
}
else
{
filterContext.Controller.TempData["Message"] = "Please login to continue.";
filterContext.Result = new RedirectToRouteResult(
new System.Web.Routing.RouteValueDictionary
{ { "controller", "home" }, { "action", "login" }, { "Area", "" } });
}
}
}
On Server, actions are decorated with the PermissionFilter
attribute for filtering the requests to that action.
Client Side - PermissionChecker.js
This script removes the non authorized feature elements from DOM on page load and on any Ajax requests.
var allowedFeatureList = new Array();
var enablePermission = false;
$(document).ajaxComplete(function (event, request, settings) {
var allDataFeatureElements = $('[data-feature]');
if (enablePermission == true) {
$(allDataFeatureElements).each(function () {
var currentfid = $(this).data("feature");
var result = $.inArray(currentfid, allowedFeatureList);
if (result != -1) {
}
else {
$(this).remove();
}
});
}
});
$(function () {
$.ajaxSetup({
cache: false
});
var allDataFeatureElements = $('[data-feature]');
$.getJSON("/home/getAllAccessibleFeatureControlIDJSON", function (data) {
enablePermission = data.enablePerm;
if (data.enablePerm == true) {
for (var i = 0; i < data.controlIDS.length; i++) {
allowedFeatureList.push(data.controlIDS[i]);
}
$(allDataFeatureElements).each(function () {
var currentfid = $(this).data("feature");
var result = $.inArray(currentfid, allowedFeatureList);
if (result != -1) {
}
else {
$(this).remove();
}
});
}
});
});
Here page-load
and ajaxComplete
functions are used for DOM filtering.
Features are get from server on page-load
only and used in ajaxComplete
since getting in ajaxComplete
would trigger it again and it would end up being infinite loop.
Feature IDs call from Page
Features Identification
UserList Page
In the above image, Logged-In User is assigned only EditUser
permission and other features are removed from page by the script.
Unauthorized page
Points of Interest
The above requirements could well be dealt with conditional statements in every page and showing elements according to role by filtering them on the server side. But, the above architecture presents a maintained and global solution for such a requirement.
Further Up
The request going each time for features on server could be minimized by storing the values in a cookie and using them thereon in the script.