ASP.NET MVC 5 Identity: Implementing Group-Based Permissions Management Part II

Posted on February 19 2014 09:02 PM by jatten in ASP.NET MVC, ASP.Net, C#, CodeProject   ||   Comments (5)

locked-awayThis is the second part of a two-part series in which we figure out how to implement a basic Group-based permissions management system using the ASP.NET MVC 5 Identity system. In this series, we are building upon previous concepts we used in extending the IdentityUser class and implementing Role-Based application security, and also in extending and customizing the IdentityRole class.

In this series, we hope to overcome some of the limitations of the simple "Users and Roles" security model, instead assigning Roles ("permissions") to Groups, and then assigning one or more groups to each user.

Image by Kool | Some Rights Reserved

In the first installment, we figured out how to model our core domain objects for the purpose of extending the ASP.NET Identity system into a basic Group-Based Permissions management mode. We decided that Groups will be assigned various combinations of permissions, and Users are assigned to one or more groups. What we are referring to here as "permissions" are actually the familiar "Role" provided by the identity system, upon which the MVC authorization system depends for user authentication and application access authorization.

<--- Review Part I: Extending the Model

 

Up to this point, we have extended our domain model by adding a Group class, implemented many-to-many relationships between Users and Groups, as well as between Groups and Roles ("Permissions"). We created

Building the Example Application - Controllers, Views, and ViewModels

Picking up where we left off, we now need to add the functional components of our example application. Obviously, we need some controllers and Views, but before we can build those, we are going to add some ViewModels which will be consumed by the various Controllers, and passed to the Views.

Adding Group View Models

We are going to need a few new ViewModels to complete our implementation of Groups. Also, we no longer need the SelectUserRolesViewModel. We will now be assigning Users to Groups, instead of directly to Roles, so we can delete the code for that. For the sake of simplicity, we will go ahead and add all of our new ViewModels to the AccountViewModels.cs file, and then I will explain what we are doing with each on as we go.

First, open the AccountViewModels.cs file, find the code for SelectUserRolesViewModel, and delete it.

Next, add the following new ViewModels to the end of the AccountViewModels.cs file:

Add New Required ViewModels:
// Wrapper for SelectGroupEditorViewModel to select user group membership:
public class SelectUserGroupsViewModel
{
    public string UserName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<SelectGroupEditorViewModel> Groups { get; set; }
  
    public SelectUserGroupsViewModel()
    {
        this.Groups = new List<SelectGroupEditorViewModel>();
    }
  
    public SelectUserGroupsViewModel(ApplicationUser user)
        : this()
    {
        this.UserName = user.UserName;
        this.FirstName = user.FirstName;
        this.LastName = user.LastName;
  
        var Db = new ApplicationDbContext();
  
        // Add all available groups to the public list:
        var allGroups = Db.Groups;
        foreach (var role in allGroups)
        {
            // An EditorViewModel will be used by Editor Template:
            var rvm = new SelectGroupEditorViewModel(role);
            this.Groups.Add(rvm);
        }
  
        // Set the Selected property to true where user is already a member:
        foreach (var group in user.Groups)
        {
            var checkUserRole =
                this.Groups.Find(r => r.GroupName == group.Group.Name);
            checkUserRole.Selected = true;
        }
    }
}
  
  
// Used to display a single group with a checkbox, within a list structure:
public class SelectGroupEditorViewModel
{
    public SelectGroupEditorViewModel() { }
    public SelectGroupEditorViewModel(Group group)
    {
        this.GroupName = group.Name;
        this.GroupId = group.Id;
    }
  
    public bool Selected { get; set; }
  
    [Required]
    public int GroupId { get; set; }
    public string GroupName { get; set; }
}
  
  
public class SelectGroupRolesViewModel
{
    public SelectGroupRolesViewModel()
    {
        this.Roles = new List<SelectRoleEditorViewModel>();
    }
  
  
    // Enable initialization with an instance of ApplicationUser:
    public SelectGroupRolesViewModel(Group group)
        : this()
    {
        this.GroupId = group.Id;
        this.GroupName = group.Name;
  
        var Db = new ApplicationDbContext();
  
        // Add all available roles to the list of EditorViewModels:
        var allRoles = Db.Roles;
        foreach (var role in allRoles)
        {
            // An EditorViewModel will be used by Editor Template:
            var rvm = new SelectRoleEditorViewModel(role);
            this.Roles.Add(rvm);
        }
  
        // Set the Selected property to true for those roles for 
        // which the current user is a member:
        foreach (var groupRole in group.Roles)
        {
            var checkGroupRole =
                this.Roles.Find(r => r.RoleName == groupRole.Role.Name);
            checkGroupRole.Selected = true;
        }
    }
  
    public int GroupId { get; set; }
    public string GroupName { get; set; }
    public List<SelectRoleEditorViewModel> Roles { get; set; }
}
  
  
public class UserPermissionsViewModel
{
    public UserPermissionsViewModel()
    {
        this.Roles = new List<RoleViewModel>();
    }
  
  
    // Enable initialization with an instance of ApplicationUser:
    public UserPermissionsViewModel(ApplicationUser user)
        : this()
    {
        this.UserName = user.UserName;
        this.FirstName = user.FirstName;
        this.LastName = user.LastName;
        foreach (var role in user.Roles)
        {
            var appRole = (ApplicationRole)role.Role;
            var pvm = new RoleViewModel(appRole);
            this.Roles.Add(pvm);
        }
    }
  
    public string UserName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<RoleViewModel> Roles { get; set; }
}

 

Adding a Groups Controller

Before we can do much more with our application, we need to add a Groups controller and associated Views. We already have a Roles Controller from our previous article, so let's add one for Groups now. We begin with a pretty standard CRUD-type controller as might be generated by Visual Studio:

The basic Groups Controller:
public class GroupsController : Controller
{
    private ApplicationDbContext db = new ApplicationDbContext();
  
    [Authorize(Roles = "Admin, CanEditGroup, CanEditUser")]
    public ActionResult Index()
    {
        return View(db.Groups.ToList());
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup, CanEditUser")]
    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Group group = db.Groups.Find(id);
        if (group == null)
        {
            return HttpNotFound();
        }
        return View(group);
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup")]
    public ActionResult Create()
    {
        return View();
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup")]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include="Name")] Group group)
    {
        if (ModelState.IsValid)
        {
            db.Groups.Add(group);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(group);
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup")]
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Group group = db.Groups.Find(id);
        if (group == null)
        {
            return HttpNotFound();
        }
        return View(group);
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup")]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit([Bind(Include="Name")] Group group)
    {
        if (ModelState.IsValid)
        {
            db.Entry(group).State = EntityState.Modified;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(group);
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup")]
    public ActionResult Delete(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Group group = db.Groups.Find(id);
        if (group == null)
        {
            return HttpNotFound();
        }
        return View(group);
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup")]
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
        Group group = db.Groups.Find(id);
        var idManager = new IdentityManager();
        idManager.DeleteGroup(id);
        return RedirectToAction("Index");
    }
  
  
    [Authorize(Roles = "Admin, CanEditGroup")]
    public ActionResult GroupRoles(int id)
    {
        var group = db.Groups.Find(id);
        var model = new SelectGroupRolesViewModel(group);
        return View(model);
    }
  
  
    [HttpPost]
    [Authorize(Roles = "Admin, CanEditGroup")]
    [ValidateAntiForgeryToken]
    public ActionResult GroupRoles(SelectGroupRolesViewModel model)
    {
        if (ModelState.IsValid)
        {
            var idManager = new IdentityManager();
            var Db = new ApplicationDbContext();
            var group = Db.Groups.Find(model.GroupId);
            idManager.ClearGroupRoles(model.GroupId);
            // Add each selected role to this group:
            foreach (var role in model.Roles)
            {
                if (role.Selected)
                {
                    idManager.AddRoleToGroup(group.Id, role.RoleName);
                }
            }
            return RedirectToAction("index");
        }
        return View();
    }
  
  
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            db.Dispose();
        }
        base.Dispose(disposing);
    }
}

 

By now, most of the code above should look fairly familiar. We have the basic Create/Edit/Delete and Index methods, and then one odd method at the end of the class, GroupRoles. Actually, this should look sort of familiar as well. This is a simple adaptation of the code we used in the previous project to select Roles for individual Users. Here, we are doing the same thing for specific Groups instead.

Add Views for the Groups Controller

The following are the Views we need for the Groups Controller. These are also pretty standard fare, except for the GroupRoles View. We'll include them all here for completeness, though.

Of greatest interest is the GroupRoles View, so we will start there.

The GroupRoles View

This View is where we will assign one or more Roles to a specific Group. We want to display the general information for the current group selected, and then display a list of all the available Roles, with checkboxes to indicate selected status. For our presentation layer, we will describe Roles as "Permissions" so that the concept is more clear to the user: Users are members of groups, and groups have sets of permissions.

Here, we once again employ an EditorTemplate (our SelectRoleEditorTemplate from the previous version of this project) in order to render an HTML Table with checkboxes to indicate selection status for each row item. This View is nearly identical to the View we used in the previous version of this project for for the UserRoles View (in fact, I simply made a few quick changes to that one, and renamed it)..

The GroupRoles View:
@model AspNetGroupBasedPermissions.Models.SelectGroupRolesViewModel
@{ ViewBag.Title = "Group Role Permissions"; }
  
<h2>Permissions for Group @Html.DisplayFor(model => model.GroupName)</h2>
<hr />
  
@using (Html.BeginForm("GroupRoles", "Groups", FormMethod.Post, new { encType = "multipart/form-data", name = "myform" }))
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        @Html.ValidationSummary(true)
        <div class="form-group">
            <div class="col-md-10">
                @Html.HiddenFor(model => model.GroupName)
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-10">
                @Html.HiddenFor(model => model.GroupId)
            </div>
        </div>
        <h4>Select Role Permissions for @Html.DisplayFor(model => model.GroupName)</h4>
        <br />
        <hr />
        <table>
            <tr>
                <th>
                    Select
                </th>
                <th>
                    Permissions
                </th>
            </tr>
            @Html.EditorFor(model => model.Roles)
        </table>
        <br />
        <hr />
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

 

In the above, the line @html.EditorFor(model => model.Roles) causes the MVC framework to dig out our (Carefully named!!) SelectRoleEditorViewModel from the Views/Shared/EditorTempates/ directory, and uses that to render each Role item as a table row.

If you have been following this "series" of articles, this should be familiar territory by now.

From here, the rest of these views are rather standard fare.

The Index Group View

Code for the Index Group View:
@model IEnumerable<AspNetGroupBasedPermissions.Models.Group>
@{ ViewBag.Title = "Index"; }
  
<h2>Groups</h2>
  
<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th></th>
    </tr>
@foreach (var item in Model) 
{
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
            @Html.ActionLink("Permissions", "GroupRoles", new { id=item.Id }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.Id })
        </td>
    </tr>
}
</table>

 

Note above, we have added three ActionLinks at the end of each row - "Edit", "Permissions", and "Delete."

These will link us to the appropriate methods on the RolesController. Of specific interest is the "Permissions" link, which will direct us to our GroupRoles method, and allow us to assign one or more Roles ("Permissions") to each group. This, so to speak, is the business end of our authorization management.

The Create Group View

The Create Group View:
@model AspNetGroupBasedPermissions.Models.Group
@{ ViewBag.Title = "Create Groups"; }
  
<h2>Create a new Group</h2>
  
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        <h4>Group</h4>
        <hr />
        @Html.ValidationSummary(true)
        <div class="form-group">
            @Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name)
                @Html.ValidationMessageFor(model => model.Name)
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
@section Scripts { @Scripts.Render("~/bundles/jqueryval") }

 

The Edit Group View

Code for the Edit Group View:
@model AspNetGroupBasedPermissions.Models.Group
@{ ViewBag.Title = "Edit"; }
  
<h2>Edit</h2>
  
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        <h4>Group</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.Id)
        <div class="form-group">
            @Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name)
                @Html.ValidationMessageFor(model => model.Name)
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
@section Scripts { @Scripts.Render("~/bundles/jqueryval") }

 

