ASP.Net MVC Extension method to create a Security Aware Html.ActionLink

by robert 22. October 2008 12:58

I am a big fan of ASP.Net MVC and the DRY principle.

Extending the work done by Maarten Balliauw, the following is my attempt at creating an "security aware" action link that detects if a user is authorized to click (invoke) the action. The point is to show, hide or disable a link based on the Authorize attribute of the controller.

image

The code allows you to show a disabled link as a <span> label or hide it completely.

I'm trying to avoid using Reflection, but so far I haven't figured out how.

Here is the code:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal;
using System.Web.Routing;
using System.Web.Mvc;
using System.Collections;
using System.Reflection;
namespace System.Web.Mvc.Html
{
    public static class HtmlHelperExtensions
    {
        public static string SecurityTrimmedActionLink(
        this HtmlHelper htmlHelper,
        string linkText,
        string action,
        string controller)
        {
            return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, false);
        }
        public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, bool showDisabled)
        {
            if (IsAccessibleToUser(action, controller))
            {
                return htmlHelper.ActionLink(linkText, action, controller);
            }
            else
            {
                return showDisabled ? String.Format("<span>{0}</span>", linkText) : "";
            }
        }
        public static bool IsAccessibleToUser(string actionAuthorize, string controllerAuthorize)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            GetControllerType(controllerAuthorize);
            Type controllerType = GetControllerType(controllerAuthorize);
            var controller = (IController)Activator.CreateInstance(controllerType);
            ArrayList controllerAttributes = new ArrayList(controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true));
            ArrayList actionAttributes = new ArrayList();
            MethodInfo[] methods = controller.GetType().GetMethods();
            foreach (MethodInfo method in methods)
            {
                object[] attributes = method.GetCustomAttributes(typeof(ActionNameAttribute), true);
                if ((attributes.Length == 0 && method.Name == actionAuthorize) || (attributes.Length > 0 && ((ActionNameAttribute)attributes[0]).Name == actionAuthorize))
                {
                    actionAttributes.AddRange(method.GetCustomAttributes(typeof(AuthorizeAttribute), true));
                }
            }
            if (controllerAttributes.Count == 0 && actionAttributes.Count == 0)
                return true;

            IPrincipal principal = HttpContext.Current.User;
            string roles = "";
            string users = "";
            if (controllerAttributes.Count > 0)
            {
                AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
                roles += attribute.Roles;
                users += attribute.Users;
            }
            if (actionAttributes.Count > 0)
            {
                AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
                roles += attribute.Roles;
                users += attribute.Users;
            }

            if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
                return true;

            string[] roleArray = roles.Split(',');
            string[] usersArray = users.Split(',');
            foreach (string role in roleArray)
            {
                if (role == "*" || principal.IsInRole(role))
                    return true;
            }
            foreach (string user in usersArray)
            {
                if (user == "*" && (principal.Identity.Name == user))
                    return true;
            }
            return false;
        }

        public static Type GetControllerType(string controllerName)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            foreach (Type type in assembly.GetTypes())
            {
                if (type.BaseType.Name == "Controller" && (type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper())))
                {
                    return type;
                }
            }
            return null;
        }
    }
}

Tags:

ASP.Net MVC

Comments

10/22/2008 1:08:46 PM #

Sorry about the code format. I'm testing a new code formatting scheme.

robert United States

10/22/2008 11:18:06 PM #

This is actually pretty cool. Thanks for sharing. It is very useful i think.

Yazılım Turkey

10/23/2008 1:25:13 AM #

Trackback from DotNetKicks.com

ASP.Net MVC Extension method to create a Security Aware Html.ActionLin

DotNetKicks.com

10/23/2008 6:35:30 AM #

What are the benefits of toggling the link in your approach vs. giving the menu link an ID and doing something like:

menuHome.Visible = Roles.IsUserInRole("Admin");

J United States

10/24/2008 3:44:35 AM #

@J-

The benefit is that it makes the code easier to maintain.

If you put the list of roles on each link, one has to locate all the links that are affected and manually change them. This can get complicated with MVC's routing mechanisms.

With this approach, you add or remove a role (or user) from the Authorize Attribute in the controller class or method and that changes the behavior of all affected links, regardless of their location.

robert United States

10/24/2008 6:57:26 AM #

Robert,

This is very nice!!!  I've got it working in my "test" application!  I do have one question tho...you mentioned on Maarten's blog that you were having a problem with the MvcHandler returning a null everytime in the IsAccessibleToUser method...did you ever figure that out?  I'm having the same issue.  Thx for the great security utility, I don't think I'd be as interested in MVC without it...

Mike United States

10/25/2008 5:59:58 AM #

@Mike-
I abandoned the MvcHandler for the Assembly approach. I just couldn't get the RequestContext instantiated under this scenario.
I think this approach can take care of the Reflection issues if one creates a cache of all the method attributes and uses that to get the authorize information. Perhaps Maarten's sitemap can take advantage of this as well.

robert United States

12/9/2008 6:31:02 PM #

Do NOT ever use .ToUpper to insure comparing strings is case-insensitive.  Instead of this:
type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper()))

Do this:
type.Name.Equals(controllerName + "Controller", StringComparison. InvariantCultureIgnoreCase)

Marc Brooks United States

12/31/2008 10:34:13 AM #

This is such a Great Idea!

I too had a problem with using the "calling assembly". I thought about it and maybe: I think u could get rid of the GetControllerType(..) chunk of code by adding a type parameter to your extension methods.

For example, the signature would be:
public static string SecurityTrimmedActionLink[T](...)
Where typeof(T) will give u the controller Type.

the calling syntax on the view would then be:
[%= Html.SecurityTrimmedActionLink[AdminController]("LinkText","ActionX","Admin",new{ID=22}) %]
That worked for me!
I think you should consider submitting this clever concept to MVC futures (if it isnt already there).

I'm wondering if its worthwhile to consider a "wrapper div" that toggles display of its inner content.
Sorta like an HtmlHelper method that accepts innertags or partials as a rendering argument, and returns a div wrapped around the content. In classic ASP, those were "Panels"

pete w

6/11/2009 6:49:16 AM #

Here's an expression-based example you might find useful:

chriscavanagh.wordpress.com/.../

Chris Cavanagh United States

7/15/2009 9:54:59 PM #

This is very nice!!! I've got it working in my "test" application! I do have one question tho...you mentioned on Maarten's blog that you were having a problem with the MvcHandler returning a null everytime in the IsAccessibleToUser method...did you ever figure that out? I'm having the same issue. Thx for the great security utility, I don't think I'd be as interested in MVC without it...

Poker game online United States

8/8/2009 9:50:09 AM #

There are certainly a lot of details like that to take into consideration. That�s a great point to bring up. I offer the thoughts above as general inspiration but clearly there are questions like the one you bring up where the most important thing will be working in honest good faith. I don�t know if best practices have emerged around things like that, but I am sure that your job is clearly identified as a fair game.

Link Building United Kingdom

8/9/2009 9:43:14 AM #

I've got it working in my "test" application! I do have one question tho...you mentioned on Maarten's blog that you were having a problem with the MvcHandler returning a null everytime in the IsAccessibleToUser method.Did you ever figure that out? I'm having the same issue. Thx for the great security utility.

emo hair

emo hair United States

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About the author

Something about the author

Tag cloud

    Month List

    Page List