views:

156

answers:

1

How can I create a "security aware" action link that detects if a user is authorized to click (invoke) the action?
Hide link if user is not allowed to use that action...

Depending from

  • web.config (authorization) and
  • [Authorize] attributes on actions

PS
I guess it is bad practice to mix those 2 in MVC?

+10  A: 

This is some code poached from the MvcSitemap project and modified for my own use. If I remember correctly this code has been modified for MVC2 and some of the functions might have to be back ported to MVC1.

Its not bad practices at all to mix MVC and FormsAuthentication together, MVC's default authentication methods are build around the existing Asp.net security infrastructure.

Code to determine if user has permissions:

public static class SecurityTrimmingExtensions 
{

    public static bool HasActionPermission( this HtmlHelper htmlHelper, string actionName, string controllerName )
    {
        //if the controller name is empty the ASP.NET convention is:
        //"we are linking to a different controller
        ControllerBase controllerToLinkTo = string.IsNullOrEmpty(controllerName) 
                                                ? htmlHelper.ViewContext.Controller
                                                : GetControllerByName(htmlHelper, controllerName);

        var controllerContext = new ControllerContext(htmlHelper.ViewContext.RequestContext, controllerToLinkTo);

        var controllerDescriptor = new ReflectedControllerDescriptor(controllerToLinkTo.GetType());

        var actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);

        return ActionIsAuthorized(controllerContext, actionDescriptor);
    }


    private static bool ActionIsAuthorized(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        if (actionDescriptor == null)
            return false; // action does not exist so say yes - should we authorise this?!

        AuthorizationContext authContext = new AuthorizationContext(controllerContext);

        // run each auth filter until on 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 ControllerBase GetControllerByName(HtmlHelper helper, string controllerName)
    {
        // Instantiate the controller and call Execute
        IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();

        IController controller = factory.CreateController(helper.ViewContext.RequestContext, controllerName);

        if (controller == null)
        {
            throw new InvalidOperationException(

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

        }

        return (ControllerBase)controller;
    }

}

Html Helpers

public static class SecurityTrimmedLink
{
    public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkName, string actionName)
    {
        return htmlHelper.HasActionPermission(actionName, "")
                   ? htmlHelper.ActionLink(linkName, actionName)
                   : MvcHtmlString.Create("");
    }        

    public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkName, string actionName, RouteValueDictionary routeValueDictionary )
    {
        return htmlHelper.HasActionPermission(actionName, "")
                   ? htmlHelper.ActionLink(linkName, actionName, routeValueDictionary)
                   : MvcHtmlString.Create("");
    }

    public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkName, string actionName, object routeValues, object htmlAttributes )
    {
        return htmlHelper.HasActionPermission(actionName, "")
                   ? htmlHelper.ActionLink(linkName, actionName, routeValues, htmlAttributes)
                   : MvcHtmlString.Create("");
    }

    public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkName, string actionName, string controllerName)
    {
        return htmlHelper.HasActionPermission(actionName, controllerName)
                   ? htmlHelper.ActionLink(linkName, actionName, controllerName)
                   : MvcHtmlString.Create("");
    }
}
jfar
Sweet!!! Exactly what I was looking for! Thanks!
Peter Gfader
Can you update this answer or post a new one, when you implement caching as you mention in your code comment? :-) THANKS heaps... Or point to a blog or resource where to keep updated on this..
Peter Gfader
@Peter Gfader This is just some code that floats around my personal toolbox. So far its always been the smallest performance concern.
jfar
Hey, I'm trying to get this to work, and it seems to have issues when using areas. Have you used this with Areas before?
Charles Boyung
Yes, just change the htmlHelper.ActionLink calls and add some method overloads.
jfar
The problem is that when it calls CreateController, I get the following error: The controller for path '[PAGE URL]' was not found or does not implement IController. This happens whenever the page I'm viewing is in an area because the area in the RequestContext doesn't match.
Charles Boyung
Okay, I found a solution. The problem is that the "UseNamespaceFallback" DataToken in the route data is being set to false somewhere for some reason, so it is only searching for controllers in the current request's controller's namespace. I thought that this defaulted to null (meaning true in this situation), but I'm not setting it anywhere and the value is coming back as false, so that's the problem.
Charles Boyung
To fix, I store the original value of UseNamespaceFallback in a temp variable and then set the token to true right before CreateController. Right after that, I set it back to the original value.
Charles Boyung