The Delete Group View

Code for the Delete Group View:
@model AspNetGroupBasedPermissions.Models.Group
@{ ViewBag.Title = "Delete"; }
  
<h2>Delete</h2>
  
<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Group</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>
    </dl>
    @using (Html.BeginForm()) {
        @Html.AntiForgeryToken()
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            @Html.ActionLink("Back to List", "Index")
        </div>
    }
</div>

 

Update AccountController to Assign Users to Groups

Now that we have our Groups controller and Views in place, we need to update the code on our original AccountController. Previously, we have created a UserRoles method on AccountController, by which we assigned one or more Roles to a specific user. Now, instead, we are going to be assigning one or more Groups to specific User.

Open the AccountController file, and delete the UserRoles method, replacing it with the following code for UserGroups:

The UserGroups Method for AccountController:
[Authorize(Roles = "Admin, CanEditUser")]
public ActionResult UserGroups(string id)
{
    var user = _db.Users.First(u => u.UserName == id);
    var model = new SelectUserGroupsViewModel(user);
    return View(model);
}
  
  
[HttpPost]
[Authorize(Roles = "Admin, CanEditUser")]
[ValidateAntiForgeryToken]
public ActionResult UserGroups(SelectUserGroupsViewModel model)
{
    if (ModelState.IsValid)
    {
        var idManager = new IdentityManager();
        var user = _db.Users.First(u => u.UserName == model.UserName);
        idManager.ClearUserGroups(user.Id);
        foreach (var group in model.Groups)
        {
            if (group.Selected)
            {
                idManager.AddUserToGroup(user.Id, group.GroupId);
            }
        }
        return RedirectToAction("index");
    }
    return View();
}

Notice in the above, when we the HTTP Post is returned from the View, we need to clear all the User Group assignments, and then individually add the user to each of the groups selected in the ViewModel.

Replace the UserRoles View with a UserGroups View

With our new UserGroups method in place on AccountController, we need to replace the former UserRoles View with a very similar UserGroups View. As previously, we will display the basic User data for a specific, user, along with a list of available Groups to which the the User might be assigned. Once again, using a table with checkboxes, we can assign the user to one or more Groups.

As with the now-deprecated UserRoles View, and also the newly added GroupRoles View, we need a special Editor Template ViewModel and a correspondingly-named Editor Template View to represent each Group in the table. We have already added a SelectGroupEditorViewModel to the AccountViewModels.cs file. Now we need to add the corresponding Editor Template View.

Add the following View to the Views/Shared/EditorTemplates/ directory.Be careful to name it SelectGroupEditorViewModel so that it exactly matches the name of the ViewModel it represents:

The SelectGroupEditorViewModel View:
@model AspNetGroupBasedPermissions.Models.SelectGroupEditorViewModel
@Html.HiddenFor(model => model.GroupId)
<tr>
    <td style="text-align:center">
        @Html.CheckBoxFor(model => model.Selected)
    </td>
    <td style="padding-right:20px">
        @Html.DisplayFor(model => model.GroupName)
    </td>
</tr>

 

Now, with the in place, delete the old UserRoles View from the Views/Account/ directory, and add a new View named UserGroups as follows:

The UserGoups View:
@model AspNetGroupBasedPermissions.Models.SelectUserGroupsViewModel
@{ ViewBag.Title = "User Groups"; }
  
<h2>Groups for user @Html.DisplayFor(model => model.UserName)</h2>
<hr />
  
@using (Html.BeginForm("UserGroups", "Account", FormMethod.Post, new { encType = "multipart/form-data", name = "myform" }))
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        @Html.ValidationSummary(true)
        <div class="form-group">
            <div class="col-md-10">
                @Html.HiddenFor(model => model.UserName)
            </div>
        </div>
        <h4>Select Group Assignments</h4>
        <br />
        <hr />
        <table>
            <tr>
                <th>
                    Select
                </th>
                <th>
                    Group
                </th>
            </tr>
            @Html.EditorFor(model => model.Groups)
        </table>
        <br />
        <hr />
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

 

Update the Action Links on the Account/Index View

We need to make a minor update to the Account Index View. Currently, the links next to each User in the table indicate "Roles" and point to the (now non-existent) UserRoles method. Instead, we will display the text "Groups" and point the link at the newly added UserGroups method. The modified code should look like the following:

Update the ActionLinks for the Table Items in the Account/Index View:
@model IEnumerable<AspNetGroupBasedPermissions.Models.EditUserViewModel>
@{ ViewBag.Title = "Index"; }
<h2>Index</h2>
<p>
    @Html.ActionLink("Create New", "Register") 
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.UserName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.FirstName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.LastName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Email)
        </th>
        <th></th>
    </tr>
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.UserName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.FirstName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.LastName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Email)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id = item.UserName }) |
            @Html.ActionLink("Groups", "UserGroups", new { id = item.UserName }) |
            @Html.ActionLink("Delete", "Delete", new { id = item.UserName })
        </td>
    </tr>
}
</table>

 

Update Navigation Links on _Layout.cshtml

Now we just need to make sure we can access all of the new functionality we just built into our application. In the middle of the code on the _Layout.cshtml file, we need to update the Navigation links to match this:

Updated Navigation Links on _Layout.cshtml
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
        <li>@Html.ActionLink("Users", "Index", "Account")</li>
        <li>@Html.ActionLink("Groups", "Index", "Groups")</li>
        <li>@Html.ActionLink("Permissions", "Index", "Roles")</li>
    </ul>
    @Html.Partial("_LoginPartial")
</div>

 

Setting up Initial Authorization Permissions

What is not obvious in the code here (unless you cloned the completed project) is that we are implementing our new Users/Groups/Permissions model upon the controllers we just created. Given the example Roles ("Permissions" I included in the Migrations Configuration file (what we are "Seeding" the database with), I set up the initial method-level [Authorize] attributes using the following permissions scheme. If it is not already, add the following [Authorize] Attributes to the appropriate method on each controller. Replace any that exist from the previous project.

Account Controller [Authorize] Roles:

Action Method

Roles Allowed
Login [AllowAnonymous]
Register [Authorize(Roles = "Admin, CanEditUser")]
Manage [Authorize(Roles = "Admin, CanEditUser, User")]
Index [Authorize(Roles = "Admin, CanEditGroup, CanEditUser")]
Edit [Authorize(Roles = "Admin, CanEditUser")]
Delete [Authorize(Roles = "Admin, CanEditUser")]
UserGroups [Authorize(Roles = "Admin, CanEditUser")]

 

Groups Controller [Authorize] Roles:

Action Method

Roles Allowed
Index [Authorize(Roles = "Admin, CanEditGroup, CanEditUser")]
Details [Authorize(Roles = "Admin, CanEditGroup, CanEditUser")]
Create [Authorize(Roles = "Admin, CanEditGroup")]
Edit [Authorize(Roles = "Admin, CanEditGroup")]
Delete [Authorize(Roles = "Admin, CanEditGroup")]
GroupRoles [Authorize(Roles = "Admin, CanEditGroup")]

 

Roles Controller [Authorize] Roles:

Action Method

Roles Allowed
Index [Authorize(Roles = "Admin, CanEditGroup, CanEditUser")]
Create [Authorize(Roles = "Admin")]
Edit [Authorize(Roles = "Admin")]
Delete [Authorize(Roles = "Admin")]

 

As we can see, I have done my best (within space and the practical constraints of this already lengthy example project) to structure a tiered authorization scheme, modestly following a sort of "Principle of Least Privilege." In a production application, we can assume you would have additional business domains, and would need to think through the Role permission assignments with care.

Running the Application

If we start our application, we should be greeted with the standard Login screen. Once logged in, if we navigate to the Groups Link, we should be greeted with a list of the Groups we seeded our database with:

The Groups Screen:

application-groups_thumb3

If we click on the "Permissions" link, we find the roles, or "permissions" currently assigned to a particular group:

Permissions Screen:

application-group-roles_thumb3

If we navigate to the Users screen, we see what we might expect - a list of users, with the option to drill down and see which Groups a specific User belongs to:

The Users Screen:

application-users_thumb2

What might be Handy, though, is an additional link for each listed user whereby we can see what permissions they have as a result of all the groups in which they participate.

Adding a View For User Effective Permissions

Fortunately, we can do just that. First, we need to add one more link to the Accounts/Index View. Near the bottom, where we set up the links next to each row of table data, add the link as below for "Effective Permissions:

Add Effective Permissions Link to Account/Index View:
<td>
    @Html.ActionLink("Edit", "Edit", new { id = item.UserName }) |
    @Html.ActionLink("Groups", "UserGroups", new { id = item.UserName }) |
    @Html.ActionLink("Effective Permissions", "UserPermissions", new { id = item.UserName }) ||| 
    @Html.ActionLink("Delete", "Delete", new { id = item.UserName })
</td>

 

We have already added the UserPermissionsViewModel to AccountViewModels.cs, so now we just need to add a UserPermissions view to the Views/Account directory:

The UserPermissions View:
@model AspNetGroupBasedPermissions.Models.UserPermissionsViewModel
@{ ViewBag.Title = "UserPermissions"; }
  
<h2>Effective Role Permissions for user: @Html.DisplayFor(model => model.UserName)</h2>
<hr />
  
<table class="table">
    <tr>
        <th>
            Role Permission
        </th>
        <th>
           Description
        </th>
        <th></th>
    </tr>
    @foreach (var item in Model.Roles)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.RoleName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
        </tr>
    }
