Custom Development

Security Using MVC Activity-based Authorization

Aref Al-kamel

I worked on a midsize project that had a requirement to implement a security for windows and form authentication. The requirement also stated that I could not use the .NET membership provider since there was a WCF service that provided users and roles to the application. Looking into the problem from a higher level, I wanted to either implement a role-based security model or an operation-based security model. Here's a brief overview of the pros and cons of each option:

Role-based Security Model

Role-based security models create a user and role and also link said users to roles... and that is it. This is very straightforward and fits perfectly for some applications. The problem with this approach is the limitation of extendibility since it is very hard to maintain as the application grows and roles are added overtime.

Operation-based Security Model

Operation-based security models creates users, roles and operations (activities) that enable a user to be linked to one or more roles -- and roles to be linked to one or more operations. I find this model suitable for many applications -- small or large -- and a variety of scenarios. Since this is my model of choice, I'll explain it in the rest of this post.

A Step-by-Step Process to Achieve Activity-based Authorization

The requirement has to create a security model that allows users to conduct some, but not all of, the admin functions.

This can be achieved by creating a separate controller for the admin functions and assign different roles for each one. However, this solution is basically a hack because if the business changes since a developer will need to create a mini admin controller any time there is a change in security requirements. In fact, this is a bad strategy since it would be a nightmare to maintain and expand the application. An ideal solution for this problem is to create an activity-role security model. So a developer will need to create an admin controller that can access either the admin activities and/or someone who has a permission to conduct some function in it. To accomplish that, we will need to do the following:

1. Create a user object that encapsulates all roles and activities that look like the one below. You can customize it to fit your business needs

public class User
{
public string Name { get; set; }
public string AuthenticationType { get; set; }
public string EmailAddress { get; set; }
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
public List<string> Roles { get; set; }
public List<string> Activities { get; set; }

}

This class can be expanded to hold a collection for Role and Activity objects but, for the sake of simplicity, I am using a list of roles and activities. Let’s assume we have a repository that will give us a user object as it shows in this example:

public User GetUser(string p)
{
return new User()
{
Activities = new List<string>("Submit".Split(',').Select(s => s)),
AuthenticationType = "Windows",
Name = "Test",
Roles = new List<string>("User,Submitter".Split(',').Select(s => s)),
IsAuthenticated = true
};
}

2. We have the user object and need to pass the Identity object for the user principal. So we will need to create a custom Identity that implements IIIdentity and a custom Principal that implements IPrincipal.

public class CustomIdentity : IIdentity
{
Security.User _user;
public CustomIdentity(Security.User User)
{
this._user = User;
}
public string AuthenticationType
{
get { return _user.AuthenticationType; }
}
public bool IsAuthenticated
{
get { return this._user.IsAuthenticated; }
}
public string Name
{
get { return this._user.Name; }
}
public List<string> Roles
{
get { return this._user.Roles; }
}
public List<string> Activities
{
get { return this._user.Activities; }
}
}

public class CustomPrincipal : IPrincipal
{

private CustomIdentity _identity;
public CustomPrincipal(CustomIdentity identity)
{
this._identity = identity;
}
public IIdentity Identity
{
get { return _identity; }
}
public bool IsInRole(string role)
{
// convert comma delimited list to an array
List<string> givenRole = new List<string>();
givenRole = new List<string>(role.Split(',').Select(s=>s));
foreach (var r in _identity.Roles)
{
foreach (var ir in givenRole)
{
if (r.ToUpper() == ir.ToUpper())
{
return true;
}
}
}
return false;
}
public bool IsInActivity(string activity)
{
// convert comma delimited list to an array
List<string> givenActivity = new List<string>();
givenActivity = new List<string>(activity.Split(',').Select(s => s));
foreach (var r in _identity.Activities)
{
foreach (var ia in givenActivity)
{
if (r.ToUpper() == ia.ToUpper())
{
return true;
}
}
}
return false;
}

public bool hasRoleActivity (string role, string activity)
{
bool hasRole = false;
bool hasActivity = false;
foreach (var r in _identity.Roles)
{
if (r.ToUpper() == role.ToUpper())
{
hasRole = true;
}
}
foreach (var a in _identity.Activities)
{
if (a.ToUpper() == activity.ToUpper())
{
hasActivity= true;
}
}
return hasActivity && hasRole;
}
}

Notice the CustomPrincipal object provides a way to check if the user has roles or activities or both. This behavior will make our lives a lot easier when we have a situation where a user has to be in a role with a certain type of permission to conduct an action.

3. Now it is time to create a custom Authorize attribute and tag all of the controllers in the application with it so they're restricted.
public class CustomAuthorize : AuthorizeAttribute
{
public string Activities { get; set; }
public CustomAuthorize()
{ }
public CustomAuthorize(string Role, string Activity)
{
this.Activities = Activity;
this.Roles = Role;
}
protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
{

var principle = httpContext.User as CustomPrincipal;
if (principle == null || principle.Identity == null || !principle.Identity.IsAuthenticated)
return false;
if (!string.IsNullOrEmpty(Roles) && string.IsNullOrEmpty(Activities))
{
return principle.IsInRole(Roles);
}
if (string.IsNullOrEmpty(Roles) && !string.IsNullOrEmpty(Activities))
{
return principle.IsInActivity(Activities);
}
if (!string.IsNullOrEmpty(Roles) && !string.IsNullOrEmpty(Activities))
{
return principle.hasRoleActivity(Roles, Activities);
}
return false;
}
}

This custom authorize attribute we just created can be used to pass it either roles or activities or both as we will see in the example below.

4. We need to populate the principal object during the application HTTP request and keep the principle object for the duration of the session to boost the performance of the application. I have used the PostAcquireRequestState to implement that in the Global.asax as it appears below:
protected void Application_PostAcquireRequestState(Object sender, EventArgs e)
{
CustomPrincipal principal;
try
{
if (HttpContext.Current.Session != null)
{
if (Session["currentUser"] != null)
{
principal = Session["currentUser"] as CustomPrincipal;
HttpContext.Current.User = principal;
}
else
{
User user = GetUser(HttpContext.Current.User.Identity.Name);
CustomIdentity identity = new CustomIdentity(user);
principal = new CustomPrincipal(identity);
HttpContext.Current.User = principal;
Session["currentUser"] = principal;
}
}
}
catch { }
}

5. We have everything ready for our application and we just need to wire the controllers/Actions with whatever role/activity needed. I have created three controllers and tagged them with the custom authorize attribute as follows:

[Security.CustomAuthorize(Roles="Admin")]
public class AdminController : Controller
{}
[CustomAuthorize(Roles="User")]
public class HomeController : Controller
{}

[Security.CustomAuthorize(Activities="Submit")]
public class POController : Controller
{}
Now run the application customized for your permission as needed. In the next post I will use enumeration for the roles and activity instead of a list of string for easy maintenance. I will also provide the source code later.
Have you managed security in enterprise applications with MVC-Activity-based Authorization? What did you think?
Happy coding!

 

Aref Al-kamel
ABOUT THE AUTHOR

Senior Technical Consultant