ASP.Net MVC Hide/Show Menu Items Based On Security

Gavin picture Gavin · Dec 7, 2011 · Viewed 29k times · Source

I'm working on an ASP.Net MVC 3 site. The _Layout master view contains a menu and I want to hide some of the items in the menu based on if you are logged in and what roles you are in.

This currently works using code like this

@if (HttpContext.Current.User.Identity.IsAuthenticated)
{
   <li id="MyLearningTab">@Html.ActionLink("My Learning", "MyLearning", "Learning")</li> 
   if (HttpContext.Current.User.IsInRole("Reporters"))
   {
      <li id="ReportTab">@Html.ActionLink("Reports", "Index", "Reports")</li>
   }
   if (HttpContext.Current.User.IsInRole("Administrators"))
   {
      <li id="DashboardTab">@Html.ActionLink("Dashboard", "Dashboard", "Admin")</li>
      <li id="AdminTab">@Html.ActionLink("Admin", "Index", "Admin")</li> 
   }
}

I'd like to refactor this in to something more readable and came up with something like this

@if ((bool)ViewData["MenuMyLearning"]){<li id="MyLearningTab">@Html.ActionLink("My Learning", "MyLearning", "Learning")</li> }    
@if((bool)ViewData["MenuReports"]){<li id="ReportTab">@Html.ActionLink("Reports", "Index", "Reports")</li>}
@if ((bool)ViewData["MenuDashboard"]){<li id="DashboardTab">@Html.ActionLink("Dashboard", "Dashboard", "Admin")</li>}
@if ((bool)ViewData["MenuAdmin"]){<li id="AdminTab">@Html.ActionLink("Admin", "Index", "Admin")</li>}

I originally added the following to my base controller constructor thinking I could setup the ViewData for these properties there

ViewData["MenuDashboard"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Administrators");
ViewData["MenuAdmin"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Administrators");
ViewData["MenuReports"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Reportors");
ViewData["MenuMyLearning"] = User != null && User.Identity.IsAuthenticated;

However it turns out the User object is null at this point in the lifecycle. I've also tried creating a custom global filter but the ViewData is then not accessable.

What is the recommended way of doing something like this? Should I just leave it how it was at first with all the HttpContext code in the view?

Answer

Tom Chantler picture Tom Chantler · Dec 8, 2011

General advice about roles

The way I have done this is to create a custom principal and to store the extra required information in there. In your example, this would at least include the roles for the user. That way you avoid making lots of extra trips to the user store (which is likely a SQL database).

Have a look a this question of mine in which I give the code which I am using successfully: Is this Custom Principal in Base Controller ASP.NET MVC 3 terribly inefficient?

Note that I am storing the custom principal in the cache rather than in the session (just being paranoid about session hijacking).

I like this approach as it is very extensible. For example, I have since extended this to expose Facebook credentials for when the user logs in via Facebook.

Just remember that if you are caching data you need to remember to update it when it changes!

Answer to your question

Just to add, in your specific case, you should probably store this extra information in a ViewModel and then your view would say things like:

@if(ShowReports) { <li id="ReportTab">@Html.ActionLink("Reports", "Index", "Reports")</li> }
@if(ShowDashboard) { <li id="DashboardTab">@Html.ActionLink("Dashboard", "Dashboard", "Admin")</li> }
@if(ShowAdmin { <li id="AdminTab">@Html.ActionLink("Admin", "Index", "Admin")</li> }

with the ViewModel Code saying something like:

public bool ShowReports {get;set;}
public bool ShowDashboard {get;set;}
public bool ShowAdmin {get;set;}

public void SetViewModel()
{
  if (User.Identity.IsAuthenticated)
  {
    if (HttpContext.Current.User.IsInRole("Reporters"))
    {
       ShowReports = true;
    }
    if (HttpContext.Current.User.IsInRole("Administrators"))
    {
       ShowDashboard = true;
       ShowAdmin = true;
    }
  }
}

I actually tend to take this one step further and create a ReportsLink in my ViewModel and set it to contain the link if the user is authorised or to be an empty string if they are not. Then the view just says:

@Model.ReportsLink
@Model.DashboardLink
@Model.AdminLink

In that case the pertinent part of the ViewModel might be like this:

ReportLink = new MvcHtmlString(HtmlHelper.GenerateLink(HttpContext.Current.Request.RequestContext, System.Web.Routing.RouteTable.Routes, "linktext", "routename", "actionname", "controllername", null, null));