</table>
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

 

Now, if we run our application, we can drill down and see all of the permissions afforded a given user as a result of all the groups in which that user participates:

Navigate to Users to Find Effective Permissions Link:

users-before-effective-permission-se[1]

 

View Effective Permissions for the Selected User across All Ggroups:

users-after-effective-permission-sel[2]

Some Thoughts About Authorization Management and Your Website

The ASP.NET Identity system affords us an easy to use abstraction over a complicated topic, and one which is at the forefront of today's news ("Ripped from today's headlines" so to speak). The ASP.NET Identity system represents one option for securing your site and managing authentication, but it is not the only way. The ASP.NET team presents several options for site security, and in fact Visual Studio gives you several choices as part of setting up your MVC project.

The Identity system appears to be a good choice for public-facing websites, integration with social media providers, and sites with simpler permissions management needs. Other options for managing site security include Active directory integration, and Windows Authentication for intranet-based services.

Of these options, the Identity system is the easiest to implement, and is built-in to the project templates included with ASP.NET and MVC.

As mentioned repeatedly throughout this article, the more finely-grained your security system can be, the more control you have. However, that control comes at the price of complexity. Not just in terms of the code, but also in terms of managing the various Users, Groups, and permission sets you create.

I guarantee you don't want to be sprinkling new roles willy-nilly throughout your application code, and then trying to manage them later. Plot it out ahead of time, with your business domains firmly in mind. Strike a balance between manageability and the Principle of Least Privilege.

The example solution presented here offers a starting point. Keep in mind, though, that the hard-coded nature of the security protections using the [Authorize] attribute could easily become a code-maintenance nightmare if you get carried away. Also, adding the ability to create/edit "permissions" (which. remember, are actually "Roles" so far as the Identity framework is concerned) introduces a new convenience, and also a new dimension for trouble.

I recommend that the creation, editing, and deletion of roles be limited to application developers only (create a special role/group just for them!), and only really as a means to add roles to the database after first adding them to the appropriate [Authorize] attributes in the code.

I am most interested to hear feedback on this. Whether this was the exact solution you have been looking for, or you have spotted an idiotic, gaping flaw in my reasoning. Please feel free to comment, or shoot me an email at the address under the "About the Author" sidebar.

Additional Resources and Items of Interest

 

Posted on February 19 2014 09:02 PM by jatten     

Comments (5)

ASP.NET MVC 5 Identity: Implementing Group-Based Permissions Management Part I

Posted on February 19 2014 09:01 PM by jatten in ASP.NET MVC, ASP.Net, C#, CodeProject   ||   Comments (0)

leeds-castle-portcullis-500Over the course of several recent articles, we're examined various ways and means of working with and extending the ASP.NET Identity System. We've covered the basics of configuring the database connections and working with  the EF Code-First approach used by the Identity System, extending the core IdentityUser class to add our own custom properties and behaviors, such as email addresses, First/Last names, and such. While we did that, we also looked at utilizing the basic Role-based account management which comes with ASP.NET Identity out of the box.

In the last post, we figured out how to extend the IdentityRole class, which took a little more doing than was required with IdentityUser.

Image by Shaun Dunmall | Some Rights Reserved

Here, we are going one step further, and building out a more advanced, "permissions management" model on top of the basic Users/Roles paradigm represented by the core ASP.NET Identity System out of the box.

Update: 2/27/2014: According to reader feedback, there will be some modifications required to the code in this article if you are using the newly-released ASP.NET Identity Preview. If you are checking out the Identity preview, be ready to make some adjustments. If you are using the current, stable 1.0 version, this should work quite well. I will post an article detailing the differences in the near future. 

Before we go too much further, it bears mentioning that implementing a complex permissions management system is not a small undertaking. While the model we are about to look at is not overly difficult, managing a large number of granular permissions in the context of a web application could be. You will want to think hard and plan well before you implement something like this in a production site.

With careful up-front planning, and a well-designed permission structure, you should be able to find a middle ground for your site between bloated, complex, and painful enterprise-type solutions such as Active Directory or Windows Authentication and the overly simple Identity management as it comes out of the box.

More on this later. First, some background.

Granular Management of Authorization Permissions - The Principle of Least Privilege

Good security is designed around (among other things) the Principle of Least Privilege. That is, "in a particular abstraction layer of a computing environment, every module (such as a process, a user or a program depending on the subject) must be able to access only the information and resources that are necessary for its legitimate purpose"

As we are well aware by now, the primary way we manage access to different functionality within our ASP.NET MVC application is through the [Authorize] attribute. We decorate specific controller methods with [Authorize] and define which roles can execute the method. For example, we may be building out a site for a business. Among other things, the site will likely contain any number of operational or business domains, such as Site Administration, Human Resources, Sales, Order Processing, and so on.

A hypothetical PayrollController might contain, among others, the following methods:

Methods from a hypothetical Payroll Controller:
[Authorize(Roles = "HrAdmin, CanEnterPayroll")]
[HttpPost]
public ActionResult EnterPayroll(string id)
{
    //  . . . Enter some payroll . . . 
}
  
  
[Authorize(Roles = "HrAdmin, CanEditPayroll, CanProcessPayroll")]
[HttpPost]
public ActionResult EditPayroll(string id)
{
    //  . . . Edit existing payroll entries . . . 
}
  
  
[Authorize(Roles = "HrAdmin, CanProcessPayroll")]
[HttpPost]
public ActionResult ProcessPayroll(string id)
{
    //  . . . Process payroll and cut checks . . . 
}

 

We infer from the above that the grunts who simply enter the payroll information have no business editing work already in the system. On the other hand, there are those in the company who may need to be able to edit existing payroll, which might include the managers of particular employees departments, the HR Manager themselves, and those whose job it is to process the payroll.

The action of actually processing payroll and creating checks for payment is very restricted. Only the HR manager, and those members of the "ProcessPayroll" role are able to do this, and we can assume their number is few.

Lastly, we see that the HrAdmin role has extensive privileges, including all of these functions, and also  presumable is able to act as the administrator within the Human Resources application Domain, assigning these and other domain permissions to the various users within the domain. 

Limitations of Application Authorization Under Identity

Under the current Identity system's out-of-the-box implementation (even with the ways in which we have extended it over these last few articles), We have Users, and Roles. Users are assigned to one or more roles as part of our security setup, and Admins are able to add or remove users from various roles.

Role access to various application functionality is hard-coded into our application via [Authorize], so creating and modifying roles in production is of little value, unless we have implemented some other business reason for it.

Also under the current system, each time we add a new user to the system, we need to assign individual roles specific to the user. This is not a big deal if our site includes (for example) "Admins", "Authors" and "Users." However, for a more complex site, with multiple business domains, and multiple users serving in multiple roles, this could become painful.

When security administration becomes painful, we tend to default to time-saving behavior, such as ignoring the Principle of Least Privilege, and instead granting users broad permissions so we don't have to bother (at least, if we don't have a diligent system admin!).

A Middle of the Road Solution

In this article, we examine one possible manner of extending the Identity model to form a middle-of-the road solution. For applications of moderate complexity, which require a little more granularity in authorization permissions, but which may not warrant moving to a heavy-weight solutions such as Active Directory.

I am proposing the addition of what appear to be authorization Groups to the identity mix. Groups are assigned various combinations of permissions, and Users are assigned to one or more groups.

To do this, we will be creating a slight illusion. We will simply be treating what we currently recognize as Roles as, instead, Permissions. We will then create groups of these "Role-Permissions" and assign users to one or more groups. Behind the scenes, of course, we are still constrained by the essential elements of Identity; Users and Roles. We are also still limited by having to hard-code our "Permissions" into [Authorize] attributes. However, we can define these "Role-Permissions" at a fairly granular level now, because managing assignment of Role Permissions to users will be done by assigning Users to Groups, at which point such a user will assume all of the specific permissions of each particular Group.

Building on Previous Work

I started with the foundation we have built so far, by cloning the project from the last article where we extended our Roles by inheriting from IdentityRole. We didn't do anything earth-shaking in that, but we did get a closer look at how we might override the OnModelCreating() method of ApplicationDbContext and bend EF and the Identity framework to our will, without compromising the underlying security mechanisms created by the ASP.NET team.

You can either do the same, and follow along as we walk through building this out, or you can clone the finished source from this article.

Get the Original Source from Github:
Get the Completed Source for this Article:

As in previous articles, once I have cloned the initial source project, I renamed the solution files, namespaces, directory, and project files, since in my case, I will be pushing this up as a new project, not as new changes to the old.

Next, delete the existing Migrations files (but not the Migrations folder, and not the Configuration.cs file). We will be adding to our Code-First model before we build the database, so we don't need these files anymore.

Now, we're ready to get started.

Adding the Group and ApplicationRoleGroup Models

First, of course, we need our Group class. The Group class will represent a named group of roles, and therefore we consider that the Group class has a collection of roles.  However, since each Group can include zero or many roles, and each role can also belong to zero or many Groups, this will be a many-to-many mapping in our database. Therefore, we first need an intermediate object, ApplicationRoleGroup which maps the foreign keys in the many-to-many relationship.

Add the following classes to the Models folder:

The Application Role Group Model Class:
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
  
  
namespace AspNetGroupBasedPermissions.Models
{
    public class ApplicationRoleGroup
    {
        public virtual string RoleId { get; set; }
        public virtual int GroupId { get; set; }
  
        public virtual ApplicationRole Role { get; set; }
        public virtual Group Group { get; set; }
    }
}

 

