Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apply an action filter to every controller in only one part of an ASP.NET MVC site?

Tags:

asp.net-mvc

I've seen a great answer to a similar question which explains, by inheriting all controllers from a new base class decorated with your own ActionFilter attribute, how you could apply some logic to all requests to your site.

I'd like to find a way to do that based on the area of a site my user is visiting.

For example, I will have a Product controller with a View action but I want to allow that to be used for the two following urls:

/Product/View/321 - display product id 321 to 'normal' users /Admin/Product/View/321 - use the same View controller but spit out extra functionality for my admin users.

I could pass "admin" in as a parameter named "user" into my view action on my product controller to show extra information for administrators, a method for doing that is shown here. But what I'd then need to do is confirm my user was allowed to view that url. I don't want to decorate my Product controller with an ActionAttribute that checks for authentication because when unauthenticated users (and logged in administrators) view it at /Product/View/321, I want them all to see the standard view.

So what I'd like to do, is described below in pseudo-code:

When a url in the format "{userlevel}/{controller}/{action}/{id}" is called, I'd like like to call another controller that does the authentication check and then 'chain' to the original {controller} and pass through the {action}, {id} and {userlevel} properties.

How would I do that?

(I know that the over-head for doing a check on every call to the controller is probably minimal. I want to do it this way because I might later need to do some more expensive things in addition to user authentication checks and I'd prefer to only ever run that code for the low-traffic admin areas of my site. There seems no point to do these for every public user of the site)

like image 526
Neil Trodden Avatar asked May 25 '09 23:05

Neil Trodden


2 Answers

At first I thought this might be as simple as adding a new route like this:

routes.MapRoute(
    "Admin",
    "Admin/{*pathInfo}",
    new { controller="Admin", action="Index", pathInfo="" }
    );

and then have a controller something like this:

public class AdminController : Controller
{
    public ActionResult Index(string pathInfo)
    {
        //Do admin checks, etc here....
        return Redirect("/" + pathInfo);
    }
}

However, unfortunately all the options you have available in order to do the redirect (i.e. Redirect, RedirectToAction & RedirectToRoute) all do a 302 style redirect. Basically this means that your /Admin/Product/Whatever will execute & then bounce back to the browser telling it to redirect to /Product/Whatever in a totally new request, which means you've lost your context. I don't know of a clean way of keeping the redirect server side (i.e. like a Server.Transfer of old), apparently neither does the SO community...

(obviously, this is a non-solution, since it doesn't solve your problem, but I thought I'd put it here anyway, in case you could use the ideas in some other way)


So, what's an actual solution to the problem then? Another idea is to use an ActionFilter (yes I know you said you didn't want to do so, but I think the following will serve your purposes). Add a new route like this:

routes.MapRoute(
    "Admin",
    "Admin/{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = "", userLevel = "Admin" }
    );

and then add an ActionFilter like this (that you could apply to all requests via a base controller object as you mentioned):

public class ExtendedAdminViewAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        object userLevel = filterContext.RouteData.Values["userLevel"];
        if (userLevel != null && userLevel.ToString() == "Admin")
        {
            //Do your security auth checks to ensure they really are an admin
            //Then do your extra admin logic...
        }
    }
}

So although it is using an ActionFilter that will apply to all requests, the only extra work done in most normal cases (i.e. a request for /Product/Whatever), is a single check of that bit of route data (userLevel). In other words, you should really see a performance hit for normal users since you're only doing the full auth check and extra admin work if they requested via /Admin/Product/Whatever.

like image 116
Alconja Avatar answered Nov 15 '22 12:11

Alconja


1) Can't you just check for the role within the view?

<% if (HttpContext.Current.User.IsInRole ("Administrator")) { %>
  // insert some admin specific stuff here
  <%= model.ExtraStuff %>
% } %>

You can perform the same check in the controller if you need to set admin specific view model properties. In your controller you can do your extra processing only when the user is already authenticated:

public ActionResult Details (int productId)
{
  ProductViewModel model = new ProductViewModel ();

  if (User.Identity.IsAuthenticated && User.IsInRole ("Administrator"))
  {
    // do extra admin processing
    model.ExtraStuff = "stuff";
  }

  // now fill in the non-admin specific details
  model.ProductName = "gizmo";

  return View (model);
}

The only thing missing here is a redirect to your login page when an admin tries to access the view without being authenticated.

2) Alternatively if you want to reuse your default product view with some extra bits you could try the following:

public class AdminController
{
  [Authorize(Roles = Roles.Admin)]
  public ActionResult Details(int productId)
  {
    ProductController productController = new ProductController(/*dependencies*/);

    ProductViewModel model = new ProductViewModel();
    // set admin specific bits in the model here
    model.ExtraStuff = "stuff";
    model.IsAdmin = true;

    return productController.Details(productId, model);
  }
}

public class ProductController
{
  public ActionResult Details(int productId, ProductViewModel model)
  {
    if (model == null)
    {
        model = new ProductViewModel();      
    }

    // set product bits in the model

    return Details(model);
  }
}

NOTE: I would prefer solution 1) over 2) due to the fact that you need to create a new instance of ProductController and that brings up it's own set of issues especially when using IoC.

like image 32
Todd Smith Avatar answered Nov 15 '22 13:11

Todd Smith