Introduction
Back in 2012 I was completing the final project towards my BSC Honours Degree in Computing. The project was a KPI Management solution that looked at the migration of existing business workflow utilising multiple spreadsheets and reports and developing a web based approach to streamline the process.
One element that arose during the project development was management of individual user permissions, i.e. those that could add/edit/view different data sets, different reports as well as manage the users of the system.
I wanted to do something different, rather than just have typical lists of permissions that were added or removed between listviews or a checkbox matrix where the users permissions could be turned on and off.
After a bit of thinking I thought about using a drag and drop jQuery approach. Something that was new to me and would allow me to demonstrate self learning and problem solving.
In this article I will show the approach used for this project. It would be pretty pointless to fill this article with all the irrelevant elements from the rest of the project, but will purely focus on the parts required to perform the use case being discussed in the article.
The final user admin roles management page can be seen in the animated screenshot below;
Background
I was using Asp.Net MVC 3 and the Razor view engine. The data was all stored in an SQL Express backend and EntityFramework V4.3.1 with 'Code First' the database approach used.
The standard Membership/Roles providers had been adopted to manage the users within the system.
This was the first time I had worked with MVC, EntityFramework and was really my first real commitment to move to C#.
As you could imagine, moving to a whole bunch of 'new stuff' for the project for a final degree course was a huge gamble. However, Google was my friend for a few months and also very much relied on two key sources of learning information;
- Professional Asp.Net MVC 3 book (more info)
- Pluralsight videos on Asp.Net website (see here)
The Back End
The role management was performed through a controller, UserAdminController. There were two main actions that were required to manage the user roles;
- The GET action that returned the list of roles the user being managed currently had.
- The POST action that provided the list of roles the user being managed should now have.
Let us first take a look at the complete GET action;
1
4 [Authorize(Roles = "Admin-User-Edit")]
5 public ActionResult AssignRoles(String username)
6 {
7 if (User.Identity.Name.Equals(username))
9 {
10 ModelState.AddModelError("", "You cannot edit your own roles.");
11 }
12 else
13 {
14 MembershipUser user = Membership.GetUser(username);
15
16 if (user == null)
17 {
18 ModelState.AddModelError("", "Username is not valid.");
19 }
20 else
21 {
22 ViewBag.Username = username;
23 ViewBag.AllRoles = Roles.GetAllRoles();
24 ViewBag.AllowRoles = Roles.GetRolesForUser(username);
25 }
26 }
27 return View();
28 }
The first thing you will notice is that the user making this request must have the "Admin-User-Edit" role [line 4], and the username of the user being modified is simply passed in as a string [line 5].
The code subsequently checks that the user making the request is not trying to modify their own permissions [line 8]. If the they are trying to edit their own permissions, and error message is added to the ModelState
[line 10] and the user is returned back to the view [line 28].
Next, we obtain the user object for the username supplied [line 14], if a user with the provided username is not found in the system the GetUser
method will return null
, this is tested [line 16] for and if the username is invalid an error message is added to the ModelState
[line 18] and the user is again returned to the view [line 28].
If the username is valid and we have obtained a relevant MembershipUser
object, we pop the required data for the management of the user into the viewbag bucket.
The Username
property is added that simply contains the username string [line 22]. The AllRoles
property is added that contains a string list of all the roles used in the system, this is obtain by calling the GetAllRoles
method [line 23]. Finally the AllowRoles
is a list of roles that the user being modified currently has assigned to them. This is obtained by calling the GetRolesForUser
and passing in the username (which we have already tested to be valid) [line 24].
We now have all the information required to allow the user to be modified, so we pass back the relevant view [line 28] which we will look at later.
Once the user has had their roles amended on the web page, the admin will submit the data back using a POST event.
The action method method below is one that handles this back at the controller;
1
4 [Authorize(Roles = "Admin-User-Edit")]
5 [HttpPost]
6 public ActionResult AssignRoles(String username, FormCollection formItems)
7 {
8 if (User.Identity.Name.Equals(username))
10 {
11 ModelState.AddModelError("", "You cannot edit your own roles.");
12 }
13 else
14 {
15 try
16 {
17 MembershipUser user = Membership.GetUser(username);
18 if (user == null)
19 {
20 throw new ArgumentException();
21 }
22 String[] newRoles = formItems["GrantRoles"].Split(',');
24 String[] oldRoles = Roles.GetRolesForUser(username);
26 if (!(oldRoles == null))
27 {
28 foreach (String role in oldRoles)
29 {
30 if (Roles.RoleExists(role)) { Roles.RemoveUserFromRole(username, role);}
31 }
32 }
33 foreach (String Role in newRoles)
35 {
36 if (!(Role.Equals("")) && Roles.RoleExists(Role))
37 {
38 Roles.AddUserToRole(username,Role);
39 }
40 }
41
42 ViewBag.Username = username;
43 ViewBag.AllRoles = Roles.GetAllRoles();
44 ViewBag.AllowRoles = Roles.GetRolesForUser(username);
45
46 ModelState.AddModelError("","User roles have been updated.");
47
48 }
49 catch (Exception)
50 {
51 ModelState.AddModelError("", "Username is not valid");
52 }
53
54 }
55 return View();
56 }
There is a little bit more going on in the POST action, lets break it down as we go through it;
[line 4] We check that the user has the correct permissions to execute the method.
[line 6] The username being edited and a collection of Form elements from the webpage are passed in.
[line 10] The user is checked to see they are not attempting to modify their own roles.
[line 18] Obtain the user object we want to edit and throw and error if invalid [line 22].
[line 26] Create a new string array of the list of roles which is obtained by splitting a list passed in from the GrantRoles
formItems
collection (see the front end section on how this is generated).
[line 29-36] Get the list of roles the user currently has and remove each role from the user.
[line 38-45] Assign each new role in the array created earlier to the user.
[line 47-49] Rebuild the viewbag with the updated information.
[line 51] We use the ModelState
to add a message to inform the user has been updated.
[line 57] Any thrown exception earlier is simply handled by a invalid user message via ModelState
.
[line 62] Return the view.
This approach ensures even if they modify the webpage attributes manually before posting that;
- Only users who have permission to modify users can do so.
- A user cannot modify their own permissions.
- Only valid roles are added to the user.
The Front End
All the views shared a common layout which contained the reference back to the CSS files and library files. In this project we used jQuery, jQueryUI as can be seen below in the snippet from the layout template;
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="Stylesheet" type="text/css"/>
<script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/modernizr-2.5.3.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/MicrosoftAjax.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/MicrosoftMvcAjax.js")" type="text/javascript"></script>
<script type="text/javascript">
</script>
</head>
<body>
// Common Page section elements such as menu headers / footers etc.
</body>
</html>
The view for the AssignRoles page is made up as follows;
@{
ViewBag.Title = "Assign Roles";
}
<h2>Assign Roles for User: @ViewBag.Username</h2>
Page title is added and a header telling you which username you are trying to modify. If you recall, we added this to the ViewBag
in the back end.
Next we assign relevant styles that used on this page only. These are the unordered list and the listitem elements used for the two role types, Assigned and Denied. The styles allow for the Red/Green unordered lists to have a minimum height, but will grow if more items are added than the current height. You can see this in action in the animated screenshot at the start of the article.
<style type="text/css">
ul.listRoles
{
width: 300px;
height: auto;
padding: 5px;
margin: 5px;
list-style-type: none;
border-radius: 5px;
min-height: 500px;
}
ul.listRoles li
{
padding: 5px;
margin: 10px;
background-color: #ffff99;
cursor: pointer;
border: 1px solid Black;
border-radius: 5px;
}
</style>
Next we have some JavaScript that is used to manage the two lists.
<script type="text/javascript">
$(function () {
$("#listDenyRoles, #listAllowRoles").sortable({
connectWith: ".listRoles"
}).disableSelection();
});
function submitNewRoles() {
var outputList = $("#listAllowRoles li").map(function () { return $(this).html(); }).get().join(',');
$("#GrantRoles").val(outputList);
$("#formAssignRoles").submit();
}
</script>
The first code runs automatically and sets up the drag and drop between the two lists of roles. The relation is established using the connectWith
, and have the same class name assigned in the item listRoles
. These elements are also set as sortable but have no selection capability.
The second part is the function submitNewRoles
. This is used as an intermediary between the form being submitted using a pseudo submit button and the actual form being submitted which is triggered via code.
What this function does is generates a list of Role strings separated by commas by mapping the listitem elements contained in the Allow Roles unordered list. It appends this list to a hidden GrantRoles
element before then submitting the form.
For more information on jQuery.map() visit http://api.jquery.com/jquery.map/
The next section provides the user with some basic instruction, then creates the form that will be posted back HTML.BeginForm
and sets the method for the form to FormMethod.POST
. The code also sets up two arrays that contain the lists of Roles available and the Roles currently assigned to the user. If either list from the viewbag is null it creates an empty string list.
<p>To GRANT a user a role, click and drag a role from the left Red box to the right Green box.<br />
To DENY a user a role, click and drag a role from the right Green box to the left Red box. </p>
@using (Html.BeginForm("AssignRoles", "UserAdmin", FormMethod.Post, new { id = "formAssignRoles" }))
{
String[] AllRoles = ViewBag.AllRoles;
String[] AllowRoles = ViewBag.AllowRoles;
if (AllRoles == null) { AllRoles = new String[] { }; }
if (AllowRoles == null) { AllowRoles = new String[] { }; }
The HTML Helper for the error messages is appended to the page and the two hidden elements that contain the username and the roles being granted for posting back.
@Html.ValidationSummary(true)
<fieldset><legend>Drag and Drop Roles as required;</legend>
@Html.Hidden("Username", "Username")
@Html.Hidden("GrantRoles", "")
A table is created that contains the two columns one with the Deny Roles and the other with the Allow Roles. The Unordered Lists are added to relevant column and the background colours are added using style attributes. Red for Deny, Green for Allow.
The two foreach
statements iterate through the relevant lists of roles and adds a list item to the unordered list that has the role name as the text of the item.
<table>
<tr>
<th style="text-align: center">
Deny Roles
</th>
<th style="text-align: center">
Allow Roles
</th>
</tr>
<tr>
<td style="vertical-align: top">
<ul id="listDenyRoles" class="listRoles" style="background-color: #cc0000;">
@foreach (String role in AllRoles)
{
if (!AllowRoles.Contains(role))
{
<li>@role</li>
}
}
</ul>
</td>
<td style="vertical-align: top">
<ul id="listAllowRoles" class="listRoles" style="background-color: #00cc00;">
@foreach (String hasRole in AllowRoles)
{
<li>@hasRole</li>
}
</ul>
</td>
</tr>
</table>
Finally, we add a button that acts as the submit and triggers the submitNewRoles
function.
<p><input type="button" onClick="submitNewRoles()" value="Assign Roles"/></p>
</fieldset>
}
And that is it! It comes across relatively simply, but I went through some pain to get there. Particularly the mapping of the list elements back to a string list using jQuery.
It is worth noting that there are probably better, more optimum ways of doing this, however for the small set of roles I had to deal with it was acceptable for me.
One are for example is when assigning the new roles, the method above simply removes ALL roles from the user, then applies the new list of roles. It could have been argued that why do you want to remove a role to potentially have to add it back again. I felt that it was cleaner and less of a risk to simply start a fresh with the users role list. This way you couldn't somehow leave a role that should no longer be there.
Demo Project
Sadly, as this was from an existing project, I haven't had time to build up a demo from scratch. There is sufficient information within the narrative of the article however that you could easily do something similar yourself.
History
19th May 2014 - First published.