Patrick Desjardins Blog
Patrick Desjardins picture from a conference

How to create an Html Extension like ActionLink to create link only if the user has the authorization

Posted on: 2013-05-08

The idea to create an Html Extension like ActionLink to create link only if the user has the authorization can be very helpful. The goal here is to display the link only if the action allow the user to execute the action. This way, every user will see link to action that it belong to. On the other hand, people with the access to these actions will see links to those action.

Here is an example of the final product. The first line is the new ActionLink and the second line is the default one. The result will be that a user that has access to "Edit" will see the link when others that doesn't have access won't see it. The second line will show for every body the link for "Details".

@Html.Input().ActionLink("Edit", "Edit", null, (new { id=item.Id })) 
@Html.ActionLink("Details", "Details", new { id=item.Id })

On the controller side, you will see the Edit action with the authorization attribute.

[HttpGet]
[Authorize(Roles = Roles.ADMINISTRATOR)]
public ActionResult Edit(int id) {
  //...
}

First of all, we need to create a class to be able to get the attribute of the action. Here is the class.

public class AttributeHelper {
  private readonly HtmlHelper_htmlHelper;
  public AttributeHelper(HtmlHelper htmlHelper) {
    _htmlHelper = htmlHelper;
  }

  public IEnumerable<Attribute> GetAttributes( string actionName, string controllerName, string method = "GET") {
    var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
    var otherController = (ControllerBase)controllerFactory.CreateController(new RequestContext(_htmlHelper.ViewContext.HttpContext, new RouteData()), controllerName);
    var controllerDescriptor = new ReflectedControllerDescriptor(otherController.GetType());
    var controllerContext2 = new ControllerContext(new HttpContextWrapperWithHttpMethod(_htmlHelper.ViewContext.HttpContext.ApplicationInstance.Context, method), new RouteData(), otherController);
    var actionDescriptor = controllerDescriptor.FindAction(controllerContext2, actionName);
    var attributes = actionDescriptor.GetCustomAttributes(true).Cast<Attribute>().ToArray();
    return attributes;
  }

  private class HttpContextWrapperWithHttpMethod : HttpContextWrapper {
    private readonly HttpRequestBase_request;

    public HttpContextWrapperWithHttpMethod(HttpContext httpContext, string method): base(httpContext) {
      this._request = new HttpRequestWrapperWithHttpMethod(httpContext.Request, method);
    }

    public override HttpRequestBase Request {
      get { return_request; }
    }

    private class HttpRequestWrapperWithHttpMethod : HttpRequestWrapper {
      private readonly string_httpMethod;

      public HttpRequestWrapperWithHttpMethod(HttpRequest httpRequest, string httpMethod): base(httpRequest) {
        this._httpMethod = httpMethod;
      }

      public override string HttpMethod {
        get { return_httpMethod; }
      }
    }
  }
}

If you want to use it, you must call the method and then search for the required attribute.

 var attributeHelper = new AttributeHelper(HtmlHelper);
 var att = attributeHelper.GetAttributes(actionName, controllerName).OfType<AuthorizeAttribute>();
 var isInRole = att.Aggregate(false, (f, g) => f | HtmlHelper.ViewContext.HttpContext.User.IsInRole(g.Roles));

Line 1 instantiate the helper that will give you the possibility to get attributes. The second line will filter to get AuthorizedAttribute. This can return multiple attribute so we have to use the third line to aggregate if the user is in one of the AuthorizedAttribute.

The Html helper can now use this to display or not the link.

public MvcHtmlString ActionLink(string linkText , string actionName = null , string controllerName = null , object routeValues = null , object htmlAttributes=null) {
  if (actionName == null) {
     actionName = HtmlHelper.ViewContext.RouteData.GetRequiredString("action");
  }

  if (controllerName == null) {
      controllerName = HtmlHelper.ViewContext.RouteData.GetRequiredString("controller");
  }
  
  var routeValues2 = new RouteValueDictionary(routeValues); 
  var attributes = (IDictionary<string, object>)HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);

  if (string.IsNullOrEmpty(linkText)) 
    throw new ArgumentException("linkText"); 
  else { 
    var attributeHelper = new AttributeHelper(HtmlHelper); 
    var att = attributeHelper.GetAttributes(actionName, controllerName).OfType<AuthorizeAttribute>(); 
    var isInRole = att.Aggregate(false, (f, g) => f | HtmlHelper.ViewContext.HttpContext.User.IsInRole(g.Roles)); 
    if (isInRole) { 
      return MvcHtmlString.Create(HtmlHelper.GenerateLink(HtmlHelper.ViewContext.RequestContext, HtmlHelper.RouteCollection, linkText, (string)null, actionName, controllerName, routeValues2, attributes)); 
    } 
    return new MvcHtmlString(""); 
  } 
}

Line 9 and 13 are only there if the controller or and action is not defined. In practice, the controller shouldn't be used all the time because we are most of the time using an action that is inside the controller. For example, we are in the Index of the controller Muscle. If we want to Edit, Create, etc, all of theses action are in the controller, it doesn't require you to specify every time the controller.