Then add the Group class as another new class in Models:

The Group Class:
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
  
namespace AspNetGroupBasedPermissions.Models
{
    public class Group
    {
        public Group() {}
  
  
        public Group(string name) : this()
        {
            this.Roles = new List<ApplicationRoleGroup>();
            this.Name = name;
        }
  
  
        [Key]
        [Required]
        public virtual int Id { get; set; }
  
        public virtual string Name { get; set; }
        public virtual ICollection<ApplicationRoleGroup> Roles { get; set; }
    }
}

 

Next, we need to create a similar many-to-many mapping model for ApplicationUser and Group. Once again, each user can have zero or many groups, and each group can have zero or many users. We already have our ApplicationUser class (although we need to modify it a little), but we need an ApplicationUserGroup class to complete the mapping.

Add the ApplicationUserGroup Model

Add the ApplicationUserGroup class to the Models folder:

using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
  
namespace AspNetGroupBasedPermissions.Models
{
    public class ApplicationUserGroup
    {
        [Required]
        public virtual string UserId { get; set; }
        [Required]
        public virtual int GroupId { get; set; }
  
        public virtual ApplicationUser User { get; set; }
        public virtual Group Group { get; set; }
    }
}

 

Next, we need to add a Groups Property to ApplicationUser, in such a manner that Entity Framework will understand and be able to use it to populate the groups when the property is accessed. This means we need to add a virtual property which returns a instance of ICollection<ApplicationUserGroup> when the property is accessed.

Modify the existing ApplicationUser class as follows:

Modified ApplicationUser Class:
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
  
namespace AspNetGroupBasedPermissions.Models
{
    public class ApplicationUser : IdentityUser
    {
        public ApplicationUser()
            : base()
        {
            this.Groups = new HashSet<ApplicationUserGroup>();
        }
  
        [Required]
        public string FirstName { get; set; }
  
        [Required]
        public string LastName { get; set; }
  
        [Required]
        public string Email { get; set; }
  
        public virtual ICollection<ApplicationUserGroup> Groups { get; set; }
    }
}

 

Update ApplicationDbContext to Reflect the New Model

Now that we have extended our model somewhat, we need to update the OnModelCreating method of ApplicationDbContext so that EF can properly model our database, and work with our objects.

** This whole method becomes a little messy and cluttered, but a discerning read of the code reveals the gist of what is happening here. Don't worry too much about understanding the details of this code - just try to get a general picture of how it is mapping model entities to database tables. **

Update the OnModelCreating() method of ApplicationDbContext as follows:

Modified OnModelCreating Method for ApplicationDbContext:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    if (modelBuilder == null)
    {
        throw new ArgumentNullException("modelBuilder");
    }
    // Keep this:
    modelBuilder.Entity<IdentityUser>().ToTable("AspNetUsers");
    // Change TUser to ApplicationUser everywhere else - IdentityUser 
    // and ApplicationUser essentially 'share' the AspNetUsers Table in the database:
    EntityTypeConfiguration<ApplicationUser> table = 
        modelBuilder.Entity<ApplicationUser>().ToTable("AspNetUsers");
    table.Property((ApplicationUser u) => u.UserName).IsRequired();
    // EF won't let us swap out IdentityUserRole for ApplicationUserRole here:
    modelBuilder.Entity<ApplicationUser>().HasMany<IdentityUserRole>((ApplicationUser u) => u.Roles);
    modelBuilder.Entity<IdentityUserRole>().HasKey((IdentityUserRole r) => 
        new { UserId = r.UserId, RoleId = r.RoleId }).ToTable("AspNetUserRoles");
    // Add the group stuff here:
    modelBuilder.Entity<ApplicationUser>().HasMany<ApplicationUserGroup>((ApplicationUser u) => u.Groups);
    modelBuilder.Entity<ApplicationUserGroup>().HasKey((ApplicationUserGroup r) => 
        new { UserId = r.UserId, GroupId = r.GroupId }).ToTable("ApplicationUserGroups");
    // And here:
    modelBuilder.Entity<Group>().HasMany<ApplicationRoleGroup>((Group g) => g.Roles);
    modelBuilder.Entity<ApplicationRoleGroup>().HasKey((ApplicationRoleGroup gr) => 
        new { RoleId = gr.RoleId, GroupId = gr.GroupId }).ToTable("ApplicationRoleGroups");
    // And Here:
    EntityTypeConfiguration<Group> groupsConfig = modelBuilder.Entity<Group>().ToTable("Groups");
    groupsConfig.Property((Group r) => r.Name).IsRequired();
    // Leave this alone:
    EntityTypeConfiguration<IdentityUserLogin> entityTypeConfiguration = 
        modelBuilder.Entity<IdentityUserLogin>().HasKey((IdentityUserLogin l) => 
            new { UserId = l.UserId, LoginProvider = l.LoginProvider, ProviderKey = 
                l.ProviderKey }).ToTable("AspNetUserLogins");
    entityTypeConfiguration.HasRequired<IdentityUser>((IdentityUserLogin u) => u.User);
    EntityTypeConfiguration<IdentityUserClaim> table1 = 
        modelBuilder.Entity<IdentityUserClaim>().ToTable("AspNetUserClaims");
    table1.HasRequired<IdentityUser>((IdentityUserClaim u) => u.User);
    // Add this, so that IdentityRole can share a table with ApplicationRole:
    modelBuilder.Entity<IdentityRole>().ToTable("AspNetRoles");
    // Change these from IdentityRole to ApplicationRole:
    EntityTypeConfiguration<ApplicationRole> entityTypeConfiguration1 = 
        modelBuilder.Entity<ApplicationRole>().ToTable("AspNetRoles");
    entityTypeConfiguration1.Property((ApplicationRole r) => r.Name).IsRequired();
}

 

Next, we need to explicitly add a Groups property on ApplicationDbContext. Once again, this needs to be a virtual property, but in this case the return type is ICollection<Group>:

Add the Groups Property to ApplicationDbcontext:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    // Add an instance IDbSet using the 'new' keyword:
    new public virtual IDbSet<ApplicationRole> Roles { get; set; }
  
    // ADD THIS:
    public virtual IDbSet<Group> Groups { get; set; }
  
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }
  
  
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Code we just added above is here . .  .
    }
  
    // Etc . . .
}

 

Add Group Management Items to Identity Manager

Now, let's add some code to help us manage the various functionality we need related to Groups, and management of users and Roles ("permissions") related to Groups. Look over the methods below carefully to understand just what is going on most of the time, as several of the actions one might take upon a group have potential consequences across the security spectrum.

For example, when we delete a group, we need to also:

  • Remove all the users from the group. Remember, there is a foreign key relationship here with an intermediate or "relations table" - related records need to be removed first, or we will generally get a key constraint error.
  • Remove all the roles from that group. Remember, there is a foreign key relationship here with an intermediate or "relations table" - related records need to be removed first, or we will generally get a key constraint error.
  • Remove the roles from each user, except when that user has the same role resulting from membership in another group (this was a pain to think through!).

Likewise, when we add a Role ("Permission") to a group, we need to update all of the users in that group to reflect the added permission.

Add the following methods to the bottom of the existing IdentityManager class:

Add Group Methods to Identity Manager Class:
public void CreateGroup(string groupName)
{
    if (this.GroupNameExists(groupName))
    {
        throw new System.Exception("A group by that name already exists in the database. Please choose another name.");
    }
  
    var newGroup = new Group(groupName);
    _db.Groups.Add(newGroup);
    _db.SaveChanges();
}
  
  
public bool GroupNameExists(string groupName)
{
    var g = _db.Groups.Where(gr => gr.Name == groupName);
    if (g.Count() > 0)
    {
        return true;
    }
    return false;
}
  
  
public void ClearUserGroups(string userId)
{
    this.ClearUserRoles(userId);
    var user = _db.Users.Find(userId);
    user.Groups.Clear();
    _db.SaveChanges();
}
  
  
public void AddUserToGroup(string userId, int GroupId)
{
    var group = _db.Groups.Find(GroupId);
    var user = _db.Users.Find(userId);
  
    var userGroup = new ApplicationUserGroup()
    {
        Group = group,
        GroupId = group.Id,
        User = user,
        UserId = user.Id
    };
  
    foreach (var role in group.Roles)
    {
        _userManager.AddToRole(userId, role.Role.Name);
    }
    user.Groups.Add(userGroup);
    _db.SaveChanges();
}
  
  
public void ClearGroupRoles(int groupId)
{
    var group = _db.Groups.Find(groupId);
    var groupUsers = _db.Users.Where(u => u.Groups.Any(g => g.GroupId == group.Id));
  
    foreach (var role in group.Roles)
    {
        var currentRoleId = role.RoleId;
        foreach (var user in groupUsers)
        {
            // Is the user a member of any other groups with this role?
            var groupsWithRole = user.Groups
                .Where(g => g.Group.Roles
                    .Any(r => r.RoleId == currentRoleId)).Count();
            // This will be 1 if the current group is the only one:
            if (groupsWithRole == 1)
            {
                this.RemoveFromRole(user.Id, role.Role.Name);
            }
        }
    }
    group.Roles.Clear();
    _db.SaveChanges();
}
  
  
public void AddRoleToGroup(int groupId, string roleName)
{
    var group = _db.Groups.Find(groupId);
    var role = _db.Roles.First(r => r.Name == roleName);
    var newgroupRole = new ApplicationRoleGroup()
    {
        GroupId = group.Id,
        Group = group,
        RoleId = role.Id,
        Role = (ApplicationRole)role
    };
  
    group.Roles.Add(newgroupRole);
    _db.SaveChanges();
  
    // Add all of the users in this group to the new role:
    var groupUsers = _db.Users.Where(u => u.Groups.Any(g => g.GroupId == group.Id));
    foreach (var user in groupUsers)
    {
        if(!(_userManager.IsInRole(user.Id, roleName)))
        {
            this.AddUserToRole(user.Id, role.Name);
        }
    }
}
  
  
public void DeleteGroup(int groupId)
{
    var group = _db.Groups.Find(groupId);
  
    // Clear the roles from the group:
    this.ClearGroupRoles(groupId);
    _db.Groups.Remove(group);
    _db.SaveChanges();
}

 

We now have the core code needed to manage the relationships between Users, Groups, and Roles ("Permissions") in the back end. Now we need to set up our Migrations Configuration file to properly seed our database when we run EF Migrations.

