C#: Using Reflection and Custom Attributes to Map Object Properties

Posted on March 10 2014 05:40 PM by jatten in C#, CodeProject   ||   Comments (0)

map-to-the-cyan-studio-500I have a general distaste for decorating my code with Attributes and Annotations. Most of the time, I can't help but feel like there must be a better way to accomplish what I am trying to do, and/or that I have somewhere sprung a leak in what should be a helpful abstraction.

Other times, though, custom attributes can be just the tool for the job, and sometimes, the only practical way to solve a problem.

An easy to understand use case for Custom Attributes might be the mapping of object properties to database fields in a data access layer. You have no doubt seen this before when using Entity Framework. In EF, we often utilize System.ComponentModel.DataAnnotations to decorate the properties of our data objects.

Image by Elizabeth Briel | Some Rights Reserved

Here, we're going to take a quick look at creating our own custom attributes.

Use Custom Attributes to Give Hints or Property Metadata

Yes, EF and the System.ComponentModel.DataAnnotations namespace provide a ready-made means to do this, but for one, you may find yourself building your own data access layer or tool, and for another, this is an easy-to-understand example case.

Let's see how we might implement our own version of these data annotations as Custom Attributes. To create a Custom Attribute in C#, we simply create a class which inherits from System.Attribute. For example, if we wanted to implement our own [PrimaryKey] Attribute to indicate that a particular property on a class in our application represents the Primary Key in our database, we might create the following Custom Attribute:

public class PrimaryKeyAttribute : Attribute { }

 

Now, consider a class in our application, the Client class. Client has a ClientId property which corresponds to the Primary Key in the Clients database table:

public class Client 
{
    public int ClientId { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public string Email { get; set; }
}

 

We could simply decorate the ClientId property with our new attribute, and then access this from code as in the simple example following:

Decorate the ClientId property with the Custom Primary Key Attribute:
public class Client
{
    [PrimaryKey]
    public int ClientId { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public string Email { get; set; }
}

 

Use Reflection to Examine the Properties and Attributes of an Object

Then, we can cook up a simple console app demo to see how this works. First, we'll take the long way around, iterating using a foreach structure so we can see more clearly what's going on. Then we will look at a more concise (and efficient) LINQ-based implementation.

Silly Example of Accessing a Custom Property:
static void WritePK<T>(T item) where T : new()
{
    // Just grabbing this to get hold of the type name:
    var type = item.GetType();
  
    // Get the PropertyInfo object:
    var properties = type.GetProperties();
    Console.WriteLine("Finding PK for {0}", type.Name);
    foreach(var property in properties)
    {
        var attributes = property.GetCustomAttributes(false);
        foreach(var attribute in attributes)
        {
            if(attribute.GetType() == typeof(PrimaryKeyAttribute))
            {
            string msg = "The Primary Key for the {0} class is the {1} property";
            Console.WriteLine(msg, type.Name, property.Name);
            }
        }
    }
}

 

In the code above, we pass in a Generic object of type T (meaning, this method could be used with ANY domain object to check for the presence of a [PrimaryKey] attribute). We first use the GetType() method to find the object's Type information, and then we call the GetProperties() method of the Type instance, which returns an array of PropertyInfo objects.

Next, we iterate over each of the PropertyInfo instances, and call the GetCustomAttributes() method, which will return an array of objects representing the CustomAttributes found on that property. We can then check the type of each CustomAttribute object, and if it is of type PrimaryKeyAttribute, we know we have found a property that represents a primary key in our database.

LINQ Makes it Mo' Bettah

We could re-write the code above, using LINQ, for a more compact and efficient method as follows:

The WritePk Method, Re-Written Using LINQ:
static void WritePK<T>(T item) where T : new()
{
    var type = item.GetType();
    var properties = type.GetProperties();
    Console.WriteLine("Finding PK for {0}", type.Name);
    // This replaces all the iteration above:
    var property = properties
        .FirstOrDefault(p => p.GetCustomAttributes(false)
            .Any(a => a.GetType() == typeof(PrimaryKeyAttribute)));
    if (property != null)
    {
        string msg = "The Primary Key for the {0} class is the {1} property";
        Console.WriteLine(msg, type.Name, property.Name);
    }
}

 

This example is fairly simplistic, but illustrates well how we can access CustomAttributes to useful end. 

Another case, which I ran into recently is mapping properties to database columns. In creating a general-purpose data access tool, you never know how database columns are going to align with the properties on your domain objects. In my case, we needed to dynamically build some SQL, using reflection to grab object properties, and map to the database. However, there is no guarantee that the database column names will match the property names on the domain object.

In cases where column names differ from object properties in such a situation, Custom Attributes are one means of dealing with the situation (this is the part where the abstraction layer of the data access tool gets violated by the Db rearing its head into the business object domain . . .).

Use Custom Attributes to Map Properties to Database Columns

The previous example simply used a Custom Attribute simply as sort of a tag on a property. Attributes can also convey information if needed. Let's consider a means to map the property to a specific database column name.

Once again, we create a class which inherits from System.Attribute, but this time we will add a property and a constructor:

The Custom DbColumn Attribute:
public class DbColumnAttribute : Attribute
{
    string Name { get; private set; }
    public DbColumnAttribute(string name)
    {
        this.Name = name;
    }
}

 

Now, let's pretend you inherit a database which you need to integrate with your existing code base. The table from which the client information will be sourced uses all lower-case column names, with underscores between segments instead of proper or camel casing:

SQL For a Table With Column Names Which Do Not Match Class Properties:
CREATE TABLE Clients (
    client_id int IDENTITY(1,1) PRIMARY KEY NOT NULL,
    last_name varchar(50) NOT NULL,
    first_name varchar(50) NOT NULL,
    email varchar(50) NOT NULL
);

 

Now you can do this:

The Client Class with Column Name Attributes:
public class Client
{
    [PrimaryKey]
    [DbColumn("client_id")]
    public int ClientId { get; set; }
  
