Wednesday 8 July 2009

ASP.NET MVC Security Trimming

We've been searching for a good way to security trim our links and button since the MVC Beta. After raising an issue on CodePlex MS assured us this would be made easier in the RTM. Turns out they were right.

Our security trimming requirements were to:

  • Use the existing ASP.Net MVC role based authorization offered by the Authorize attribute
  • Support the trimming of Action Links, Action Urls and custom scenarios

We wanted to be able to give Administrators full access to pages like this:

Untrimmed page - this user is an administrator so all links are visible

Whilst also giving standard users access only to the actions they are allowed to perform as shown below:

Trimmed page - this user has fewer privileges so the links adapt accordingly 

How Does It Work?

ASP.Net MVC 1.0 introduced the ReflectedControllerDescriptor and ReflectedActionDescriptor classes which can be used to identify the target controller and action method for a given action. The code below shows how a HasActionPermission method can be implemented to decide whether a user is authorized to perform a particular action according to the use of the Authorize attribute at the controller or action level. From this method it's fairly trivial to implement a raft of security trimmed extension methods such as SecureActionLink to complement their non-trimmed counterparts. We use this for Sharepoint style pop-up command lists and it works very nicely.

Example Code

public static bool HasActionPermission (this UrlHelper helper,
 string actionName,
 string controllerName)
{            
  // Get the controller
  var controller = GetController(helper, controllerName);
  var controllerContext = new ControllerContext (helper.RequestContext,
                                                 (ControllerBase)controller);        

  var controllerDescriptor = new ReflectedControllerDescriptor(controller.GetType());            
  var actionDescriptor = controllerDescriptor.FindAction (controllerContext,
                                                          actionName);

  // action does not exist so in this example
  // we'll authorise the request
  if (actionDescriptor == null) return true; 

  var authContext = new AuthorizationContext(controllerContext);

  // run each auth filter until one fails
  // performance could be improved by some caching
  foreach (IAuthorizationFilter authFilter 
           in actionDescriptor.GetFilters().AuthorizationFilters)
  {
    authFilter.OnAuthorization(authContext);

    if (authContext.Result != null) 
      return false;
  }

  return true;                       
}

private static IController GetController(UrlHelper helper,
                                         string controllerName)
{
  // Get the controller type
  controllerName = controllerName ?? 
                   helper.RequestContext.RouteData.GetRequiredString("controller");

  // Instantiate the controller and call Execute
  var factory = ControllerBuilder.Current.GetControllerFactory();
  var controller = factory.CreateController(helper.RequestContext,
                                            controllerName);

  if (controller != null) return controller;

  throw new InvalidOperationException(
    String.Format(
      CultureInfo.CurrentUICulture,
      "Controller factory {0} controller {1} returned null",
      factory.GetType(),
      controllerName));
}