Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Question
Monday, June 7, 2010 10:41 AM
I recently stumbled upon this thread over at StackOverflow regarding the dynamic construction of a Master Page menu based on what roles a user is a part of. The original poster proposed a solution to his own problem, but rightfully noted that it was kludgey in that it didn't scale, but also that it violated the Separation of Concerns (SoC) model of MVC.
EDIT: Sorry, I should have clarified why I wanted it done "by Roles". The original poster in the StackOverflow thread also wrote:
I have [Authorize] Attributes setup on the Actions for my Controllers to prevent unauthorized access -- but I'd like to remove those items from the menu for users who don't have the correct Role, because when they see it and click on it and it tells them they don't have permission, they'll want it. If they don't know its there, thats just better for everyone involved...
I'm basically after the same thing...
Another poster proposed this answer:
- use a common base class for in the controllers ('layer supertype')
- in the BaseController, override OnActionExecuted (you could also define an ActionFilter attribute for this)
protected override void OnActionExecuted(ActionExecutedContext filterContext
{
// build list of menu items based on user's permissions, and add it to ViewData
IEnumerable<MenuItem> menu = BuildMenu();
ViewData["Menu"] = menu;
}
In the master page:
<% var model = ViewData["Menu"] as IEnumerable<MenuItem>; %>
<% Html.RenderPartial("Menu", model); %>
(Note: in reality, I have a MasterViewModel that contains a.o. the menu model)
Does this seem like a reasonable approach? I don't love the idea of extending the "Controller" class and then inheriting that extended class in all of my other Controllers. Seems kind of like a hack. (Although it does appear to adhere to the SoC notion.)
I figure something like this surely has been solved by now as I imagine pretty much everyone would want something like this.
Is there a standard approach/code for this issue?
All replies (24)
Monday, June 7, 2010 10:55 AM ✅Answered
Myself are using RenderAction to call an action method which create a custom class and use that to render a partial view that display relevant items/links.
So in my Site.Master I go:
<% Html.RenderAction("UserWidget", "Account", new { area = "Account" }); %>
The action method it is calling: (as you can see my implementation is incomplete...krokonoster.com is a work in progress I'm using to figure things out)
[ChildActionOnly]
public ActionResult UserWidget()
{
var model = new CurrentUserViewModel();
if (User.Identity.IsAuthenticated)
{
model.IsAuthenticated = true;
model.UserName = User.Identity.Name;
model.IsAdmin = true; // TODO: fake, we dont have roles yet User.IsInRole("Admin");
}
return PartialView(model);
}
My ViewModel (which is just very basic still):
public class CurrentUserViewModel
{
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
public bool IsAdmin { get; set; }
}
and of course, the partial View: (again...work in progress)
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Krok.Web.Core.ViewModels.Account.CurrentUserViewModel>" %>
<div id="user-widget">
<% if(Model.IsAuthenticated) { %>
<p>
Howzit, <b><%: Model.UserName%></b>!
-
<%:Html.RouteLink("Sign Out", "Account.SignOut")%>
|
<%:Html.RouteLink("Change Password", "Account.ChangePassword")%>
|
<%:Html.RouteLink("Your Profile", "Account.Profile")%>
<% if(Model.IsAdmin) { %>
|
<%:Html.RouteLink("Admin", "Admin.Default")%>
<% } %>
</p>
<% } else { %>
<p>
Howzit, <strong>Guest</strong>
-
<%:Html.RouteLink("Sign In", "Account.SignIn")%>
|
<%:Html.RouteLink("Register", "Account.Register")%>
</p>
<% } %>
</div>
Works well for me, got single points of failure, easy to be tested and all is strongly-typed.
Open to suggestions/critic on this of course.
Monday, June 7, 2010 10:56 AM ✅Answered
Since MVC2 there's the ability to render actions using: <% Html.RenderAction("Action", "Controller"); %>. This works great for menu purposes. In your master page you just have to include this. Create a controller and action for your menu, mark it as [ChildAction] (so it can't get rendered besides through RenderAction). And there you go, no overrides, no large inheritance structures, just your dynamic menu in your master page.
Monday, June 7, 2010 4:40 PM ✅Answered
Wouldn't it happen every time the user loads a new page?
On the super class I will have a static Dictionary<Type, List<string, Attributes>>
where
Type - is the type of constructor
Dictionary<string, List<Attributes>> { string = action name , List<Attributes> = the attributes on the action)
You can add this ones
a) on runtime - on each constructor of the controller reflect on the "this.GetType()" if and only if does not exists in the Dictionary
b) on design time - see MVCContrib .tt file how it obtains the list of actions names, etc. Maybe you can do the same ...
Friday, June 11, 2010 2:06 PM ✅Answered
Ok, I never made it thru that T4MVC doc. It just looked far too complex for what I was trying to do. So here's the solution that I came up with.
public abstract class ExtController : Controller
{
protected static Dictionary<string, List<string>> RolesControllerDictionary;
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
// build list of menu items based on user's permissions, and add it to ViewData
IEnumerable<MenuItem> menu = BuildMenu();
ViewData["Menu"] = menu;
}
private IEnumerable<MenuItem> BuildMenu()
{
// Code to build a menu
var dynamicMenu = new List<MenuItem>();
SomeRoleProvider rp = new SomeRoleProvider();
// ^^^^^INSERT DESIRED ROLE PROVIDER HERE^^^^^
rp.Initialize("", new NameValueCollection());
try
{
// Get all roles for user from RoleProvider
foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
{
// Check if role is in dictionary
if (RolesControllerDictionary.Keys.Contains(role))
{
var controllerList = RolesControllerDictionary[role];
foreach (var controller in controllerList)
{ // Add controller to menu only if it is not already added
if (dynamicMenu.Any(x => x.Text == controller))
{ continue; }
else
{ dynamicMenu.Add(new MenuItem(controller)); }
}
}
}
}
catch { } // Most role providers can throw exceptions. Insert Log4NET or equiv here.
return dynamicMenu;
}
public ExtController()
{
// Check if ControllerRolesDictionary is non-existant
if (RolesControllerDictionary == null)
{
RolesControllerDictionary = new Dictionary<string, List<string>>();
// If so, use Reflection to add List of all Roles associated with Controllers
const bool allInherited = true;
const string CONTROLLER = "Controller";
var myAssembly = System.Reflection.Assembly.GetExecutingAssembly();
// get List of all Controllers with [Authorize] attribute
var controllerList = from type in myAssembly.GetTypes()
where type.Name.Contains(CONTROLLER)
where !type.IsAbstract
let attribs = type.GetCustomAttributes(allInherited)
where attribs.Any(x => x.GetType().Equals(typeof(AuthorizeAttribute)))
select type;
// Loop over all controllers
foreach (var controller in controllerList)
{
// Find first instance of [Authorize] attribute
var attrib = controller.GetCustomAttributes(allInherited).
First(x => x.GetType().Equals(typeof(AuthorizeAttribute))) as AuthorizeAttribute;
foreach (var role in attrib.Roles.Split(',').AsEnumerable())
{
// If there are Roles associated with [Authorize] iterate over them
if (!RolesControllerDictionary.ContainsKey(role))
{ RolesControllerDictionary[role] = new List<string>(); }
// Add controller to List of controllers associated with role (removing "controller" from name)
RolesControllerDictionary[role].Add(controller.Name.Replace(CONTROLLER,""));
}
}
}
}
}
So a controller class would look like this:
[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : ExtController
{
public ActionResult Index()
{ return View(); }
}
And the middle of the Site.Master file contains this:
<ul id="menu">
<li><%= Html.ActionLink("Home", "Index", "Home")%></li>
<% if (ViewData.Keys.Contains("Menu"))
{
foreach (MenuItem menu in (IEnumerable<MenuItem>)ViewData["Menu"])
{
%> <li><%= Html.ActionLink(menu.Text, "Index", menu.Text)%></li> <%
}
}
%>
<li><%= Html.ActionLink("About", "About", "Home")%></li>
</ul>
Pretty cool, huh?
Monday, June 7, 2010 11:10 AM
Does this adhere to the [Authorize(Roles="somerole")] attributes?
so for example, if my class was:
[Authorize(Roles="Biologist")]
[ChildActionOnly]
public class BiologistController : Controller
{
public ActionResult Index()
{
return View();
}
}
I presume that would only render for users in the Biologist Role. Yes?
Also, did you mean [ChildActionOnly]?
Monday, June 7, 2010 12:26 PM
If you do not want to duplicate code, I think you should stick with 'layer supertype'
If you do not mind duplicate code, and having both in Attributes and in the action itself, I would propose an base class for ViewModelMasterPage(with a "Menu" field to null) and each ViewModel for each View inherits from ViewModelMasterPage modify the "Menu" field
Monday, June 7, 2010 3:26 PM
So looking back at the original answer, I'm beginning to see that there is already a class "MenuItem" which is designed to created (*surprise*) Menus as well as nested menus/submenus. But I guess what I am struggling with is how such a super-class can know what Controllers and Actions a user has the rights to access.
Do I use Reflection to locate all the [Authorize] attributes with roles (like, say: [Authorize(Roles="Biologist")]) and then match the roles that the user is a part of with the Controllers and Actions that have the same Authorize attributes?
That seems like it would be expensive and/or slow.
Monday, June 7, 2010 4:02 PM
That seems like it would be expensive and/or slow.
Right. But it will happen just one time!
Monday, June 7, 2010 4:25 PM
Hmmm... Wouldn't it happen every time the user loads a new page?
If not, I'm clearly not understanding some concept here.
Monday, June 7, 2010 5:01 PM
Riiiiiight. This works because the application is constantly running. So the static Dictionary stays in memory.
Wow, ok. Thanks for clarifying. I'm used writing WinForms apps and am still very new to web programming (and ASP.NET, for that matter.)
But you agree that I can do this via Reflection?
Monday, June 7, 2010 5:05 PM
But you agree that I can do this via Reflection?
yes - I said
"on each constructor of the controller reflect on the "this.GetType()" if and only if does not exists in the Dictionary"
that shoould be said
"on each controller, on the constructor, reflect on the "this.GetType()" if and only if does not exists in the Dictionary"
Monday, June 7, 2010 5:30 PM
Ah ha! Okay, that makes sense. Thanks.
(as an aside, I dislike the term "reflection" as it already has so many other meanings in the English language that it becomes confusing when used in programmer parlance... But that's my problem, I guess...)
I'll give that a shot tomorrow.
Wednesday, June 9, 2010 12:44 PM
Ok, I've started to implement this class:
public abstract class ExtController : Controller{
protected static Dictionary<Type,List<string>> ControllerRolesDictionary;
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
// build list of menu items based on user's permissions, and add it to ViewData
IEnumerable<MenuItem> menu = BuildMenu();
ViewData["Menu"] = menu;
}
private IEnumerable<MenuItem> BuildMenu()
{
// Code to build a menu
List<MenuItem> menu = new List<MenuItem>();
AdsXmlRoleProvider rp = new AdsXmlRoleProvider();
foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
{
}
return menu;
}
public ExtController()
{
// Use this.GetType() to determine if this Controller is already in the Dictionary
if (!ControllerRolesDictionary.ContainsKey(this.GetType()))
{
// If not, use Reflection to add List of Roles to Dictionary
// associating with Controller
}
}
}
Does this look right?
How do I use Reflection to determine what Attributes are assigned to my Controller?
ALSO, I tried looking for the MvcContrib .tt file, but I cannot locate this file. Did you mean something else?
Wednesday, June 9, 2010 1:39 PM
How do I use Reflection to determine what Attributes are assigned to my Controller?
like here
http://www.switchonthecode.com/tutorials/csharp-tutorial-method-attributes-and-reflection
I tried looking for the MvcContrib .tt file
http://mvccontrib.codeplex.com/
Wednesday, June 9, 2010 2:12 PM
Yes, I found the MVCContrib website. I don't know what you mean by the ".tt file"
Wednesday, June 9, 2010 4:30 PM
I don't know what you mean by the ".tt file"
http://mvccontrib.codeplex.com/wikipage?title=T4MVC&referringTitle=Documentation
Wednesday, June 9, 2010 4:51 PM
Thanks, I'll take a look at that now
Saturday, June 12, 2010 1:41 AM
Pretty cool, huh?
Do not forget to "mark as result" your code ;-)
Saturday, June 12, 2010 8:07 PM
Heh, good point! Done!
Hey, thanks again for all your guidance and help! Much appreciated.
Monday, August 16, 2010 5:53 AM
Hi
Can you please help me in this matter.I have used your code in my MVC application.My application is a Discussion Forum(in MVC) same functionality as in this forum forums.asp.net
I have used Controllers for post, thread etc.In each controller there are some actions that are using [Authorize(Roles)] attribute
but this coding does not count those Actions in Controllers having Authorize attribute
I saw your code you have applied Authorize attribute to your class but my requirement is of Applying Authrize attributes to some actions in controller so that some are available for all users and some links are available rolewise.Now what is the solution for that?
Actually i want to make Rolebase menu in MVC such that if user doesnot have permission for some action then that Action name shouldnot be shown in the Menu.
// get List of all Controllers with [Authorize] attribute
var controllerList = from type in myAssembly.GetTypes()
where type.Name.Contains(CONTROLLER)
where !type.IsAbstract
let attribs = type.GetCustomAttributes(allInherited)//this atrribs give value object[0] whenever Controller having Authorize attribute comes
where attribs.Any(x => x.GetType().Equals(typeof(AuthorizeAttribute)))
select type;
so the var RolesControllerDictionary gets 0 value....
I have define ExtController.cs class in my MVC application.It is not placed within any folder and all my controllers are inheriting from this abstract class
Regards
shivani
Tuesday, December 7, 2010 11:04 AM
@shivani,
I don't think this code will be suitable for your purposes. It is more suitable to someone who wants to create a menu on a master page which would list just the controller names and then link to the default action ("Index") for each controller.
You might look into using an Html helper to create an ActionLink which checks the security permissions on the specific action before rendering html code. There is an example here: http://inq.me/post/ASPNet-MVC-Extension-method-to-create-a-Security-Aware-HtmlActionLink.aspx
It checks both the controller rules and the specific action rules before rendering.
There is another tool which promises to build sitemap type menu lists and breadcrumb elements. Check it out at http://mvcsitemap.codeplex.com/
Tuesday, December 7, 2010 11:57 AM
Matt,
I like what you did and extended it to work with areas.
First I defined a simple object with properties which would be needed to render and ActionLink: Action, Area, Controller and Text
public class ActionMenuItem
{
/// <summary>
/// string Action - the name of the action - eg. "Index"
/// </summary>
public string Action { get; set; }
/// <summary>
/// string Area - the name of the area - eg. "ManageClient", ""
/// </summary>
public string Area { get; set; }
/// <summary>
/// string Controller - the name of the controller - eg. "Client"
/// </summary>
public string Controller { get; set; }
/// <summary>
/// string Text - the text to display for the link - eg. "ManageClient"
/// </summary>
public string Text { get; set; }
}
I modified your BuildMenu() method to return a List of these ActionMenuItem objects
public IEnumerable<ActionMenuItem> BuildMenu(List<string> groups)
{
// Code to build a menu
var dynamicMenu = new List<ActionMenuItem>();
foreach (KeyValuePair<string, List<ActionMenuItem>> kvp in RolesControllerDictionary)
{
string role = kvp.Key;
List<ActionMenuItem> menuitems = kvp.Value;
if (string.IsNullOrEmpty(role) || groups.Contains(role))
{
foreach (ActionMenuItem menuitem in kvp.Value)
{
if (!dynamicMenu.Contains(menuitem))
{
dynamicMenu.Add(menuitem);
}
}
}
}
return dynamicMenu;
}
Note that if the role is Null or Empty it will get put into the menu. This is for controllers which require authorization but aren't restricted by roles. Here we intend to login to the entire site and then display the menu items for specific roles, but the landing page itself will be displayed to all logged in users.
There was no direct way that I could find out how to determine the name of the area that the controller was a part of. In my case I intend to put all my area controllers under the "Areas" directory which is the default place. So I took advantage of the fact that the Namespace for these controllers would be ProjectName.Areas.AreaName.Controllers
Here is the code I added to the place in your code where the RolesControllerDictionary is created
foreach (var role in attrib.Roles.Split(',').AsEnumerable())
{
// If there are Roles associated with [Authorize] iterate over them
if (!RolesControllerDictionary.ContainsKey(role))
{ RolesControllerDictionary[role] = new List<ActionMenuItem>(); }
// Add controller to List of controllers associated with role (removing "controller" from name)
ActionMenuItem menuitem = new ActionMenuItem();
menuitem.Action = "Index";
menuitem.Area = ""; // Default area
menuitem.Controller = controller.Name.Replace(CONTROLLER, "");
menuitem.Text = menuitem.Controller;
// find the area from the namespace
string[] names = controller.Namespace.Split('.');
for (int i = 0; i < names.Length; i++)
{
if (names[i] == "Areas")
{
menuitem.Area = names[i + 1];
menuitem.Text = menuitem.Area;
break;
}
}
RolesControllerDictionary[role].Add(menuitem);
}
Note that the default Area is the empty string and the Text to display defaults to the Controller name when there is no Area but displays the Area name when there is an Area.
Here is the code from Site.Master which displays the menu. I don't need a separate entry for "Home" because it is included in the menu because it gets included as an Authorized controller even though it does not require any roles.
<div id="menucontainer">
<ul id="menu">
<% if (HttpContext.Current.Session["RoleBasedMenu"] != null)
{
foreach (ActionMenuItem menu in
(IEnumerable<ActionMenuItem>)HttpContext.Current.Session["RoleBasedMenu"])
{
%>
<li>
<%= Html.ActionLink(menu.Text, menu.Action, menu.Controller, new { area = menu.Area }, null)%>
</li>
<%
}
}
%>
</ul>
</div>
You may notice that I am pulling the RoleBasedMenu from the Session variables rather than from the ViewData.
Instead of extending the Controller class and building the menu as part of each and every controller, I decided to make it part of the logon procedure and just put it in the Logon action. I agree with you, that extending the Controller class is probably more in the spirit of the MVC model. In our case we the log on is based on the active directory LDAP log on and the roles are based on the LDAP member groups, so it made sense to build the menu at the time of logging on and then to store it in the Session variables. In addition it gets removed if the user logs out.
Here is the code from the Account controller for the Logon and Logoff actions.
// **************************************
// URL: /Account/LogOn
// **************************************
/// <summary>
/// LogOn() - show the initial the account LogOn page
/// </summary>
/// <returns>ActionResult</returns>
public ActionResult LogOn()
{
return View();
}
/// <summary>
/// Logon() - process the submit from the LogOn page
/// </summary>
/// <param name="model">LogOnModel</param>
/// <param name="returnUrl">string</param>
/// <returns>ActionResult</returns>
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
ILdapUser ldapUser = LdapAuthentication.ValidateUser(domain, model.UserName, model.Password);
if (ldapUser != null)
{
// Create the cookie with the member roles set to the Ldap member groups
FormsService.SignIn(ldapUser, model.RememberMe);
// Build the menu items for this user
RoleBasedMenu roleBasedMenu = new RoleBasedMenu();
IEnumerable<ActionMenuItem> menulist = roleBasedMenu.BuildMenu(ldapUser.Groups);
HttpContext.Session["RoleBasedMenu"] = menulist;
// Now select the next view
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home", new { area = "" });
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
// **************************************
// URL: /Account/LogOff
// **************************************
/// <summary>
/// LogOff()- do a logoff and return to home page
/// </summary>
/// <returns>ActionResult</returns>
public ActionResult LogOff()
{
FormsService.SignOut();
HttpContext.Session.Remove("RoleBasedMenu");
return RedirectToAction("Index", "Home", new { area = "" });
}
Tuesday, December 7, 2010 1:59 PM
Hey, that's pretty cool that you were able to extend my Dynamic Menuing system.
I wrote that 5 months ago and haven't really gone back and looked at it -- mostly because it's working just fine. I think I may have made one change/improvement, but for the most part it's the same.
Anyway, I'm glad you liked my code. When I wrote it, I had only just started using ASP.NET MVC and wasn't entirely sure this would be a good approach (or if I was re-inventing the wheel), but it seems to be holding up very well and I'm glad someone else has been able to make use of it.
Your code changes make sense to me. I'll have to take a closer look later regarding the move of the Menu into the Session variable instead of ViewData. I think the only reason I didn't use Session previously was that I was only peripherally aware of it and what it was supposed to do, so I used ViewData instead. I'm not sure which object is "more proper" though. I'm still figuring out MVC.
Yeah, I decided to use our LDAP (well, ActiveDirectory) system for Authentication, but then Roles/Groups would be custom to the ASP.NET MVC web-app. Essentially ignoring the LDAP groups that people are already in. I did this because this website is internal to a group only and I didn't want to involve IT in making sub-groups in LDAP and then having to go to them to manage permissions, etc.
Hey, well, thanks for sharing your improvements.
Wednesday, January 18, 2012 5:11 PM
Ivan, this sounds like the way to go...
I'm using MVC 3 with Razor for a project. We have our own SQL tables with role, user and page level access.
Is there any way you can create a code snippet as a small example for the Controller and the other in the _Lyaout.cshtml file?