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
I wanted to provide some URL separation for my public/anonymous controllers and views from the admin/authenticated controllers and views. So I ended up using entirely Attribute Routing in order to take more control of my URLs. I wanted my public URLs to start with "~/Admin/etc." while my public URLs would not have any such prefix.
Public Controller (one of several)
[RoutePrefix("Home")]
public class HomeController : Controller
{
[Route("Index")]
public ActionResult Index()
{ //etc. }
}
Admin Controller (one of several)
[RoutePrefix("Admin/People")]
public class PeopleController : Controller
{
[Route("Index")]
public ActionResult Index()
{ //etc. }
}
This allows me to have public URLs such as:
http://myapp/home/someaction
...and admin/authenticated URLs such as:
http://myapp/admin/people/someaction
But now I want to do some dynamic stuff in the views based on whether the user is in the Admin section or the Public section of the site. How can I access this programmatically, properly?
I know I could do something like
if (Request.Url.LocalPath.StartsWith("/Admin"))
...but it feels "hacky." I know I can access the controller and action names via
HttpContext.Current.Request.RequestContext.RouteData.Values
...but the "admin" piece isn't reflected in there, because it's just a route prefix, not an actual controller name.
So, the basic question is, how do I programmatically determine whether the currently loaded view is under the "admin" section or not?
You just need to reflect the RoutePrefixAttribute from the Controller type, and then get its Prefix value. The Controller instance is available on the ViewContext.
This example creates a handy HTML helper that wraps all of the steps into a single call.
using System;
using System.Web.Mvc;
public static class RouteHtmlHelpers
{
public static string GetRoutePrefix(this HtmlHelper helper)
{
// Get the controller type
var controllerType = helper.ViewContext.Controller.GetType();
// Get the RoutePrefix Attribute
var routePrefixAttribute = (RoutePrefixAttribute)Attribute.GetCustomAttribute(
controllerType, typeof(RoutePrefixAttribute));
// Return the prefix that is defined
return routePrefixAttribute.Prefix;
}
}
Then in your view, you just need to call the extension method to get the value of the RoutePrefixAttribute.
#Html.GetRoutePrefix() // Returns "Admin/People"
I am relatively new to the MVC way of doing things and have run into a bit of a performance issue due to loading a lot of extraneous data in a base controller. I have read a bit about action filters and was wondering if anyone had advice on how to proceed. Here is my issue:
I have a Controller called RegController that inherits from a base Controller called BaseController. In the onActionExectuting method of the base controller I load several variables, etc. that will be used in a visible page such as viewbag, viewdata, etc. I only need to load this stuff for Actions with the result type ActionResult. In the same RegController I also have some JsonResult Actions that do not need all of this extra info loaded because they do not load a page or view template they only return Json. Does anyone have a suggestion how to handle this properly within existing framework features? Should I be putting these action in different controllers instead? I have many controllers with varying functionality split up in this same way and am hoping I can filter the action type versus moving them all to a JsonResultController or whatever.
Code (some of it pseudo) below:
public class BaseController : Controller
{
protected string RegId;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
if (filterContext.ActionParameters.ContainsKey("rid") && filterContext.ActionParameters["rid"] != null)
RegId = filterContext.ActionParameters["rid"].ToString();
Reg oReg = new Reg(RegId);
// IF A VISIBLE PAGE IS BEING SERVED UP, I NEED TO LOAD THE VIEWDATA FOR THE PAGE
SetViewAndMiscData(oReg);
// IF THIS IS A JSON RESULT ACTION, I DO NOT WANT TO LOAD THE VIEWDATA FOR THE PAGE
}
}
public class RegController : BaseController
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
}
public ActionResult Page(string id)
{
// THE VIEW DATA SHOULD BE SET FROM THE BASECONTROLLER FOR USE IN THE PAGE
return View();
}
public JsonResult Save(string id, string data)
{
// I DON"T NEED ALL OF THE VIEW AND MISC DATA LOADED HERE, INCLUDING THE AUTO LOAD
// OF THE REG OBJECT SINCE I DO IT HERE FROM THE PASSED PARAMS.
GenericResponseObject Response = new GenericResponseObject();
Reg oReg = new Reg(id);
if (!oReg.IsValid)
{
Response.Status = 1;
Response.Message = "Invalid Record";
}
else
Response = oReg.SaveData(data);
return Json(Response);
}
}
I have this error:
Error executing child request for handler 'System.Web.Mvc.HttpHandlerUtil+ServerExecuteHttpHandlerAsyncWrapper'.
with inner exception:
Child actions are not allowed to perform redirect actions.
Any idea why this happening?
Incidentally, the error is happening on this line:
#Html.Action("Menu", "Navigation")
The Menu Action in the Navigation Controller looks like this:
public ActionResult Menu()
{
return PartialView();
}
This happened to me because I had [RequireHttps] on the Controller, and a child action was called from a different controller. The RequireHttps attribute caused the redirect
This is not allowed because MVC has already started Rendering the View to the browser (client).
So the MVC Frameworks blocks this, because the client already receives data (html). As long as the rendering is in progress you not able to redirect in your child view.
You can return RedirectToAction instead.
Instead of #Html use #Url.
Before: #Html.Action("Menu", "Navigation")
After: #Url.Action("Menu", "Navigation")
I had same situation like Doug described above
My solution:
1)Created custom Controller Factory. It's need for getting ControllerContext in my custom https attribute.
public class CustomControllerFactory : DefaultControllerFactory
{
public override IController CreateController(RequestContext requestContext, string controllerName)
{
var controller = base.CreateController(requestContext, controllerName);
HttpContext.Current.Items["controllerInstance"] = controller;
return controller;
}
}
}
2)In Application_Start function from Global.asax file wrote next:
ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
3)Defined custom https attribute:
public class CustomRequireHttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
public bool RequireSecure = false;
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
{
if (RequireSecure && !((Controller)HttpContext.Current.Items["controllerInstance"]).ControllerContext.IsChildAction)
{
base.OnAuthorization(filterContext);
}
}
}
4)Using new attribute for definition of account controller:
[CustomRequireHttps]
redirect like this
string returnUrl = Request.UrlReferrer.AbsoluteUri;
return Redirect(returnUrl);
instead of
return redirect("Action","Controller")
remove the [NoDirectAccess] annotation if added in the controller.
and in controller for the partial view
return PartialView() instead of return View()
This happens in cases where the Action called by the Html.Action performs a redirection such as
return RedirectToAction("Error", "Home");
instead of returning the desired partialView
So the solution would be to remove the redirection.
I have a problem with my custom RouteBase implementation and [OutputCache].
We have a CMS in which urls are mapped to certain content pages. Each type of content page is handled by a different controller (and different views). The urls are completely free and we want different controllers, so a "catchall" route is not usable. So we build a custom RouteBase implementation that calls the database to find all the urls. The database knows which Controller and Action to use (based on the content page type).
This works just great.
However, combining this with the [OutputCache] attribute, output caching doesn't work (the page still works). We made sure [OutputCache] works on our "normal" routes.
It is very difficult to debug outputcaching, the attribute is there we us it, it doesn't work... Ideas how to approach this would be very welcome, as would the right answer!
The controller looks like this:
public class TextPageController : BaseController
{
private readonly ITextPageController textPageController;
public TextPageController(ITextPageController textPageController)
{
this.textPageController = textPageController;
}
[OutputCache(Duration = 300)]
public ActionResult TextPage(string pageid)
{
var model = textPageController.GetPage(pageid);
return View(model);
}
}
The custom route looks like this:
public class CmsPageRoute : RouteBase
{
private IRouteService _routeService;
private Dictionary<string, RouteData> _urlsToRouteData;
public CmsPageRoute(IRouteService routeService)
{
this._routeService = routeService;
this.SetCmsRoutes();
}
public void SetCmsRoutes()
{
var urlsToRouteData = new Dictionary<string, RouteData>();
foreach (var route in this._routeService.GetRoutes()) // gets RouteData for CMS pages from database
{
urlsToRouteData.Add(route.Url, PrepareRouteData(route));
}
Interlocked.Exchange(ref _urlsToRouteData, urlsToRouteData);
}
public override RouteData GetRouteData(System.Web.HttpContextBase httpContext)
{
RouteData routeData;
if (_urlsToRouteData.TryGetValue(httpContext.Request.Path, out routeData))
return routeData;
else
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
private RouteData PrepareRouteData(ContentRouteData contentRoute)
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", contentRoute.Controller);
routeData.Values.Add("action", contentRoute.Action);
routeData.Values.Add("area", contentRoute.Area);
routeData.Values.Add("pageid", contentRoute.Constraints["pageid"]); // variable for identifying page id in controller method
routeData.DataTokens.Add("Namespaces", new[] { contentRoute.Namespace });
routeData.DataTokens.Add("area", contentRoute.Area);
return routeData;
}
// routes get periodically updated
public void UpdateRoutes()
{
SetCmsRoutes();
}
}
Thank you for reading until the end!
In the end we tracked it down to a call to
... data-role="#this.FirstVisit" ...
in our _Layout.cshtml
This called a property on our custom view page that in turn called a service which always set a cookie. (Yikes setting cookies in services!, we know!)
Had it not been Friday and at the end of the sprint we might have noticed the cache busting
Cache-Control: no-cache="Set-Cookie": Http Header.
I still don't understand why this only busted the cache for our custom RouteBase implementation and not all pages. All pages use the same _Layout.cshtml.
You can try the following code if it helps
[OutputCache(Duration = 300, VaryByParam="*")]
public ActionResult TextPage(string pageid)
{
var model = textPageController.GetPage(pageid);
return View(model);
}
You might look here to customize caching :
Ignore url values with Outputcache
Output Cache problem on ViewEngine that use 2 separate view for 1 controller
Also, check the cache location.