When you create a website with multiple permissions, it would be nice to display action links depending on those permissions. For example if your website contains a menu, its better to display links according to the permissions. You don't want your users to click on a link, and then display an unauthorized access message.
With ASP.NET MVC, we can easily create an action link that is authorizations aware. It will be above the standard action link.
Important: note that the code below is only for ASP.NET MVC 3. It's because there has been some changes with filters since ASP.NET MVC 2.
Creation
First we will create an extension that will allow us to know if an action is authorized:
using System.Web.Mvc; namespace MvcApplication.AuthorizedActionLink.Extensions { public static class ActionExtensions { public static bool ActionAuthorized(this HtmlHelper htmlHelper, string actionName, string controllerName) { ControllerBase controllerBase = string.IsNullOrEmpty(controllerName) ? htmlHelper.ViewContext.Controller : htmlHelper.GetControllerByName(controllerName); ControllerContext controllerContext = new ControllerContext(htmlHelper.ViewContext.RequestContext, controllerBase); ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(controllerContext.Controller.GetType()); ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName); if (actionDescriptor == null) return false; FilterInfo filters = new FilterInfo(FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor)); AuthorizationContext authorizationContext = new AuthorizationContext(controllerContext, actionDescriptor); foreach (IAuthorizationFilter authorizationFilter in filters.AuthorizationFilters) { authorizationFilter.OnAuthorization(authorizationContext); if (authorizationContext.Result != null) return false; } return true; } } }
As you can see we are retrieving action authorization filters. For each filter we test if the authorization context Result property is null. If this property is not null, it means that the user is not authorized to go on the action.
We also use an extension named GetControllerByName, in case the controller name is not specified:
using System; using System.Globalization; using System.Web.Mvc; namespace MvcApplication.AuthorizedActionLink { internal static class Helpers { public static ControllerBase GetControllerByName(this HtmlHelper htmlHelper, string controllerName) { IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory(); IController controller = factory.CreateController(htmlHelper.ViewContext.RequestContext, controllerName); if (controller == null) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "The IControllerFactory '{0}' did not return a controller for the name '{1}'.", factory.GetType(), controllerName)); } return (ControllerBase)controller; } } }
Now that we are able to know if an action is authorized, we can create a new link extension named ActionLinkAuthorized so it sticks to the standard ActionLink:
using System.Collections.Generic; using System.Web.Mvc; using System.Web.Mvc.Html; using System.Web.Routing; using MvcApplication.AuthorizedActionLink.Extensions; namespace MvcApplication.AuthorizedActionLink.Html { public static class LinkExtensions { public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, bool showActionLinkAsDisabled = false) { return htmlHelper.ActionLinkAuthorized(linkText, actionName, null, new RouteValueDictionary(), new RouteValueDictionary(), showActionLinkAsDisabled); } public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues, bool showActionLinkAsDisabled = false) { return htmlHelper.ActionLinkAuthorized(linkText, actionName, null, new RouteValueDictionary(routeValues), new RouteValueDictionary(), showActionLinkAsDisabled); } public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, bool showActionLinkAsDisabled = false) { return htmlHelper.ActionLinkAuthorized(linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary(), showActionLinkAsDisabled); } public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues, bool showActionLinkAsDisabled = false) { return htmlHelper.ActionLinkAuthorized(linkText, actionName, null, routeValues, new RouteValueDictionary(), showActionLinkAsDisabled); } public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues, object htmlAttributes, bool showActionLinkAsDisabled = false) { return htmlHelper.ActionLinkAuthorized(linkText, actionName, null, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes), showActionLinkAsDisabled); } public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool showActionLinkAsDisabled = false) { return htmlHelper.ActionLinkAuthorized(linkText, actionName, null, routeValues, htmlAttributes, showActionLinkAsDisabled); } public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes, bool showActionLinkAsDisabled = false) { return htmlHelper.ActionLinkAuthorized(linkText, actionName, controllerName, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes), showActionLinkAsDisabled); } public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool showActionLinkAsDisabled) { if (htmlHelper.ActionAuthorized(actionName, controllerName)) { return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes); } else { if (showActionLinkAsDisabled) { TagBuilder tagBuilder = new TagBuilder("span"); tagBuilder.InnerHtml = linkText; return MvcHtmlString.Create(tagBuilder.ToString()); } else { return MvcHtmlString.Empty; } } } } }
Example of use
Let's say we want to authorize only users with role Administrator on the HomeController action named ThePrivilegeZone. ThePrivilegeZone action will be decorated with an authorize attribute. We add a link to this action in the site menu with the authorized action link extension specifying that we want to show the action link as disabled if the user is not authorized:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %> <%@ Import Namespace="MvcApplication.AuthorizedActionLink.Html" %> <!DOCTYPE html> <html> <head runat="server"> <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title> <link href="../../Content/Site.css" rel="stylesheet" type="text/css" /> <script src="<%: Url.Content("~/Scripts/jquery-1.4.4.min.js") %>" type="text/javascript"></script> </head> <body> <div class="page"> <div id="header"> <div id="title"> <h1>My MVC Application</h1> </div> <div id="logindisplay"> <% Html.RenderPartial("LogOnUserControl"); %> </div> <div id="menucontainer"> <ul id="menu"> <li><%: Html.ActionLink("Home", "Index", "Home")%></li> <li><%: Html.ActionLink("About", "About", "Home")%></li> <li><%: Html.ActionLinkAuthorized("The Privilege Zone", "ThePrivilegeZone", "Home", true)%></li> </ul> </div> </div> <div id="main"> <asp:ContentPlaceHolder ID="MainContent" runat="server" /> <div id="footer"> </div> </div> </div> </body> </html>
The result looks like below when the user is not authorized and the action link disabled:
It would be better to add some styling :)
To go further
To be honest I didn't invent anything here. I have just take a deep look into the ASP.NET MVC 3 source code available on the Microsoft website in order to understand how authorizations works, and I have mixed it with the standard ActionLink. My code is based on three classes: ControllerActionInvoker, MvcHandler, LinkExtensions.
Summary
We have seen how to create and use an authorized action link.
You can download the example solution here:
(Note that the project uses ASP.NET MVC 3)
Please feel free to comment or contact me if you have any question about this article.
I must say, that this is genius and I will be translating this to VB and adding it to my ever expanding collection of HTMLHelper extensions. I'm disappointed that you seemed to have stopped posting.
Unfortunately this solution doesn't work when trying to create actionlinks across Areas.
This code is never going to work correctly working with Areas.
IController controller = factory.CreateController(htmlHelper.ViewContext.RequestContext, controllerName);
your website is very useful, thnx hey!!
its very useful but not working with Areas. Is there any workaround?
Great article, totally what I wanted to find.
I have implemented AuthorizeAttribute with customized authorization logic. I was wondering how to call my overriden OnAuthorization instead of the one you're calling in ActionAuthorized method?