    [DbColumn("last_name")]
    public string LastName { get; set; }
  
    [DbColumn("first_name")]
    public string FirstName { get; set; }
  
    [DbColumn("email")]
    public string Email { get; set; }
}

 

You can access these attributes, and their properties, from code like so:

Reading Custom Attribute Properties from Code:
static void WriteColumnMappings<T>(T item) where T : new()
{
    // Just grabbing this to get hold of the type name:
    var type = item.GetType();
  
    // Get the PropertyInfo object:
    var properties = item.GetType().GetProperties();
    Console.WriteLine("Finding properties for {0} ...", type.Name);
    foreach(var property in properties)
    {
        var attributes = property.GetCustomAttributes(false);
        string msg = "the {0} property maps to the {1} database column";
        var columnMapping = attributes
            .FirstOrDefault(a => a.GetType() == typeof(DbColumnAttribute));
        if(columnMapping != null)
        {
            var mapsto = columnMapping as DbColumnAttribute;
            Console.WriteLine(msg, property.Name, mapsto.Name);
        }
    }
}

 

"But John," you say, "Entity Framework already does this!"

Precisely. But now you know how it works. Believe me, you may not always have EF at your disposal. Also, you WILL run into databases "in the wild" where column naming conventions do not align with C# Class and property naming conventions (Work with a Postgresql database for five minutes, and get back to me).

Cache Results from Calls Using Reflection Where Appropriate

A quick note, which is only marginally applicable to the examples above, but important in the design of a real-world application. Calls to using reflection can be expensive. Overall, machines these days are fast, and generally, the odd call to GetType() and GetCustomAttributes() are not all that significant. Except when they are.

For example, if the above code were used in a larger application context repeatedly, it might be better to walk through the object properties at object initialization (or even at application load) and map the object properties for each to its respective column name and stash them all in a Dictionary<string, string>. Then, anywhere in your code where the mapping is needed, you can access the primary key name for a specific object by use of the property as a key.

How you do this and where would depend heavily on what you are doing, and the larger structure of your application. For an example of what I am talking about, check out the Biggy project, where I recently had to do this very thing.

Custom Attributes Can Be an Architectural Trade-Off

I was working on an open-source project recently, and the project maintainer wisely pointed out that column-mapping attributes such as the above are "the database pushing right on up through the abstraction." Which is true. In the case of mapping database columns to object properties, we are attempting to solve but one aspect of the age-old impedance mismatch problem faced by all Object-Relational Mapping (ORM) frameworks. It ain't always elegant, but sometimes, it is the only way.

None of this is to say that Custom Attributes are only useful in the context of mapping database columns. There are any number of potential use-cases. While I personally dislike cluttering up my code with attributes and annotations, there will be times when it is the best way to solve a problem.

The next time you find yourself wishing you could know something extra about a particular property or method, Custom Attributes are one more tool in your chest.

Additional Resources and Items of Interest

 

Posted on March 10 2014 05:40 PM by jatten     

Comments (0)

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)

About the author

My name is John Atten, and my username on many of my online accounts is xivSolutions. I am Fascinated by all things technology and software development. I work mostly with C#, Java, SQL Server 2012, learning ASP.NET MVC, html 5/CSS/Javascript. I am always looking for new information, and value your feedback (especially where I got something wrong!). You can email me at:

jatten@typecastexception.com

Web Hosting by