Update the Migrations Configuration File to Seed the Database

Most of the basic model stuff is now in place such that we can run EF Migrations and build out our modified database. Before we do that, though, we want to update our Migrations Configuration class so that we seed our database with the minimal required data to function. Remember, our site is closed to "public" registration. Therefore, at the very least we need to seed it with an initial admin-level user, just like before.

What is NOT like before is that we have changed the manner in which roles are assigned and managed. Going forward, we need to seed our initial user, along with one or more initial Groups, and seed at least one of those groups with sufficient admin permissions that our initial user can take it from there.

There are many ways this code could be written. Further, depending upon your application requirements, how the database is seeded may become an extensive exercise in planning (remember that bit about how a more complex authorization model requires more and more up-front planning?).

Here, we are going to update our Configuration class with a few new methods. We will add an initial user, a handful of potentially useful Groups, and some roles relevant to managing security and authorization.

Updated Migrations Configuration File:
internal sealed class Configuration 
    : DbMigrationsConfiguration<ApplicationDbContext>
{
    IdentityManager _idManager = new IdentityManager();
    ApplicationDbContext _db = new ApplicationDbContext();
    public Configuration()
    {
        AutomaticMigrationsEnabled = true;
    }
  
  
    protected override void Seed(ApplicationDbContext context)
    {
        this.AddGroups();
        this.AddRoles();
        this.AddUsers();
        this.AddRolesToGroups();
        this.AddUsersToGroups();
    }
   
    string[] _initialGroupNames = 
        new string[] { "SuperAdmins", "GroupAdmins", "UserAdmins", "Users" };
    public void AddGroups()
    {
        foreach (var groupName in _initialGroupNames)
        {
            _idManager.CreateGroup(groupName);
        }
    }
  
  
    void AddRoles()
    {
        // Some example initial roles. These COULD BE much more granular:
        _idManager.CreateRole("Admin", "Global Access");
        _idManager.CreateRole("CanEditUser", "Add, modify, and delete Users");
        _idManager.CreateRole("CanEditGroup", "Add, modify, and delete Groups");
        _idManager.CreateRole("CanEditRole", "Add, modify, and delete roles");
        _idManager.CreateRole("User", "Restricted to business domain activity");
    }
  
  
    string[] _superAdminRoleNames = 
        new string[] { "Admin", "CanEditUser", "CanEditGroup", "CanEditRole", "User" };
    string[] _groupAdminRoleNames =
        new string[] { "CanEditUser", "CanEditGroup", "User" };
    string[] _userAdminRoleNames =
        new string[] { "CanEditUser", "User" };
    string[] _userRoleNames =
        new string[] { "User" };
    void AddRolesToGroups()
    {
        // Add the Super-Admin Roles to the Super-Admin Group:
        var allGroups = _db.Groups;
        var superAdmins = allGroups.First(g => g.Name == "SuperAdmins");
        foreach (string name in _superAdminRoleNames)
        {
            _idManager.AddRoleToGroup(superAdmins.Id, name);
        }
  
        // Add the Group-Admin Roles to the Group-Admin Group:
        var groupAdmins = _db.Groups.First(g => g.Name == "GroupAdmins");
        foreach (string name in _groupAdminRoleNames)
        {
            _idManager.AddRoleToGroup(groupAdmins.Id, name);
        }
  
        // Add the User-Admin Roles to the User-Admin Group:
        var userAdmins = _db.Groups.First(g => g.Name == "UserAdmins");
        foreach (string name in _userAdminRoleNames)
        {
            _idManager.AddRoleToGroup(userAdmins.Id, name);
        }
  
        // Add the User Roles to the Users Group:
        var users = _db.Groups.First(g => g.Name == "Users");
        foreach (string name in _userRoleNames)
        {
            _idManager.AddRoleToGroup(users.Id, name);
        }
    }
  
  
    // Change these to your own:
    string _initialUserName = "jatten";
    string _InitialUserFirstName = "John";
    string _initialUserLastName = "Atten";
    string _initialUserEmail = "jatten@typecastexception.com";
    void AddUsers()
    {
        var newUser = new ApplicationUser()
        {
            UserName = _initialUserName,
            FirstName = _InitialUserFirstName,
            LastName = _initialUserLastName,
            Email = _initialUserEmail
        };
  
        // Be careful here - you  will need to use a password which will 
        // be valid under the password rules for the application, 
        // or the process will abort:
        _idManager.CreateUser(newUser, "Password1");
    }
  
  
    // Configure the initial Super-Admin user:
    void AddUsersToGroups()
    {
        var user = _db.Users.First(u => u.UserName == _initialUserName);
        var allGroups = _db.Groups;
        foreach (var group in allGroups)
        {
            _idManager.AddUserToGroup(user.Id, group.Id);
        }
    }
}

 

As you can see in the above, I have (rather arbitrarily) decided to set up some initial groups and roles related to the Users/Groups/Roles domain. If we already knew the domain structure of the rest of our application, we might want to include additional roles ("Permissions") as part of our Configuration, since roles need to be hard-coded into our controllers using the [Authorize] attribute. The earlier we can determine the role structure for our application security model, the better. You will want to strike a balance between granularity and manageability here, though.

For the moment, we have a sufficient starting point, and we are ready to run EF Migrations and see if our database is built successfully.

Run Migrations and Build Out the Database

As mentioned previously, as I did this, I deleted the previous Migration files, but left the Migrations folder intact, with the (now modified Configuration.cs file). Therefore, in order to perform the migration, I simply type the following into the Package Manager Console:

Add New Migration:
PM> Add-Migration init

 

This scaffolds up a new migration. Next:

Build Out the Database:
PM> Update-Database

 

If everything went well, we should be able to open our database in the Visual Studio Server Explorer and see how we did. You should see something like this:

The Database in VS Server Explorer:

vs-server-explorer-database-view

Looks like everything went ok!

Next: Controllers, ViewModels, and Views

This article became long enough that I decided to break it into two parts. In this post, we figured out how to model our Users, Groups, and Roles ("Permissions") in our application, and by extension, in our database via EF Code-First and Migrations.

Next, we will start pulling all this together into the business end of our application

Next:  Part II - Controllers, ViewModels, and Views --->

 

Additional Resources and Items of Interest

 

Posted on February 19 2014 09:01 PM by jatten     

Comments (0)

ASP.NET MVC 5 Identity: Extending and Modifying Roles

Posted on February 13 2014 09:17 PM by jatten in ASP.NET MVC, ASP.Net, C#, CodeProject   ||   Comments (8)

 

security-cam-by-hay-kranenIn a recent article I took a rather long look at extending the ASP.NET 5 Identity model, adding some custom properties to the basic IdentityUser class, and also some basic role-based identity management. We did not discuss modifying, extending, or working directly with Roles, beyond seeding the database with some very basic roles with which to manage application access.

Extending the basic ASP.NET IdentityRole class, and working directly with roles from an administrative perspective, requires some careful consideration, and no small amount of work-around code-wise.

Image by Hay Kranen | Some Rights Reserved

There are two reasons this is so:

  • As we saw in the previous article, the ASP.NET team made it fairly easy to extend IdentityUser, and also to get some basic Role management happening.
  • When roles are used to enforce access restrictions within our application, they are basically hard-coded, usually via the [Authorize] attribute. Giving application administrators the ability to add, modify, and delete roles is of limited use if they cannot also modify the access permissions afforded by [Authorize] .

The above notwithstanding, sometimes we might wish to add some properties to our basic roles, such as a brief description.

If you are using the Identity 2.0 Framework:

This article focuses on customizing and modifying version 1.0 of the ASP.NET Identity framework. If you are using the recently released version 2.0, this code in this article won't work. For more information on working with Identity 2.0, see

Many of the customizations implemented in this article are included "ini the box" with the Identity Samples project. I discuss extending and customizing IdentityUser and IdentityRole in Identity 2.0 in a new article, ASP.NET Identity 2.0: Customizing Users and Roles

If you are using the Identity 1.0 Framework:

Keep Reading!

In this post we will see how we can extend the IdentityRole class, by adding an additional property. We will also add the basic ability to create, edit, and delete roles, and what all is involved with that, despite the fact that any advantage to adding or removing roles is limited by the hard-coded [Authorize] permissions within our application.

In the next post, ASP.NET MVC 5 Identity: Implementing Group-Based Permissions Management,  look at working around the limitations of the Role/[Authorize] model to create a more finely-grained role-based access control system.

UPDATE: 2/24/2014 - Thanks to Code Project user Budoray for catching some typos in the code. The EditRoleViewModel and RoleViewModel classes were referenced incorrectly  in a number of places, preventing the project from building properly. Fixed!

In an upcoming post we will look at working around the limitations of the Role/[Authorize] model to create a more finely-grained role-based access control system.

Getting Started - Building on Previous Work

We have laid the groundwork for what we will be doing in the previous article on Extending Identity Accounts, so we will clone that project and build on top of the work already done. In that project, we:

  • Created a restricted, internal access MVC site.
  • Removed extraneous code related to social media account log-ins, and other features we don't need.
  • Extended the IdentityUser class to include some additional properties, such as first/last names, and email addresses.
  • Added the ability to assign users to pre-defined roles which govern access to various functionality within our application.

Clone the Source

You can clone the original project and follow along, or you can grab the finished project from my Github repo. To get the original project and build along with this article, clone the source from:

If you want to check out the finished project, clone the source from:

First, a Little Refactoring

In the original project, I had left all of the Identity-related models in the single file created with the default project template. Before getting started here, I pulled each class out into its own code file. Also, we will be re-building our database and migrations.

After cloning the source, I deleted the existing Migration (not the Migrations folder, just the Migration file within named 201311110510410_Init.cs. We will keep the Migrations/Configuration.cs file as we will be building out on that as we go.

If you are following along, note that I also renamed the project and solution, namespaces, and such, as I am going to push this project up to Github separately from the original.

There is plenty of room for additional cleanup in this project, but for now, it will do. Let's get started.

Getting Started

Previously, we were able to define the model class ApplicationUser, which extended the Identity class IdentityUser, run EF Migrations, and with relative ease swap it with IdentityUser in all the areas of our application which previously consumed IdentityUser. Things are not so simple, however, when it comes to extending IdentityRole.

IdentityRole forms a core component in the authorization mechanism for an ASP.NET application. For this reason, we might expect the Identity system to be resistant to casual modification of the IdentityRole class itself, and perhaps equally importantly, the manner in which the rest of the identity system accepts derivatives. So we need to find a way to accomplish what we wish to achieve without compromising the integrity of the Identity mechanism, or those components downstream which may depend upon an instance of IdentityRole to get the job done.

First off, let's take a look at our existing ApplicationDbContext class:

The ApplicationDbContext Class:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }
}

 

