ASP.Net

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


Image by Kool | Some Rights Reserved

This 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.

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

ASP.Net
ASP.NET Identity 2.0: Customizing Users and Roles
ASP.Net
ASP.NET Web API: Understanding OWIN/Katana Authentication/Authorization Part III: Adding Identity
C#
C#: Using Reflection and Custom Attributes to Map Object Properties
  • Anil

    AnilAnil

    Author Reply

    Role are not dynamic in this frame work. If we got to add a new role at the run time then we would need to modify the code. rohttps://lostechies.com/derickbailey/2011/05/24/dont-do-role-based-authorization-checks-do-activity-based-checks/


  • Musthaan

    MusthaanMusthaan

    Author Reply

    Nice Article.. Need to Implement this in my project.


  • John

    JohnJohn

    Author Reply

    Hi Michael –

    RE: "The most valuable part was where you altered the ApplicationDBContext."

    Indeed, that is where a tool like Reflector or Telerik's JustDecompile come is SOOOO handy. I took a look at what was happening under the hood with Identity framework, and from there was able to suss out what I might get away with.

    Nice solutions/hack for Paul's issue, too!

    Cheers! :-)


  • Michael

    MichaelMichael

    Author Reply

    Hi John, I found this series extremely helpful and applicable to my current design goals. The most valuable part was where you altered the ApplicationDBContext. I could not have done this on my own and I'm lucky my goals were very similar to what you've set out to demonstrate.

    To Answer Paul's question – I added an "IsEmpty" property to the viewmodel of my roles (and permissions) so that if it was in use, the delete link would be disabled by the razor. e.g.
    @if (item.Empty)
    {
    @Html.ActionLink("Delete", "DeleteRole", new { id = item.Id })
    }
    else
    {
    <span title="A user is mapped to this role">In use</span>
    }

    Also, to answer Leo's question, I altered the _layout.html to hide the Users/Roles/Permissions links from the menu if you weren't an admin, again using razor as such:
    @if ( User.Identity.IsAuthenticated )
    {
    if (User.IsInRole("Admin"))
    {
    <li>@Html.ActionLink("Users", "Users", "Account")</li>
    <li>@Html.ActionLink("Roles", "Index", "Roles")</li>
    <li>@Html.ActionLink("Permissions", "Index", "Permissions")</li>
    }
    }

    I hope this helps.
    -Mick.


  • John

    JohnJohn

    Author Reply

    Good morning –

    I am most certain what you are seeking can be done, I just haven;t had a moment to go digging for it. I believe it is not that difficult either.

    Bear in mind, the Identity system is not actually designed to be a granular permissions management system such as I am forcing it into in this article. It was interesting to me to see if it coujld be done, and to maybe cover middle-of-the-road cases where some granularity is needed, but no so much that a different methodology entirely is indicated.

    Identity is primarily designed for managing application access for a public-facing website. That said, given a part of an afternoon, I feel pretty certain what you want to do can be achieved. I'll look into it sometime this week, and try to update you (or write another post!).

    In the meantime, if you make any progress on your own, I would love to hear about it, either here, or drop me an email (look in the "about the author box for the address).

    Thanks for reading, and taking the time to comment. :-)


  • Paul Hermans

    Interesting article, but i am struggling with the [Authorize] stuff in my code.
    I don't see how it applies to doing anything of use to me.

    If i want to Display, or not display a "Delete" button based on if the user has or does not have the appropriate permission, this will not be done with a hard coded [authorize] tag on an entire mehod……i simply want to write code like:

    If ( usesHasPermission( XXX ) )
    deletebutton.visible=true

    I like the grouping of permissions (or as MS calls them Roles) into groups, that is one piece of what i need to do. I haven't read all your code yet in detail but it seems like MS has made this simple enough thing ridiculously hard. Is there a simpler way?


  • LeoR

    LeoRLeoR

    Author Reply

    Hi John,

    How do you show or hide the menu items based on the Roles or permissions?

    Can you have a menu that different Roles and see?

    Thanks John!