Strongly Typed Roles in MVC with Authorize Attribute

Recently I was working on a project that had a large amount of roles that were going to be utilized on many different controllers and even on individual controller actions. Originally it was given to me utilizing the standard out-of-the-box way of Authorizing with MVC 1.0:

//MVC’s standard authorize attribute
[Authorize(Roles=”Administrator, User”)]
public class HomeController : BaseController
{
	...
}

I was told that role changes may occur in the future. This caused fear of the amount of work that would be required to add or edit roles if there are currently 20 to 30 roles and that each individual controller and sometimes individual actions were using the authorize attribute. Not to mention how easy it would be to miss one of the Roles strings in one of the authorize calls.  This lead me to want to find a way to make the roles strongly typed. The first attempt I tried (proposed to me by Ryan Ohs) was to set a static class with a method to join strings back into the required format:

public static class ProjectRoles
{
	public static string List(params string[] roles) 
	{ 
      		return string.Join(", ", roles); 
	}
	public static string Admin = “Admin”;
	public static string Support = “Support”;
	public static string User = “User”;
	//and all roles continue
}

With that class the call to MVC’s authorize would be:

[Authorize(Roles=ProjectRoles.List(ProjectRoles.Admin, ProjectRoles.Support))] 
public class HomeController : BaseController 
{ 
}

A great looking solution that allowed me to change roles without having to go and change every single controller. Unfortunately, since attribute values must be defined at compile time and not at run time this solution wouldn’t work. So while continuing to try and find a method to strongly type the roles I came across a solution to support multiple roles with enums. This solution was a bit unsightly too me since it required a bitwise OR anytime you wanted to use multiple roles. Finally, I decided to change the ProjectRoles into a series of constants to represent the individual roles and to create a custom authorize attribute. I wanted to accomplish two goals with the custom authorize attribute: first I wanted to redirect a failed authorization to somewhere other than the logon page, and second I wanted the authorize attribute to allow me to use my ProjectRole constants. To accomplish this I had to override two of MVC’s authorize attribute methods, OnAuthorization and AuthorizeCore:

public static class ProjectRoles 
{
	//now constants for the attribute values
	public const string Admin = “Admin”; 
	public const string Support = “Support”; 
	public const string User = “User”; 
	public const string Guest = “Guest”;
	//and roles continue
}

public class CustomAuthorize : AuthorizeAttribute
{
	//Property to allow array instead of single string.
	private string[] _authorizedRoles;
	public string[] AuthorizedRoles
	{
		get { return _authorizedRoles ?? new string[0]; }
		set { _authorizedRoles = value; } 
	}
	public override void OnAuthorization(AuthorizationContext filterContext)
	{
		base.OnAuthorization(filterContext); 
		
		//If its an unauthorized/timed out ajax request go to top window and redirect to logon.
		if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
                	filterContext.Result = new JavaScriptResult() { Script = top.location = '/Account/LogOn?Expired=1'; };

		//If authorization results in HttpUnauthorizedResult, redirect to error page instead of Logon page.
		if(filterContext.Result is HttpUnauthorizedResult)
			filterContext.Result = new RedirectResult("~/Error/Authorization");
	}
	protected override bool AuthorizeCore(HttpContextBase httpContext)
	{
		if (httpContext == null)
			throw new ArgumentNullException("httpContext");

		if (!httpContext.User.Identity.IsAuthenticated)
			return false;

		//Bypass role check if user is Admin, prevents having to add Admin role across the whole project.
		if(httpContext.User.IsInRole("Admin"))
			return true;

		//If no roles are supplied to the attribute just check that the user is logged in.
		if(AuthorizedRoles.Length==0)
			return true;

		//Check to see if any of the authorized roles fits into any assigned roles only if roles have been supplied.
		if (AuthorizedRoles.Any(httpContext.User.IsInRole))
			return true;

		return false;
	}
}

Side Note: I do know you can utilize OnAuthorizedFailed() in MVC 2.0, but unfortunately this project was built using MVC 1.0 and not the most recent version.

This solution had an additional benefit besides my two original goals in that any user in the Admin role will always be authorized without having to check for it. The main accomplishment was being able to utilize the string constants in ProjectRoles:

[CustomAuthorize(Roles= new []{ProjectRoles.Support, ProjectRoles.User})]
public class HomeController : BaseController
{
}

This allows any admin, support, or user access to the HomeController, and although I really would rather not have to declare the new array, it seems I will have to live with it due to the restrictions on attributes in C#.

The other issue that came up with utilizing strongly typed roles in MVC is using them in custom controls and/or restricting partial views, such as only allowing certain menu items to populate based upon the user’s role. Not wanting my code to look something like this:

<% var acceptedRoles = new string[] {ProjectRoles.Admin, ProjectRoles.User};
if(acceptedRoles.Any(HttpContext.Current.User.IsInRole)) { %>
<%=Html.ActionLink(“Home”, “Home”, “Home”)%>
<%}%>

I decided to create an extension for the IPrincipal class to be able to check if the current user was in any of the roles listed from ProjectRoles. This made the code for the menu become much more readable:

public static class IPrincipalExtend
{
	public static bool HasAnyRole(this IPrincipal user, params string[] roles)
	{
		return roles.Any(user.IsInRole);
	}
}

 

<% var currentUser = HttpContext.Current.User; %>
<% if (currentUser.HasAnyRole(ProjectRoles.Admin,
		              ProjectRoles.Support, 
			      ProjectRoles.User))
{%>
	<%=Html.ActionLink("Home", "Home", "Home")%>
<%}%>
<% if (currentUser.HasAnyRole(ProjectRoles.Admin,
			      ProjectRoles.Support, 
			      ProjectRoles.Guest
			      ProjectRoles.User))
{%>
	<%= Html.ActionLink("GuestHome", "GuestHome", "Home") %>
<% }%>

And although this will be great when I want to restrict viewing a piece of the View every now and then. I did realize that primarily I will be wanting to restrict access to ActionLinks. And not too mention if I hid them completely that some of the eixsting tables and displays might become skewed depending on what was and wasn't rendered. So I decided to go ahead and write an extension for the HtmlHelper as well:

public static string ActionLinkWithRoles(this HtmlHelper html, string linkText, string actionName, string controllerName, params string[] roles) 
{
	if (HttpContext.Current.User.HasAnyRole(roles)) 
        	return html.ActionLink(linkText, actionName, controllerName); 
    
	return linkText;
}

Now for the most common restriction that needs to be placed simply became:

<%=Html.ActionLinkWithRoles("Home", "Index", "Home", ProjectRoles.Admin, ProjectRoles.User)%>

And that concluded my fight for implementing strongly typed roles in an existing MVC project.

-Tom