Sunday, June 5, 2016

ASP.NET MVC Fine Grained Identity & Access Control - Part 3

In this series I will explain how I designed a generic claims based access control system ASP.NET MVC 5 with Identity 2.0.

In Part 1, I describe my approach and the initial database design. ASP.NET MVC Fine Grained Identity & Access Control - Part 1

In Part 2, I started laying foundation for the whole system with all the data models and a functional UI.

In this Part 3, I will start with most of the coding to make it work.

So far completed coding is in the GitHub repo: https://github.com/dirnthelord/ClaimsAuth

Note that, in part 2, I've mentioned the functionalities I want the system to have. In this post, I will complete all the core functionality so that I can directly edit the DB table values (using SSMS - SQL Server Management Studio) to test things out. I still need to create a UI for the super admins to easily manage permissions.

First off, lets grab all the Areas, Controllers and Actions in the system. These are what you going to lock down to certain groups.

I have posted the code for doing this a while back. This is it.

When you call 'ResourceHelper.GetResources()' it will give you a list of all the areas, controllers, actions for MVC/WebAPI in the system. Then you have to save these in the db. I choose to do this task in background in the Global.asax Application-Start method.

I will list down the tasks I need to do to make this work.
  1. Store all the Areas, Controllers and Actions in the DB
    • If the route exists, I don't need to insert it again. So to do this, first, I retrieve all the app-resources compare against the newly extracted, save only those which are new. This way, I don't have to manually enter anything every time a new feature gets added to the system. As long as I follow a clean Area-Controller-Action separation security is dynamic and adaptable.
  2. Retrieve the group permissions from the db
  3. Put them all in a dictionary for fast access.
  4. Once a user logs in, we check their group(s) and setup claims for each route. 
  5. For admins, we use special claims so we can reduce the number of claims
  6. For other users, we have to specify the claims they have.  
First, I'm gonna use my ResourceHelper class to get all area-controller-actions. and gonna save them in the db.

To do that, I have created a class called APM (Application Permission Manager) and have "SaveResources" method call 'ResourceHelper.GetResources()' and save them in db in a background task.

    /// <summary>
    /// Application Permission Manager. 
    /// Calling APM for short
    /// </summary>
    public class APM
    {
        //... rest of the code (check source)
        public static void SaveResources()
        {
            TaskHelper.RunBg(() =>
            {
                var resources = ResourceHelper.GetResources();
                Trace.WriteLine($"Saving {resources.Count} Resources");
                var repo = new ResourceRepository();
                repo.Save(resources);

                Initialize();
            });
        }

Following code shows my logic to save only the new ones, along with any newly created anonymous resources.

public class ResourceRepository
{
 //...other code (check source)

 public int Save(List<ApplicationResource> resources)
 {
  // get all existing/saved resources
  var existing = GetResources().ToList();
  // select only the ones which are new
  var itemsToInsert = resources.Where(item => !existing.Contains(item)).ToList();

  var result = 0;

  // go through each new resource and save them, one by one(for now.)
  foreach (var item in itemsToInsert)
  {
   try
   {
    SaveResource(item);
    result++;
   }
   catch (Exception ex)
   {
    Trace.TraceError("Unable to save resource: " + item.ToPermissionValue(), ex);
   }
  }

  // get all existing/saved global permissions
  var existingGlobal = GetResourceGlobalPermissions().ToList();
  // find the resources which can be accessed anonymously
  var anon = resources.Where(x => x.IsAnonymous).ToList();
  // get all the newly saved anonymous resources
  // because we have to add a record of them to global permissions table.
  var savedAnon = GetResources().Where(anon.Contains).ToList();

  foreach (var anonItem in savedAnon)
  {
   try
   {
    // save only the new ones. new ones doesn't exist in the existing globals list. (yet)
    if (existingGlobal.All(g => g.ResourceID != anonItem.ID))
    {
     SaveResourceGlobalPermission(new ResourceGlobalPermission(anonItem.ID));
     result++;
    }
   }
   catch (Exception ex)
   {
    Trace.TraceError("Unable to save resource global permission: " + anonItem, ex);
   }
  }

  return result;
 }

 //... more code (check source)

}

Now that we have all the resources saved, what's left is retrieve them, cache them and use them to make sure no one gets access to anything. (Everything should be locked by default)

I have updated the connection string to point to a SQL Server database. Should auto-generate the db when you first run the application, and save all the available resources. I count 56 total resources.

I'll update the code and write another post on the next steps soon...

If you have any questions, feel free to drop me a message, anywhere you like.