In the above, we can see we are inheriting from the class IdentityDbContext<TUser>, which allows us to specify a custom type, so long as that type is derived from IdentityUser. So it appears that the Identity system generally provides a built-in mechanism for extending IdentityUser.

Is there a similar path for extending IdentityRole?

Turns out there is. Sort of.

Extending the Identity Role Class

First, of course, we need to create our derived class, ApplicationRole. Add the following class to the Models folder:

The Application Role Class:
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
namespace AspNetExtendingIdentityRoles.Models
{
    public class ApplicationRole : IdentityRole
    {
        public ApplicationRole() : base() { }
        public ApplicationRole(string name, string description) : base(name)
        {
            this.Description = description;
        }
        public virtual string Description { get; set; }
    }
}

 

As we can see, we have created a derived class and implemented a simple new Description property, along with a new overridden constructor.

Next, we need modify our ApplicationDbContext so that, when we run EF Migrations, our database will reflect the proper modeling. Open the ApplicationDbContext class, and add the following override for the OnModelCreating method:

Add These Namespaces to the Top of your Code File:
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Data.Entity;
using System;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;
using System.Data.Entity.Validation;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

 

Then add the following Code to the ApplicationDbContext class:

Overriding the OnModelCreating method:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    if (modelBuilder == null)
    {
        throw new ArgumentNullException("modelBuilder");
    }
  
    // Keep this:
    modelBuilder.Entity<IdentityUser>().ToTable("AspNetUsers");
  
    // Change TUser to ApplicationUser everywhere else - 
    // IdentityUser and ApplicationUser essentially 'share' the AspNetUsers Table in the database:
    EntityTypeConfiguration<ApplicationUser> table = 
        modelBuilder.Entity<ApplicationUser>().ToTable("AspNetUsers");
  
    table.Property((ApplicationUser u) => u.UserName).IsRequired();
  
    // EF won't let us swap out IdentityUserRole for ApplicationUserRole here:
    modelBuilder.Entity<ApplicationUser>().HasMany<IdentityUserRole>((ApplicationUser u) => u.Roles);
    modelBuilder.Entity<IdentityUserRole>().HasKey((IdentityUserRole r) => 
        new { UserId = r.UserId, RoleId = r.RoleId }).ToTable("AspNetUserRoles");
  
    // Leave this alone:
    EntityTypeConfiguration<IdentityUserLogin> entityTypeConfiguration = 
        modelBuilder.Entity<IdentityUserLogin>().HasKey((IdentityUserLogin l) => 
            new { UserId = l.UserId, LoginProvider = l.LoginProvider, ProviderKey 
            	= l.ProviderKey }).ToTable("AspNetUserLogins");
  
    entityTypeConfiguration.HasRequired<IdentityUser>((IdentityUserLogin u) => u.User);
    EntityTypeConfiguration<IdentityUserClaim> table1 = 
    	modelBuilder.Entity<IdentityUserClaim>().ToTable("AspNetUserClaims");
  
    table1.HasRequired<IdentityUser>((IdentityUserClaim u) => u.User);
  
    // Add this, so that IdentityRole can share a table with ApplicationRole:
    modelBuilder.Entity<IdentityRole>().ToTable("AspNetRoles");
  
    // Change these from IdentityRole to ApplicationRole:
    EntityTypeConfiguration<ApplicationRole> entityTypeConfiguration1 = 
    	modelBuilder.Entity<ApplicationRole>().ToTable("AspNetRoles");
  
    entityTypeConfiguration1.Property((ApplicationRole r) => r.Name).IsRequired();
}

          In the above, we are basically telling Entity Framework how to model our inheritance structure into the database.

          We can, however, tell EF to model our database in such a way that both of our derived classes can also utilize the same tables, and in fact extend them to include our custom fields. Notice how, in the code above, we first tell the modelBuilder to point the IdentityUser class at the table "AspNetUsers", and then also tell it to point ApplicationUser at the same table?

          We do the same thing later with ApplicationRole.

          As you can see, there is actually no getting away from either the IdentityUser or IdentityRole classes - both are used by the Identity system under the covers.  We are simply taking advantage of polymorphism such that, at the level of our application, we are able to use our derived classes, while down below, Identity recognizes them as their base implementations. We will see how this affects our application shortly.

          Update the Identity Manager Class

          Now, however, we can replace IdentityRole with ApplicationRole in most of the rest of our application, and begin using our new Description property.

          We will begin with our IdentityManager class. I did a little refactoring here while I was at it, so if you are following along, this code will look a little different than what you will find in the original project. Just paste this code in (but make sure your namespaces match!). I also added a few new using's at the top of the code file.

          Modified Identity Manager Class:
          public class IdentityManager
          
          {
          
              // Swap ApplicationRole for IdentityRole:
          
              RoleManager<ApplicationRole> _roleManager = new RoleManager<ApplicationRole>(
          
                  new RoleStore<ApplicationRole>(new ApplicationDbContext()));
          
            
          
              UserManager<ApplicationUser> _userManager = new UserManager<ApplicationUser>(
          
                  new UserStore<ApplicationUser>(new ApplicationDbContext()));
          
            
          
              ApplicationDbContext _db = new ApplicationDbContext();
          
            
          
            
          
              public bool RoleExists(string name)
          
              {
          
                  return _roleManager.RoleExists(name);
          
              }
          
            
          
            
          
              public bool CreateRole(string name, string description = "")
          
              {
          
                  // Swap ApplicationRole for IdentityRole:
          
                  var idResult = _roleManager.Create(new ApplicationRole(name, description));
          
                  return idResult.Succeeded;
          
              }
          
            
          
            
          
              public bool CreateUser(ApplicationUser user, string password)
          
              {
          
                  var idResult = _userManager.Create(user, password);
          
                  return idResult.Succeeded;
          
              }
          
            
          
            
          
              public bool AddUserToRole(string userId, string roleName)
          
              {
          
                  var idResult = _userManager.AddToRole(userId, roleName);
          
                  return idResult.Succeeded;
          
              }
          
            
          
            
          
              public void ClearUserRoles(string userId)
          
              {
          
                  var user = _userManager.FindById(userId);
          
                  var currentRoles = new List<IdentityUserRole>();
          
            
          
                  currentRoles.AddRange(user.Roles);
          
                  foreach (var role in currentRoles)
          
                  {
          
                      _userManager.RemoveFromRole(userId, role.Role.Name);
          
                  }
          
              }
          
          }

           

          Update the Add Users and Roles Method in Migrations Configuration

          We will also want to update our AddUsersAndRoles() method, which is called by the Seed() method in the Configuration file for EF Migrations. We want to seed the database with Roles which use our new extended properties:

          The Updated Add Users and Groups Method:
          bool AddUserAndRoles()
          
          {
          
              bool success = false;
          
              var idManager = new IdentityManager();
          
              // Add the Description as an argument:
          
              success = idManager.CreateRole("Admin", "Global Access");
          
              if (!success == true) return success;
          
              // Add the Description as an argument:
          
              success = idManager.CreateRole("CanEdit", "Edit existing records");
          
              if (!success == true) return success;
          
              // Add the Description as an argument:
          
              success = idManager.CreateRole("User", "Restricted to business domain activity");
          
              if (!success) return success;
          
              // While you're at it, change this to your own log-in:
          
              var newUser = new ApplicationUser()
          
              {
          
                  UserName = "jatten",
          
                  FirstName = "John",
          
                  LastName = "Atten",
          
                  Email = "jatten@typecastexception.com"
          
              };
          
              // Be careful here - you  will need to use a password which will 
          
              // be valid under the password rules for the application, 
          
              // or the process will abort:
          
              success = idManager.CreateUser(newUser, "Password1");
          
              if (!success) return success;
          
              success = idManager.AddUserToRole(newUser.Id, "Admin");
          
              if (!success) return success;
          
              success = idManager.AddUserToRole(newUser.Id, "CanEdit");
          
              if (!success) return success;
          
              success = idManager.AddUserToRole(newUser.Id, "User");
          
              if (!success) return success;
          
              return success;
          
          }

           

          All we really did here was pass an additional argument to the CreateRole() method, such the the seed roles will exhibit our new property.

          Now, we need to make a few adjustments to our Account Controller, View Models, and Views.

          Update the Select Role Editor View Model

          In the previous article, we created a SelectRoleEditorViewModel which accepted an instance of IdentityRole as a constructor argument. We need to modify the code here in order to accommodate any new properties we added when we extended IdentityRole. For our example, we just added a single new property, so this is pretty painless:

          The Modified SelectRoleEditorViewModel:
          public class SelectRoleEditorViewModel
          
          {
          
              public SelectRoleEditorViewModel() { }
          
            
          
              // Update this to accept an argument of type ApplicationRole:
          
              public SelectRoleEditorViewModel(ApplicationRole role)
          
              {
          
                  this.RoleName = role.Name;
          
            
          
                  // Assign the new Descrption property:
          
                  this.Description = role.Description;
          
              }
          
            
          
            
          
              public bool Selected { get; set; }
          
            
          
              [Required]
          
              public string RoleName { get; set; }
          
            
          
              // Add the new Description property:
          
              public string Description { get; set; }
          
          }

           

          Update the Corresponding Editor View Model

          Recall that, in order to display the list of roles with checkboxes as an HTML form from which we can return the selection choices made by the user, we needed to define an EditorViewModel.cshtml file which corresponded to our EditorViewModel class. In Views/Shared/EditorViewModels open SelectRoleEditorViewModel.cshtml and make the following changes:

          The Modified SelectRoleEditorViewModel:
          @model AspNetExtendingIdentityRoles.Models.SelectRoleEditorViewModel
          
          @Html.HiddenFor(model => model.RoleName)
          
          <tr>
          
              <td style="text-align:center">
          
                  @Html.CheckBoxFor(model => model.Selected)
          
              </td>
          
              <td style="padding-right:20px">
          
                  @Html.DisplayFor(model => model.RoleName)
          
              </td>
          
              <td style="padding-right:20px">
          
                  @Html.DisplayFor(model => model.Description)
          
              </td>
          
          </tr>

          Again, all we needed to do in the above was add a table data element for the new property.

          Update the User Roles View

          To this point, we actually only have one View which displays our Roles - the UserRoles view, where we assign users to one or more Roles within our application. Once again, we really just need to add a table Header element to represent our new Description property:

          The Updated UserRoles View:

          @model AspNetExtendingIdentityRoles.Models.SelectUserRolesViewModel
          
            
          
          @{
          
              ViewBag.Title = "User Roles";
          
          }
          
          <h2>Roles for user @Html.DisplayFor(model => model.UserName)</h2>
          
          <hr />
          
          @using (Html.BeginForm("UserRoles", "Account", FormMethod.Post, new { encType = "multipart/form-data", name = "myform" }))
          
          {
          
              @Html.AntiForgeryToken()
          
            
          
              <div class="form-horizontal">
          
                  @Html.ValidationSummary(true)
          
                  <div class="form-group">
          
                      <div class="col-md-10">
          
                          @Html.HiddenFor(model => model.UserName)
          
                      </div>
          
                  </div>
          
            
          
                  <h4>Select Role Assignments</h4>
          
                  <br />
          
                  <hr />
          
            
          
                  <table>
          
                      <tr>
          
                          <th>
          
                              Select
          
                          </th>
          
                          <th>
          
                              Role
          
                          </th>
          
                          <th>
          
                              Description
          
                          </th>
          
                      </tr>
          
            
          
                      @Html.EditorFor(model => model.Roles)
          
                  </table>
          
                  <br />
          
                  <hr />
          
            
          
                  <div class="form-group">
          
                      <div class="col-md-offset-2 col-md-10">
          
                          <input type="submit" value="Save" class="btn btn-default" />
          
                      </div>
          
                  </div>
          
              </div>
          
          }
          
            
          
          <div>
          
              @Html.ActionLink("Back to List", "Index")
          
          </div>

           

          Do We Really Want to Edit Roles?

          Here is where you may want to think things through a little bit. Consider - the main reason for utilizing Roles within an ASP.NET application is to manage authorization. We do this by hard-coding access permissions to controllers and/or specific methods through the [Authorize] attribute.

          If we make roles createable/editable/deletable, what can possibly go wrong? Well, a lot. First off, if we deploy our application, then add a role, we really have no way to make use of the new role with respect to the security concerns listed above, without re-compiling and re-deploying our application after adding the new role to whichever methods we hope to secure with it.

          The same is true if we change a role name, or worse, delete a role. In fact, an ambitious admin user could lock themselves out of the application altogether!

          However, there are some cases where we might want to have this functionality anyway. If, for example, you are the developer, and you add some new role permissions via [Authorize] to an existing application, it is more convenient to add the new roles to the database through the front-end than by either manually adding to the database, or re-seeding the application using Migrations. Also, if your application is in production, the re-running migrations really isn't an option anyway.

          In any case, a number of commentors on my previous post expressed the desire to be able to modify or remove roles, so let's plow ahead!

          Why Not? Adding the Roles Controller

          Once I had everything built out, and I went to use Visual Studio's built-in scaffolding to add a new Roles controller, I ran into some issues. Apparently, by extending IdentityRole the way we have, Entity Framework has some trouble scaffolding up a new controller based on the ApplicationRole model (EF detects some ambiguity between IdentityRole and ApplicationRole). I didn't spend too much time wrestling with this - it was easy enough to code yup a simple CRUD controller the old-fashioned way, so that's what I did.

          Here is my hand-rolled RolesController, ready for action:

          The Roles Controller:
          using AspNetExtendingIdentityRoles.Models;
          
          using System.Collections.Generic;
          
          using System.Data.Entity;
          
          using System.Linq;
          
          using System.Net;
          
          using System.Web.Mvc;
          
            
          
          namespace AspNetExtendingIdentityRoles.Controllers
          
          {
          
              public class RolesController : Controller
          
              {
          
                  private ApplicationDbContext _db = new ApplicationDbContext();
          
            
          
                  public ActionResult Index()
          
                  {
          
                      var rolesList = new List<RoleViewModel>();
          
                      foreach(var role in _db.Roles)
          
                      {
          
                          var roleModel = new RoleViewModel(role);
          
                          rolesList.Add(roleModel);
          
                      }
          
                      return View(rolesList);
          
                  }
          
            
          
            
          
                  [Authorize(Roles = "Admin")]
          
                  public ActionResult Create(string message = "")
          
                  {
          
                      ViewBag.Message = message;
          
                      return View();
          
                  }
          
            
          
            
          
                  [HttpPost]
          
                  [Authorize(Roles = "Admin")]
          
                  public ActionResult Create([Bind(Include = 
          
                      "RoleName,Description")]RoleViewModel model)
          
                  {
          
                      string message = "That role name has already been used";
          
                      if (ModelState.IsValid)
          
                      {
          
                          var role = new ApplicationRole(model.RoleName, model.Description);
          
                          var idManager = new IdentityManager();
          
            
          
                          if(idManager.RoleExists(model.RoleName))
          
                          {
          
                              return View(message);
          
                          }
          
                          else
          
                          {
          
                              idManager.CreateRole(model.RoleName, model.Description);
          
                              return RedirectToAction("Index", "Account");
          
                          }
          
                      }
          
                      return View();
          
                  }
          
            
          
            
          
                  [Authorize(Roles = "Admin")]
          
                  public ActionResult Edit(string id)
          
                  {
          
                      // It's actually the Role.Name tucked into the id param:
          
                      var role = _db.Roles.First(r => r.Name == id);
          
                      var roleModel = new EditRoleViewModel(role);
          
                      return View(roleModel);
          
                  }
          
            
          
            
          
                  [HttpPost]
          
                  [Authorize(Roles = "Admin")]
          
                  public ActionResult Edit([Bind(Include = 
          
                      "RoleName,OriginalRoleName,Description")] EditRoleViewModel model)
          
                  {
          
                      if (ModelState.IsValid)
          
                      {
          
                          var role = _db.Roles.First(r => r.Name == model.OriginalRoleName);
          
                          role.Name = model.RoleName;
          
                          role.Description = model.Description;
          
                          _db.Entry(role).State = EntityState.Modified;
          
                          _db.SaveChanges();
          
                          return RedirectToAction("Index");
          
                      }
          
                      return View(model);
          
                  }
          
            
          
            
          
                  [Authorize(Roles = "Admin")]
          
                  public ActionResult Delete(string id)
          
                  {
          
                      if (id == null)
          
                      {
          
                          return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
          
                      }
          
                      var role = _db.Roles.First(r => r.Name == id);
          
                      var model = new RoleViewModel(role);
          
                      if (role == null)
          
                      {
          
                          return HttpNotFound();
          
                      }
          
                      return View(model);
          
                  }
          
            
          
            
          
                  [Authorize(Roles = "Admin")]
          
                  [HttpPost, ActionName("Delete")]
          
                  public ActionResult DeleteConfirmed(string id)
          
                  {
          
                      var role = _db.Roles.First(r => r.Name == id);
          
                      var idManager = new IdentityManager();
          
                      idManager.DeleteRole(role.Id);
          
                      return RedirectToAction("Index");
          
                  }
          
              }
          
          }

           

          In the above, notice that when we go to delete a role, we make a call out to our IdentityManager class to a method named DeleteRole(). Why the complexity, John? Why not just delete the role from the datastore directly?

          There's a reason for that . . .

          About Deleting Roles

          Think about it. If you have one or more users assigned to a role, when you delete that role, you want to remove the users from the role first. Otherwise you will run into foreign key issues in your database which will not let you delete the role.

          So, we need to add a couple important methods to IdentityManager.

          Adding a Delete Role Method to Identity Manager

          Clearly, in order to delete roles, we first need to remove any users from that role first. Then we can delete the role. However, we have to employ a slight hack to do this, because the Identity framework does not actually implement a RemoveRole() method out of the box. Oh, it's there - you can find it if you look hard enough. The RoleStore<IRole> class actually defines a DeleteAsync method. However, it throws a "Not Implemented" exception.

          Here is how I worked around the issue. Add the following two methods to the IdentityManager class:

          Adding DeleteRole and RemoveFromRole Methods to Identity Manager Class:
          public void RemoveFromRole(string userId, string roleName)
          
          {
          
              _userManager.RemoveFromRole(userId, roleName);
          
          }
          
            
          
            
          
          public void DeleteRole(string roleId)
          
          {
          
              var roleUsers = _db.Users.Where(u => u.Roles.Any(r => r.RoleId == roleId));
          
              var role = _db.Roles.Find(roleId);
          
            
          
              foreach (var user in roleUsers)
          
              {
          
                  this.RemoveFromRole(user.Id, role.Name);
          
              }
          
              _db.Roles.Remove(role);
          
              _db.SaveChanges();
          
          }

           

          First, notice how we pass in a simple RoleId instead of an instance or ApplicationRole? This is because, in order for the Remove(role) method to operate properly, the role passed in as an argument must be from the same ApplicationDbContext instance, which would not be the case if we were to pass one in from our controller.

          Also notice, before calling _db.Roles.Remove(role) , we retrieve the collection of users who are role members, and remove them. This solves the foreign key relationship problem, and prevents orphan records in the AspNetUserRoles table in our database.

          ViewModels and Views for the Roles Controller

          Notice in our controller, we make use of two new View Models, the RoleViewModel, and the EditRoleViewModel. I went ahead and added these to the AccountViewModels.cs file. The code is as follows:

          The RoleViewModel and EditRoleViewModel Classes:
          public class RoleViewModel
          
          {
          
              public string RoleName { get; set; }
          
              public string Description { get; set; }
          
            
          
              public RoleViewModel() { }
          
              public RoleViewModel(ApplicationRole role)
          
              {
          
                  this.RoleName = role.Name;
          
                  this.Description = role.Description;
          
              }
          
          }
          
            
          
            
          
          public class EditRoleViewModel
          
          {
          
              public string OriginalRoleName { get; set; }
          
              public string RoleName { get; set; }
          
              public string Description { get; set; }
          
            
          
              public EditRoleViewModel() { }
          
              public EditRoleViewModel(ApplicationRole role)
          
              {
          
                  this.OriginalRoleName = role.Name;
          
                  this.RoleName = role.Name;
          
                  this.Description = role.Description;
          
              }
          
          }

           

          Views Used by the Role Controller

          The Views used by the RolesController were created by simply right-clicking on the associated Controller method and selecting "Add View." I'm including it here for completeness, but there is nothing revolutionary going on here. The code for each follows.

          The Index Role View

          Displays a list of the Roles, along with Action Links to Edit or Delete.

          Code for the Create Role View:
          @model IEnumerable<AspNetExtendingIdentityRoles.Models.RoleViewModel>
          
            
          
          @{
          
              ViewBag.Title = "Application Roles";
          
          }
          
            
          
          <h2>Application Roles</h2>
          
            
          
          <p>
          
              @Html.ActionLink("Create New", "Create")
          
          </p>
          
          <table class="table">
          
              <tr>
          
                  <th>
          
                      @Html.DisplayNameFor(model => model.RoleName)
          
                  </th>
          
                  <th>
          
                      @Html.DisplayNameFor(model => model.Description)
          
                  </th>
          
                  <th></th>
          
              </tr>
          
            
          
          @foreach (var item in Model) {
          
              <tr>
          
                  <td>
          
                      @Html.DisplayFor(modelItem => item.RoleName)
          
                  </td>
          
                  <td>
          
                      @Html.DisplayFor(modelItem => item.Description)
          
                  </td>
          
                  <td>
          
                      @Html.ActionLink("Edit", "Edit", new { id = item.RoleName }) |
          
                      @Html.ActionLink("Delete", "Delete", new { id = item.RoleName })
          
                  </td>
          
              </tr>
          
          }
          
            
          
          </table>

           

          The Create Role View

          Obviously, affords creation of new Roles.

          Code for the Create Role View:
          @model AspNetExtendingIdentityRoles.Models.RoleViewModel
          
            
          
          @{
          
              ViewBag.Title = "Create";
          
          }
          
            
          
          <h2>Create Role</h2>
          
            
          
            
          
          @using (Html.BeginForm()) 
          
          {
          
              @Html.AntiForgeryToken()
          
                
          
              <div class="form-horizontal">
          
                  <h4>RoleViewModel</h4>
          
                  <hr />
          
                  @Html.ValidationSummary(true)
          
            
          
                  @if(ViewBag.Message != "")
          
                  {
          
                      <p style="color: red">ViewBag.Message</p>
          
                  }
          
                  <div class="form-group">
          
                      @Html.LabelFor(model => model.RoleName, 
          
                      	new { @class = "control-label col-md-2" })
          
                      <div class="col-md-10">
          
                          @Html.EditorFor(model => model.RoleName)
          
                          @Html.ValidationMessageFor(model => model.RoleName)
          
                      </div>
          
                  </div>
          
            
          
                  <div class="form-group">
          
                      @Html.LabelFor(model => model.Description, 
          
                      	new { @class = "control-label col-md-2" })
          
                      <div class="col-md-10">
          
                          @Html.EditorFor(model => model.Description)
          
                          @Html.ValidationMessageFor(model => model.Description)
          
                      </div>
          
                  </div>
          
            
          
                  <div class="form-group">
          
                      <div class="col-md-offset-2 col-md-10">
          
                          <input type="submit" value="Create" class="btn btn-default" />
          
                      </div>
          
                  </div>
          
              </div>
          
          }
          
            
          
          <div>
          
              @Html.ActionLink("Back to List", "Index")
          
          </div>
          
            
          
          @section Scripts {
          
              @Scripts.Render("~/bundles/jqueryval")
          
          }

           

          The Edit Roles View

          For, um, editing Roles . ..

          Code for the Edit Roles View:
          @model AspNetExtendingIdentityRoles.Models.EditRoleViewModel
          
            
          
          @{
          
              ViewBag.Title = "Edit";
          
          }
          
            
          
          <h2>Edit</h2>
          
            
          
            
          
          @using (Html.BeginForm())
          
          {
          
              @Html.AntiForgeryToken()
          
                
          
              <div class="form-horizontal">
          
                  <h4>EditRoleViewModel</h4>
          
                  <hr />
          
                  @Html.ValidationSummary(true)
          
            
          
                  @*Hide the original name away for later:*@
          
                  @Html.HiddenFor(model => model.OriginalRoleName)
          
            
          
                  <div class="form-group">
          
                      @Html.LabelFor(model => model.RoleName, 
          
                      	new { @class = "control-label col-md-2" })
          
                      <div class="col-md-10">
          
                          @Html.EditorFor(model => model.RoleName)
          
                          @Html.ValidationMessageFor(model => model.RoleName)
          
                      </div>
          
                  </div>
          
            
          
                  <div class="form-group">
          
                      @Html.LabelFor(model => model.Description, 
          
                      	new { @class = "control-label col-md-2" })
          
                      <div class="col-md-10">
          
                          @Html.EditorFor(model => model.Description)
          
                          @Html.ValidationMessageFor(model => model.Description)
          
                      </div>
          
                  </div>
          
            
          
                  <div class="form-group">
          
                      <div class="col-md-offset-2 col-md-10">
          
                          <input type="submit" value="Save" class="btn btn-default" />
          
                      </div>
          
                  </div>
          
              </div>
          
          }
          
            
          
          <div>
          
              @Html.ActionLink("Back to List", "Index")
          
          </div>
          
            
          
          @section Scripts {
          
              @Scripts.Render("~/bundles/jqueryval")
          
          }
          

           

          The Delete Roles View

          Code for the Delete Roles View:
          @model AspNetExtendingIdentityRoles.Models.RoleViewModel
          
            
          
          @{
          
              ViewBag.Title = "Delete";
          
          }
          
            
          
          <h2>Delete</h2>
          
            
          
          <h3>Are you sure you want to delete this?</h3>
          
          <div>
          
              <h4>RolesViewModel</h4>
          
              <hr />
          
              <dl class="dl-horizontal">
          
                  <dt>
          
                      @Html.DisplayNameFor(model => model.RoleName)
          
                  </dt>
          
            
          
                  <dd>
          
                      @Html.DisplayFor(model => model.RoleName)
          
                  </dd>
          
            
          
                  <dt>
          
                      @Html.DisplayNameFor(model => model.Description)
          
                  </dt>
          
            
          
                  <dd>
          
                      @Html.DisplayFor(model => model.Description)
          
                  </dd>
          
            
          
              </dl>
          
            
          
              @using (Html.BeginForm()) {
          
                  @Html.AntiForgeryToken()
          
            
          
                  <div class="form-actions no-color">
          
                      <input type="submit" value="Delete" class="btn btn-default" /> |
          
                      @Html.ActionLink("Back to List", "Index")
          
                  </div>
          
              }
          
          </div>

           

          Modify _Layout.cshtml to Add Users and Roles Links

          I went ahead and modified the _Layout.cshtml file, changed what was the "Admin" link to simply "Users," and added a new "Roles" link as follows:

          Modify _Layout.cshtml:
          <div class="navbar-collapse collapse">
          
              <ul class="nav navbar-nav">
          
                  <li>@Html.ActionLink("Home", "Index", "Home")</li>
          
                  <li>@Html.ActionLink("About", "About", "Home")</li>
          
                  <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
          
                  <li>@Html.ActionLink("Users", "Index", "Account")</li>
          
                  <li>@Html.ActionLink("Roles", "Index", "Roles")</li>
          
              </ul>
          
              @Html.Partial("_LoginPartial")
          
          </div>

           

          Run Migrations and Build out the Database

          Ok, you should now be able to add a new migration, run it, and build out the database. If you are new to EF Migrations, you may want to review Configuring Entity Framework Migrations. In this case, the cloned project already has migrations enabled, so all we need to do is Build, then type into the Package Manager Console:

          Add New Migration (Delete the previous Migration file, or choose a new name)
          Add-Migration init

           

          Then, if all went well with that,

          Update The Database:
          Update-Database

           

          Running the Application

          If all went well (and I haven't missed anything in this post!) we should be able to run our application, log in, and navigate to the "Users" tab. Then, select the "Roles" link of the single user listed. You should see something similar to this:

          The User Roles View:user-roles-view

           

          We can see, our new property is evident.

          Some Thoughts in Closing

          This was a rather long article to implement some relatively minor functionality. However, we touched on some concepts which may prove helpful if you need just a little more from the new Identity system than is available straight out of the box. Also, I am setting the stage for the next article, where I will look at setting up "Role Groups" to which users may be assigned. In this scenario, we can use the built-in Roles to set pretty granular access permissions within our code, and then assign users to predefined "Groups" of such Role permissions, somewhat mimicking familiar domain permissions.

          Pay Attention When Messing with Auth and Security!

          I do my best when working with membership or Identity to stay within the bounds and mechanisms set up by the ASP.NET team. I am far, far from a security expert (I DID buy a book on it recently, though!), and those people know way more about creating a secure authorization system than I could ever hope to.

          In the preceding article on extending IdentityUser and adding roles to our application, we stayed well within the bounds of the intended uses of Identity. In this article, I ventured a little further afield, extending a class which it appears the ASP.NET team did not intend to be easily extended. Further, I implemented a means to create, edit, and delete roles at the application user level. I assume there are reasons such functionality was not built-in out of the box. Most likely for the reasons I discussed previously.

          That said, near as I can tell we have done nothing here to explicitly compromise the security of our application, or the integrity of the identity system. Our ApplicationRole class, through inheritance and polymorphism, is consumed properly by the internals of the ASP.NET Identity system, while delivering whatever additional properties we required.

          Got Any Thoughts? See Some Improvements?

          If you see where I have something wrong, or missed something while moving the code into this article, please do let me know in the comments, or shoot me an email at the address in the "About the Author" sidebar. If you have suggestions for improving on what you see here, please let me know, or submit a Pull Request on Github. I would love to incorporate any good ideas.

          Watch for the next article on implementing groups and permissions!

          Additional Resources and Items of Interest

           

          Posted on February 13 2014 09:17 PM by jatten     

          Comments (8)

          About the author

          My name is John Atten, and my "handle" on many of my online accounts is xivSolutions. I am Fascinated by all things technology and software development. I work mostly with C#, JavaScript/Node, and databases of many flavors. Actively learning always. I dig web development. I am always looking for new information, and value your feedback (especially where I got something wrong!). You can email me at:

          jatten at typecastexception dot com

          Web Hosting by