I'm creating a workflow tool that will be used on our company intranet. Users are authenticated using Windows Authentication and I've set up a custom RoleProvider that maps each user to a pair of roles.
One role indicates their seniority (Guest, User, Senior User, Manager etc.) and the other indicates their role/department (Analytics, Development, Testing etc.). Users in Analytics are able to create a request that then flows up the chain to Development and so on:
Models
public class Request
{
public int ID { get; set; }
...
public virtual ICollection<History> History { get; set; }
...
}
public class History
{
public int ID { get; set; }
...
public virtual Request Request { get; set; }
public Status Status { get; set; }
...
}
In the controller I have a Create() method that will create the Request header record and the first History item:
Request Controller
public class RequestController : BaseController
{
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create (RequestViewModel rvm)
{
Request request = rvm.Request
if(ModelState.IsValid)
{
...
History history = new History { Request = request, Status = Status.RequestCreated, ... };
db.RequestHistories.Add(history);
db.Requests.Add(request);
...
}
}
}
Each further stage of the request will need to be handled by different users in the chain. A small subset of the process is:
User creates Request [Analytics, User]
Manager authorises Request [Analytics, Manager]
Developer processes Request [Development, User]
Currently I have a single CreateHistory() method that handles each stage of the process. The status of the new History item is pulled up from the View:
// GET: Requests/CreateHistory
public ActionResult CreateHistory(Status status)
{
History history = new History();
history.Status = status;
return View(history);
}
// POST: Requests/CreateHistory
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateHistory(int id, History history)
{
if(ModelState.IsValid)
{
history.Request = db.Requests.Find(id);
...
db.RequestHistories.Add(history);
}
}
The CreateHistory View itself will render a different partial form depending on the Status. My intention was that I could use a single generic CreateHistory method for each of the stages in the process, using the Status as a reference to determine which partial View to render.
Now, the problem comes in rendering and restricting available actions in the View. My CreateHistory View is becoming bloated with If statements to determine the availability of actions depending on the Request's current Status:
#* Available user actions *#
<ul class="dropdown-menu" role="menu">
#* Analyst has option to withdraw a request *#
<li>#Html.ActionLink("Withdraw", "CreateHistory", new { id = Model.Change.ID, status = Status.Withdrawn }, null)</li>
#* Request manager approval if not already received *#
<li>...</li>
#* If user is in Development and the Request is authorised by Analytics Manager *#
<li>...</li>
...
</ul>
Making the right actions appear at the right time is the easy part, but it feels like a clumsy approach and I'm not sure how I would manage permissions in this way. So my question is:
Should I create a separate method for every stage of the process in the RequestController, even if this results in a lot of very similar methods?
An example would be:
public ActionResult RequestApproval(int id)
{
...
}
[MyAuthoriseAttribute(Roles = "Analytics, User")]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult RequestApproval(int id, History history)
{
...
}
public ActionResult Approve (int id)
{
...
}
[MyAuthoriseAttribute(Roles = "Analytics, Manager")]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Approve (int id, History history)
{
...
}
If so, how do I handle rendering the appropriate buttons in the View? I only want a set of valid actions appear as controls.
Sorry for the long post, any help would be greatly appreciated.
First of all if you have a lot of logic encapsulated in boolean based operations I highly recommend using the Specifications Pattern this and this should start you off well. It is highly reusable and allows great maintainability when existing logic changes or you need to add new logic. Look into making composite specifications that specify exactly what can be satisfied e.g. If the user is a manager and the request is unapproved.
Now with regards to your problem in your view - although when I faced the same issue in the past I had followed a similar approach to ChrisDixon. It was simple and easy to work with but looking back at the app now I find it tedious since it is burried in if statements. The approach I would take now is to create custom action links or custom controls that take the authorization into context when possible. I started writing some code to do this but in the end realized that this must be a common issue and hence found something a lot better than I myself intended to write for this answer. Although aimed at MVC3 the logic and purpose should still hold up.
Below are the snippets in case the article ever gets removed. :)
The is the extension method that checks the controller for the Authorized Attribute. In the foreach loop you can check for the existence of your own custom attribute and authorize against it.
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;
}
}
This is a helper method to get the ControllerBase object which is used in the above snippet to interrogate the action filters.
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;
}
}
This is the custom Html Helper that generates the action link if authorization passes. I have tweaked it from the original article to remove the link if not authorized.
public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
{
if (htmlHelper.ActionAuthorized(actionName, controllerName))
{
return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
}
else
{
return MvcHtmlString.Empty;
}
}
Call it as you would normally call an ActionLink
#Html.ActionLinkAuthorized("Withdraw", "CreateHistory", new { id = Model.Change.ID, status = Status.Withdrawn }, null)
When coding in MVC (or, well, any language) I try and keep all, or most of, my logical statements away from my Views.
I'd keep your logic processing in your ViewModels, so:
public bool IsAccessibleToManager { get; set; }
Then, in your view, it's simple to use this variable like #if(Model.IsAccessibleToManager) {}.
This is then populated in your Controller, and can be set however you see fit, potentially in a role logic class that keeps all this in one place.
As for the methods in your Controller, keep these the same method and do the logical processing inside the method itself. It's all entirely dependant on your structure and data repositories, but I'd keep as much of the logical processing itself at the Repository level so it's the same in every place you get/set that data.
Normally you'd have attribute tags to not allow these methods for certain Roles, but with your scenario you could do it this way...
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Approve (int id, History history)
{
try {
// The logic processing will be done inside ApproveRecord and match up against Analytics or Manager roles.
_historyRepository.ApproveRecord(history, Roles.GetRolesForUser(yourUser));
}
catch(Exception ex) {
// Could make your own Exceptions here for the user not being authorised for the action.
}
}
What about creating different views for each type of role, and then returning the appropriate view, from a single action?
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Approve (int id, History history)
{
// Some pseudo-logic here:
switch(roles)
{
case Manager:
case User:
{
return View("ManagerUser");
}
case Manager:
case Analyst:
{
return View("ManagerAnalyst");
}
}
}
Of course, this approach would require you to create a view for the different combinations of roles, but at least you'd be able to render the appropriate view code without the UI logic cluttering the views.
I would suggest you to use a provider to generate a list of available actions for user.
First I would define AwailableAction enum than describe what action your users might potentially have. Maybe you already have one.
Then you can define IAvailableActionFactory interface and implement it with your logic:
public interface IAvailableActionProvider
{
ReadOnlyCollection<AwailableAction> GetAvailableActions(User, Request, History/*, etc*/) // Provide parameters that need to define actions.
}
public class AvailableActionProvider : IAvailableActionProvider
{
ReadOnlyCollection<AwailableAction> GetAvailableActions(User, Request, History)
{
// You logic goes here.
}
}
Internally this provider would use similar logic you currently inplemented in the view. This approach will keep the view clean and ensure testability of the logic. Optionaly inside provivder you can use different strategies for different users and make the implementation even more decoupled.
Then in controller you define the dependency on this provider and either resolve it through container of instantioate directly if you don't use container yet.
public class RequestController : BaseController
{
private readonly IAvailableActionProvider _actionProvider;
public RequestController(IAvailableActionProvider actionProvider)
{
_actionProvider = actionProvider;
}
public RequestController() : this(new AvailableActionProvider())
{
}
...
}
Then in your action use provider to obtain available actions you can either create new view model than will contain actions or simply put it to ViewBag:
// GET: Requests/CreateHistory
public ActionResult CreateHistory(Status status)
{
History history = new History();
history.Status = status;
ViewBag.AvailableActions = _actionProvider.GetAvailableActions(User, Request, history);
return View(history);
}
And finally in view you can generate list of action based in items in ViewBag.
I hope it helps. Let me know if you have questions about it.
I would advise using claims concurrently with roles. If a role needs access to a resource I will give them a claim to the resource, meaning actionResult. If their role matches the controller, for simplicity reasons, I currently check if they have a claim to the resource. I use the Roles at the controller level so if a Guest or some other account needs anonymous access I can simple add the attribute, but more often than not I should have put it in the correct controller.
Here is some code to show.
<Authorize(Roles:="Administrator, Guest")>
Public Class GuestController
Inherits Controller
<ClaimsAuthorize("GuestClaim")>
Public Function GetCustomers() As ActionResult
Dim guestClaim As Integer = UserManager.GetClaims(User.Identity.GetUserId()).Where(Function(f) f.Type = "GuestClaim").Select(Function(t) t.Value).FirstOrDefault()
Dim list = _customerService.GetCustomers(guestClaim)
Return Json(list, JsonRequestBehavior.AllowGet)
End Function
End Class
Related
Right now, my user table has a bool called Admin. As the code shows, if user.admin = true, the user is able to see the admin area button and access it.
#if (Common.UsuarioLogueado.Admin) {
<li>Admin control panel</li>
}
This is working as intended. However, non admin users can still go to the control panel by accessing it´s url http://localhost/appName/admin/ClientesAdmin/list
How do I prevent such thing? I was thinking about showing an error msg
Going along with the other answers about using Roles, and the AuthorizeAttribute.. which in my opinion is the better way to achieve what you're trying to do, there is another way.
You could just simply redirect the user to another page.. preferable an error page saying you don't have access to the requested page, or just a 401 page which the AuthorizeAttribute would do if you weren't authorized.
Alternate Solution
public class ClientesAdmin : Controller {
// [Authorize(Roles="Admin")] could do it this way
public ActionResult List() {
// or..
if(!Common.UsuarioLogueado.Admin)
{
return new HttpStatusCodeResult(401);
// or
// return View("Error") // usually there is an 'Error' view the Shared folder
}
return View();
}
}
This is not the best solution but I don't know how far along your project is, but simply an alternate solution.
This is how I do it. However your membership system needs to be using ASP.Net Roles for this to work properly.
In your controller you just add the data annotation Authorize. for the function to be accessed by the client, they must be logged in and have the roll specified in the function.
This solution may not be direct cut and paste, but you can see the basic usage then perhaps do a little more research on the Authorize functionality.
public class MyController : Controller {
[Authorize(Roles="Admin")]
public ActionResult AdminIndex() {
return View();
}
[Authorize(Roles = "basic")]
public ActionResult BasicUsersIndex() {
return View();
}
}
Ideally you should be using role based access control. By limiting access by the role, rather than a boolean value in a table you could decorate your CientesAdmin controller with an Authorize Attribute like below.
[Authorize(Roles = "Admin")]
public class CientesAdminController : Controller
{
}
You could also use razor helpers to check if a user IsInRole("Admin").
There is a lot of help on the net to guide you down this path, but if your app is already developed you probably want to stage your changes. Then the recommendation would be to create your own AuthoriseAttribue. Something like.
public class RestrictAccessToAdmins : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//Do the default Authorise Logic (Check if user is loggedin)
base.AuthorizeCore(httpContext);
if (httpContext.User.IsInRole("Admin")) return true;
var id = httpContext.User.Identity.GetUserId();
using (ApplicationDbContext context = new ApplicationDbContext())
{
//Implement you own DB logic here returning a true or false.
return context.Common.First(u => u.userid == id).UsuarioLogueado.Admin;
}
}
}
To use the attribute you'd do the following.
[RestrictAccessToAdmins]
public class CientesAdminController : Controller
{
}
Then over time, with better understanding of the default authorise attribute and a bit of refactoring you could easily change the attribute to below :)
[RestrictAccessToAdmins(Roles = "Admin")]
public class CientesAdminController : Controller
{
}
I have my Action Method
[Authorize(Roles="Admin")]
public ActionResult EditPosts(int id)
{
return View();
}
In my case I need to authorize administrators so they can edit posts but (here comes the cool part), I also need to allow the creator of the post to be able to edit the post which is a normal user. So how can I filter out the user that created the post as well as the admins but leave the others unauthorized? I am receiving the PostEntry id as a route parameter but that's after the attribute and also attributes only accept constant parameters, looks like something very difficult, your answers are highly appreciated, Cheers!
You could write a custom authorize attribute:
public class AuthorizeAdminOrOwnerOfPostAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authorized = base.AuthorizeCore(httpContext);
if (!authorized)
{
// The user is not authenticated
return false;
}
var user = httpContext.User;
if (user.IsInRole("Admin"))
{
// Administrator => let him in
return true;
}
var rd = httpContext.Request.RequestContext.RouteData;
var id = rd.Values["id"] as string;
if (string.IsNullOrEmpty(id))
{
// No id was specified => we do not allow access
return false;
}
return IsOwnerOfPost(user.Identity.Name, id);
}
private bool IsOwnerOfPost(string username, string postId)
{
// TODO: you know what to do here
throw new NotImplementedException();
}
}
and then decorate your controller action with it:
[AuthorizeAdminOrOwnerOfPost]
public ActionResult EditPosts(int id)
{
return View();
}
I understand that you have already accepted an answer, and this was posted a while back.. (btw:excellent answer for adding custom attributes), However I would point out the following:
If you are using this attribute once. On a Single method. This isn't a good implementation. Instead you should have:
[Authorize] // Just make sure they are auth'ed at all.
public ActionResult EditPosts(int id)
{
Post SomePost = findPostByID (id); // However you do it - single lookup of post
if (!user.IsInRole("Admin") && !{IsOwnerOfPost(post)} ) Return Not Authorized
... Edit post code here
}
This has the advantages of:
No additional class that someone will later wonder where it is used.
No class that isn't usable anywhere else (you don't gain reuse with a custom attribute)
Performance is better: Single fetch of the Post
Way easier for someone to read/figure out how it works. No magic code to track down.
And Years later, when HttpContextBase class doesn't exist, or other parts of the tricks used to fetch the Id parameter are gone, the code still works...
I want to add a functionality to application such that only admin can create users and he can provide access to particular pages to user.
He can create roles and can provide users different roles.
I am using Visual Studio 2010 and building this application in MVC3.
Please give me suggestions to make over it.
Thanks in advance.
1.Decorate your user creation and permission setting actions with Authorize attribute
(Notify, that usage of Roles property of AuthorizeAttribute requires implementation of MembershipProvider (standart or custom) and registering it in web.config)
public class AccountController : Controller
{
[HttpGet, Authorize(Roles = "Admin")]
public ViewResult CreateUser()
{
return View();
}
[HttpPost, Authorize(Roles = "Admin")]
public ActionResult CreateUser()
{
//... call service method to create user
}
[HttpPost, Authorize(Roles = "Admin")]
public ActionResult AssignPageToUser(int userId, string controllerName, string ActionName)
{
//... insert record into table (UserPermissions) with attributes (userId, actionName, controllerName)
}
// other methods without decoration by authorize attribute
}
Next paragraphs are correct if you really want to have full control on action permissions separately for each user.
If you think, that your permissions can group in finite and small number on roles - you can decorate all actions/controllers by authorize attribute and specify roles, for which action/controller available: [Authorize("Customer, Manager, RegionalAdmin")] and give admin possibility to assign roles to users. But remember, that in is enough to be in only 1 of listed roles to get access, you can't require by this attribute, for example and Admin, and Manager roles.
If you want to require necessarily more than 1 role, use multiple attributes:
public class MyController:Controller
{
[Authorize(Roles = "Manager")]
[Authorize(Roles = "Admin")]
public ActionResult Action1()
{
//...
}
}
2.For your pages you can create your own filter attribute, inherited from authorize attribute, that will check, if action is available for user (i think you want to assign actions but not views to user).
public UserPermissionRequiredAttribute: AuthorizeAttribute
{
public OnAuthorization(AuthorizationContext filterContext)
{
var isAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;
var userName = filterContext.HttpContext.User.Identity.Name;
var actionName = filterContext.ActionDescriptior.ActionName;
var controllerName = filterContext.ActionDescriptior.ControllerDescriptor.ControllerName;
if (isAuthenticated && myUserActionPermissionsService.UserCanAccessAction(userName, actionName, contollerName)
{
filterContext.Result = HttpUnauthorizedResult(); // aborts action executing
}
}
}
3.Decorate actions (controllers), that accessible for users granted by admin:
MySpecialController: Controller
{
[UserPermissionRequired]
Action1()
{
//...
}
[UserPermissionRequired]
Action2()
{
//...
}
Action3()
{
//...
}
}
I don't recommend to use base controller for that aim, because attribute usage is more flexible (you have control on action/controller level instead of only controller level), it is better way to implement separated responsibility. Base controller and filter attribute usage correlated as polymorphism and switch operator.
You're asking a very broad question, and it would take some time to review all your requirements. In any case, you could start by adding a user property to a controller from which all other controllers inherit. Then, you could interrogate that user instance to determine whether they have access to the current route. This solution should give you the foundation you need to add some administrative views for your business requirements.
public class MegaController
{
protected User CurrentUser { get; set; }
protected override void Initialize(RequestContext context)
{
if (requestContext.HttpContext.User.Identity.IsAuthenticated)
{
var userRepository = new UserRepository();
CurrentUser = userRepository.GetUser(
requestContext.HttpContext.User.Identity.Name);
}
}
}
The User and UserRepository types can be your own design. You could use LINQ To Entities to wrap a table named "User" and then within your controllers, you could have access to any fields in that table.
Then, subclass all controllers from MegaController
public class AdminController : MegaController
{
public ActionResult Action1()
{
return View();
}
}
public class SomeOtherController : MegaController
{
public ActionResult Action1()
{
return View();
}
}
Now, this doesn't completely solve your "admin" issue. To do so, you could include logic in MegaController.Initialize() to interrogate the request information. Once you have the requested route and user in context, your code could make a decision whether to allow the request, redirect it, etc.
protected override void Initialize(RequestContext context)
{
// ...
if(context.HttpContext != null)
{
if(context.HttpContext.Request.Path == "some/restricted/route"
&& CurrentUser.Role != "Admin")
{
// or similar error page
var url = Url.Action("UnAuthorized", "Error");
context.HttpContext.Response.Redirect(url);
}
}
}
One caveat with this method is that any new controllers added to your application would have to inherit from MegaController, an architecture that could be easily missed by future developers on the project.
Read about plain old forms authentication to add support for roles and user management.
Then use the [Authorize(Roles="RoleName1")] on controllers or actions to control access.
Check MvcMembership, also available on Nuget. You will have all the basics for User Management in an ASP.NET MVC 3 site.
You will need a user / role provider. Read this tutorial to learn how to setup a database that will hold your users and roles. Once it is setup, you will have all the stored procedures needed for first setup / manual testing created.
I am attempting to build a system that allows users to perform certain actions, but their account must have a specific 'Ticket' per time they do it. For instance, suppose they wish to create a Product, they would need a CreateProductTicket.
I could simply do this with some 'if' statements, sure, but I want to try a bit more of a robust solution. My structure looks something like this...
interface ITicket<T> where T : ITicketable
{
}
My basic goal is to build an Attribute, perhaps like the following..
public class TicketRequiredAttribute : Attribute
{
public TicketRequiredAttribute(ITicket<T> ticket)
{
if(ticket == null)
return;
}
}
And to be able to decorate Controller or Repository Actions with this. So like ...
ProductsControlller
[TicketRequired(CreateProductTicket)]
public ActionResult CreateProduct(Product product)
{
// ... **I am unsure how to tell if TicketRequired was true or not**
}
Problem 1
I'm not familiar enough with attributes to know how to tell if TicketRequired was 'met' or not. Can anyone enlighten me on this?
Problem 2
The problem I am running into is with database querying. I want to be able to check the user (IMembershipRepository has a GetUser method), but I'm not entirely certain how to do that through an attribute.
Using Castle.Windsor, I have my Dependency Injection set up to inject repositories into controllers. I suppose I could pass the IMembershipRepository through the TicketRequired constructor, but I have a feeling that will become very messy - and extremely unstable. Is there a more logical way to approach this?
You're almost there. You can find more details at http://www.asp.net/mvc/tutorials/understanding-action-filters-cs
I would only use the attribute on the action since the website is where I do all my authorization.
Here is a possible solution. I have not tested this, but it should work. You'll need to verify the way I'm redirecting, not sure if that's the proper way.
public class TicketRequiredActionFilter : ActionFilterAttribute
{
private Type _ticketType;
public TicketRequiredAttribute(Type ticketType)
{
_ticketRequired = ticketType;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
UserServices userServices = GetUserServicesViaDIContainer(); // you'll need to figure out how to implement this
string userId = filterContext.HttpContext.User.Identity.Name
bool hasTicket = userServices.HasTicket(_ticketType, (int)userId); // again, you'll need to figure out the exact implementation
if(!hasTicket)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "Home" }, {"action", "NoPermission" } })
}
else
{
base.OnActionExecuting(filterContext);
}
}
}
In your controller:
[TicketRequiredActionFilter(typeof(CreateProductTicket))]
public ActionResult MyMethod()
{
// do stuff as if the person is authorized and has the ticket
}
If the user doesn't have the ticket, a redirect is issues;, otherwise, continue as normal.
This sounds very much like user roles.
How are you handling the user membership? If your using the built-in asp.net membership you can use roles. So each user will have a certain number of roles in your case one of the will be "CreateProductTicket" then you can decorate your action or controller with the Authorize attribute. Something like:
[Authorize(Roles="CreateProductTicket")]
public ActionResult CreateProduct(Product product)
If a user doesn't have the role or is not authorized then they can access the action.
Maybe I should back-up and widen the scope before diving into the title question...
I'm currently writing a web app in ASP.NET MVC 1.0 (although I do have MVC 2.0 installed on my PC, so I'm not exactly restricted to 1.0) -- I've started with the standard MVC project which has your basic "Welcome to ASP.NET MVC" and shows both the [Home] tab and [About] tab in the upper-right corner. Pretty standard, right?
I've added 4 new Controller classes, let's call them "Astronomer", "Biologist", "Chemist", and "Physicist". Attached to each new controller class is the [Authorize] attribute.
For example, for the BiologistController.cs
[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : Controller
{
public ActionResult Index() { return View(); }
}
These [Authorize] tags naturally limit which user can access different controllers depending on Roles, but I want to dynamically build a Menu at the top of my website in the Site.Master Page based on the Roles the user is a part of. So for example, if "JoeUser" was a member of Roles "Astronomer" and "Physicist", the navigation menu would say:
[Home] [Astronomer] [Physicist]
[About]
And naturally, it would not list links to "Biologist" or "Chemist" controller Index page.
Or if "JohnAdmin" was a member of Role "Admin", links to all 4 controllers would show up in the navigation bar.
Ok, you prolly get the idea... Now for the real question...
Starting with the answer from this StackOverflow topic about Dynamic Menu building in ASP.NET, I'm trying to understand how I would fully implement this. (I'm a newbie and need a little more guidance, so please bare with me.)
The answer proposes Extending the Controller class (call it "ExtController") and then have each new WhateverController inherit from ExtController.
My conclusion is that I would need to use Reflection in this ExtController Constructor to determine which Classes and Methods have [Authorize] attributes attached to them to determine the Roles. Then using a Static Dictionary, store the Roles and Controllers/Methods in key-value pairs.
I imagine it something like this:
public 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
SomeRoleProvider rp = new SomeRoleProvider();
foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
{
}
}
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
}
}
}
Is this doable? If so, how do I perform Reflection in the ExtController constructor to discover the [Authorize] attribute and related Roles (if any)
ALSO! Feel free to go out-of-scope on this question and suggest an alternate way of solving this "Dynamic Site.Master Menu based on Roles" problem. I'm the first to admit that this may not be the best approach.
EDIT
After much reading and experimenting, I came up with my own solution. See below for my answer. Any constructive feedback / criticism welcome!
I prefer linking to everything in my Menus and creating a HtmlHelper which checks to see if a link is accessible or not based on the [Authorize] attributes.
Ok, so I decided to flesh out my own Extended Controller class like I originally proposed. Here is a very basic version. I can see various ways of making this better (extending further, tightening up the code, etc.) but I thought I would offer up my basic results because I imagine there are plenty of other people that want something similar, but might not want all the extras.
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,""));
}
}
}
}
}
To use, just:
Add the [Authorize(Roles="SomeRole1,SomeRole2,SomeRole3,etc."] to the Controller Class
Replace the inherited "Controller" with "ExtController".
For example:
[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : ExtController
{
public ActionResult Index()
{ return View(); }
}
If you don't replace "Controller" with "ExtController", then that Controller won't have a dynamic menu. (This could be useful, in some scenarios, I think...)
In my Site.Master file, I changed the "menu" section to look like 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>
And that's it! :-)
I met the same problem that requires the logic to stay in controller side. But I do like John's approach as it uses the system filter to decide if an action is authorized. In case it helps anyone, the following code removed the HtmlHelper from John's approach:
protected bool HasActionPermission(string actionName, string controllerName)
{
if (string.IsNullOrWhiteSpace(controllerName))
return false;
var controller = GetControllerByName(ControllerContext.RequestContext, controllerName);
var controllerDescriptor = new ReflectedControllerDescriptor(controller.GetType());
var actionDescriptor = controllerDescriptor.FindAction(ControllerContext, actionName);
return ActionIsAuthorized(ControllerContext, actionDescriptor);
}
private static bool ActionIsAuthorized(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
if (actionDescriptor == null)
return false; // action does not exist so say yes - should we authorise this?!
AuthorizationContext authContext = new AuthorizationContext(controllerContext, actionDescriptor);
// run each auth filter until on fails
// performance could be improved by some caching
foreach (var filter in FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor))
{
var authFilter = filter.Instance as IAuthorizationFilter;
if (authFilter == null)
continue;
authFilter.OnAuthorization(authContext);
if (authContext.Result != null)
return false;
}
return true;
}
private static ControllerBase GetControllerByName(RequestContext context, string controllerName)
{
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(context, controllerName);
if (controller == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentUICulture,
"Controller factory {0} controller {1} returned null",
factory.GetType(),
controllerName));
}
return (ControllerBase)controller;
}