Categories
ASP.NET ASP.NET MVC Microsoft

Create an authorized action link extension for ASP.NET MVC 3 (En anglais)

In this article I will explain how to create an action link extension that is authorizations aware, so that we can hide or disable an action link based on its authorizations.

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:

Download full sources

(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.

Categories
ASP.NET ASP.NET MVC Microsoft

Créer un AuthorizeAttribute personnalisé qui accèpte des paramètres de type enum

Dans cet article je vais vous expliquer comment créer simplement un AuthorizeAttribute qui accepte des paramètres de type enum afin d’éviter le codage en dur des rôles.

Avez-vous déjà essayé dans l’un de vos projets ASP.NET MVC d’utiliser un attribut [Authorize] pour par exemple assigner des rôles, en utilisant la valeur d’une Enum ?

Si c’est le cas, vous obtiendrez le message d’erreur suivant en anglais lors de la compilation :

An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type

 

C’est parce que vous avez besoin d’utiliser des valeurs statiques et il est impossible d’utiliser une Enum pour définir les propriétés d’un Attribute. Cela signifie que vous ne pouvez pas définir la propriété Roles d’un [AuthorizeAttribute] avec une valeur d’Enum.

C’est frustrant, parce que personnellement je n’aime pas avoir à coder en dur les rôles dans une application. Cela rend l’application pas très lisible et plus compliquée à maintenir.

 

Heureusement, ASP.NET MVC nous permet de personnaliser l'[AuthorizeAttribute] facilement sans avoir à remplacer le processus de sécurité standard.

 

 Création

Pour nos besoins, nous allons créer l’Enum ci-dessous pour déclarer les rôles:

namespace MvcApplication.HowTo.Enums
{
    public enum Role
    {
        Administrator = 1,
        UserWithPrivileges = 2,
        User = 3,
    }
}

 

Maintenant, nous allons créer un [AuthorizeAttribute] personnalisé qui accepte des Enum en tant que paramètres dans le constructeur. Il héritera de System.Web.Mvc.AuthorizeAttribute :

using System;
using System.Linq;
using System.Web.Mvc;

namespace MvcApplication.HowTo.Attributes
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
    public class AuthorizeEnumAttribute : AuthorizeAttribute
    {
        public AuthorizeEnumAttribute(params object[] roles)
        {
            if (roles.Any(r => r.GetType().BaseType != typeof(Enum)))
                throw new ArgumentException("roles");

            this.Roles = string.Join(",", roles.Select(r => Enum.GetName(r.GetType(), r)));
        }
    }
}

Comme vous pouvez le voir, il faut prêter attention à plusieurs choses ici :

  • Le constructeur accepte des paramètres de type object, c’est la petite astuce. Si vous utilisez des paramètres de type Enum, vous obtiendrez le même message d’erreur que ci-dessus. Nous pouvons faire cela car une Enum est un object.
  • Pour veiller à ce que l’on passe des paramètres de type Enum, on vérifie le type de tous les rôles. Si un rôle n’est pas de type Enum, le constructeur lèvera une ArgumentException.
  • Ensuite nous définissons la propriété standard Roles avec le nom de nos rôles, à l’aide de la méthode string.join.

 

Exemple d’utilisation

Disons que nous voulons autoriser uniquement les utilisateurs ayant les rôles Administrator ou UserWithPrivileges sur l’action nommée ThePrivilegeZone du HomeController. L’action ThePrivilegeZone sera décorée de notre authorize attribute personnalisé comme ci-dessous :

using System.Web.Mvc;
using MvcApplication.HowTo.Attributes;
using MvcApplication.HowTo.Enums;

namespace MvcApplication.HowTo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";

            return View();
        }

        public ActionResult About()
        {
            return View();
        }

        [AuthorizeEnum(Role.Administrator, Role.UserWithPrivileges)]
        public ActionResult ThePrivilegeZone()
        {
            return View();
        }
    }
}

Notre code est propre comme ça, n’est-ce pas ?

 

Pour aller plus loin

Si l’on regarde avec un outil comme Reflector, il est intéressant de comprendre comment l’action ThePrivilegeZone est décorée lors de la compilation. Voici une capture d’écran :

Nous comprenons qu’une fois compilées, les valeurs de l’Enum Role sont utilisés et non les noms. Ici 1 (Administrator) et 2 (UserWithPrivileges).

Notez que si vous utilisez une Enum pour vos rôles sans mettre de valeurs comme nous l’avons fait ici, les valeurs de l’Enum seront dans l’ordre 0, 1, 2, etc juqsu’au nombre de vos rôles moins 1.

 

En résumé

Nous avons vu comment créer et utiliser un AuthorizeAttribute personnalisé qui accepte des paramètres de type enum. Ici nous définissons seulement la propriété Roles, mais en fonction de vos besoins vous pouvez faire la même chose avec la propriété Users. Personnellement, j’utilise cet attribut personnalisé dans mon framework afin de le réutiliser dans tous mes projets ASP.NET MVC.

Vous pouvez télécharger la solution exemple ici :

Télécharger la solution Authorize Enum

(Notez que le projet utilise ASP.NET MVC 3)

 

N’hésitez pas à commenter ou à me contacter si vous avez des questions à propos de cet article.