views:

489

answers:

5

So I was reading another question regarding login loop when you have a user logging in, set to return to a URL which they might not have access to after logging in (ie. an admin page, and the user logs in with a normal account).

The solution under WebForms seems to be to utilize the UrlAuthorizationModule.CheckUrlAccessForPrincipal method. However that does not work for URLs going to Action Methods secured with the Authorize Attribute. I figured I could work out which method the URL was pointing at and reflect over it to solve my problem - but I can't seem to work out how I get this information out of the routing table.

Anyone ever worked with this, or have a solution for this? If I can just get hold of the route information from a URL I think I could work the rest out, but if anyone has a generic solution - ie. some hidden method akin to the before mentioned one for MVC, then that would be totally awesome as well.

I'm not asking how to check if the User has acces to a specified Controller/Action pair. I first and foremost need to work out how to get the Controller/Action pair from the RouteTable based off the URL. The reason for all the background story, is in case that there does indeed exist an equivalent to UrlAuthorizationModule.CheckUrlAccessForPrincipal for MVC.

A: 

This is probably going to sound controversial, but I check security at the beginning of each controller method, inside the method:

public class ProductController : Controller
{
    IProductRepository _repository

    public ActionResult Details(int id)
    {
        if(!_repository.UserHasAccess(id))
            return View("NotAuthorized");

        var item = _repository.GetProduct(id);

        if (item == null)
            return View("NotFound");

        return View(item);
    }
}

The reason I don't use the [Authorize] attributes for this is that you cannot pass an id, or any other identifying information, to the attribute at runtime.

Robert Harvey
Thanks, but I am not sure how this approach would help me - see the comments I left for jfar.
kastermester
+2  A: 

I ported and hacked this code from the MvcSitemap:

public static class SecurityTrimmingExtensions 
{

    /// <summary>
    /// Returns true if a specific controller action exists and
    /// the user has the ability to access it.
    /// </summary>
    /// <param name="htmlHelper"></param>
    /// <param name="actionName"></param>
    /// <param name="controllerName"></param>
    /// <returns></returns>
    public static bool HasActionPermission( this HtmlHelper htmlHelper, string actionName, string controllerName )
    {
        //if the controller name is empty the ASP.NET convention is:
        //"we are linking to a different controller
        ControllerBase controllerToLinkTo = string.IsNullOrEmpty(controllerName) 
                                                ? htmlHelper.ViewContext.Controller
                                                : GetControllerByName(htmlHelper, controllerName);

        var controllerContext = new ControllerContext(htmlHelper.ViewContext.RequestContext, controllerToLinkTo);

        var controllerDescriptor = new ReflectedControllerDescriptor(controllerToLinkTo.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);

        // run each auth filter until on fails
        // performance could be improved by some caching
        foreach (IAuthorizationFilter authFilter in actionDescriptor.GetFilters().AuthorizationFilters)
        {
            authFilter.OnAuthorization(authContext);

            if (authContext.Result != null)
                return false;
        }

        return true;
    }

    private static ControllerBase GetControllerByName(HtmlHelper helper, string controllerName)
    {
        // Instantiate the controller and call Execute
        IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();

        IController controller = factory.CreateController(helper.ViewContext.RequestContext, controllerName);

        if (controller == null)
        {
            throw new InvalidOperationException(

                String.Format(
                    CultureInfo.CurrentUICulture,
                    "Controller factory {0} controller {1} returned null",
                    factory.GetType(),
                    controllerName));

        }

        return (ControllerBase)controller;
    }

It could use some caching but for my case that was a premature optimization.

jfar
My issue is that I have a URL, not a controller and an action. The code you have written here is the stuff I believe I could cook up myself - although thanks for sharing it as I will probably need something like this. But the question is - how do I get Controller + Action extracted out of the RouteTable from a URL?The reason why I wrote alot about the other stuff was because I was suspecting that there might actually be a method to call to tell me all of this in 1 go.
kastermester
@kastermester: To do that, you will have to intercept the request prior to the controller method being executed. For ideas on how to do this, look here: http://stackoverflow.com/questions/2122459/how-malleable-are-the-conventions-in-asp-net-mvc/2122521#2122521
Robert Harvey
Just what I needed. I've been searching high and low for this one. Thanks a lot!
Jan Aagaard
A: 

Why not attribute your controller methods with the security requirement.

I wrote an attribute to do this as follows:

  public class RequiresRoleAttribute : ActionFilterAttribute
        {
            public string Role { get; set; }

            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                if (string.IsNullOrEmpty(Role))
                {
                    throw new InvalidOperationException("No role specified.");
                }


                if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
                {
                    filterContext.HttpContext.Response.Redirect(loginUrl, true);
                }
                else
                {
                    bool isAuthorised = filterContext.HttpContext.User.IsInRole(this.Role);

                    << Complete Logic Here >>



                }  
            }      
        }
RemotecUk
My problem is this: A user goes to my login page. Upon entry a "return" url is set and carried in the query string. Let's suppose for a second that somehow that URL is pointing to an admin part of the site. The user logs in - with a normal account. Now using this kind of code (which I am) the user would be redirected to the login page - but he's already logged in! I could simply redirect to the default page - but then everyone would get there - also unauthenticated people. I would very much like to verify this before redirecting after login.
kastermester
A: 

What is the problem you are trying to solve? It sounds like you may be headed down a path to a complex solution that could use a simple solution instead.

If a user doesn't have permissions to access the page after login, are you wanting non-logged in users to go to one page, while logged in users go to a different page?

If that's the case I might be tempted to create another controller for just such scenarios and redirect to that controller anywhere the user doesn't have access. Or if you are using your own base Controller I would put the functionality there.

Then the controller could present the desired view. For example if a non-logged in user tries to access a page they could get redirected to a generic error page. If the user is logged in, they could get redirected to a not authorized page.

This is very similar to Robert's answer.

Here's a basic skeleton for a base controller.

public BaseController: Controller
{

... // Some code

    public ActionResult DisplayErrorPage()
    {
        // Assumes you have a User object with a IsLoggedIn property
        if (User.IsLoggedIn())    
            return View("NotAuthorized");

        // Redirect user to login page
        return RedirectToAction("Logon", "Account");
    }

}

Then in lets say a AdminController (that inherits from BaseController) action

public ActionResult HighlyRestrictedAction()
{
    // Assumes there is a User object with a HasAccess property
    if (User.HasAccess("HighlyRestrictedAction") == false)
        return DisplayErrorPage();

    // At this point the user is logged in and has permissions
    ...
}
37Stars
I will accept this for now as it seems to be the best solution off the bat for now. I am not certain I will actually use this though. I do really like the idea of not having to have this kind of logic embedded into my controllers - also I'd like to avoid the extra redirect.
kastermester
A: 

In my application I have created a custom filter derived from AuthorizeAttribute, so any unauthorized access will simply go to AccessDenied page. For links, I replace Html.ActionLink with a custom helper Html.SecureLink. In this helper extension, I check for this user's roles access to controller/action against database. If he/she have the authorization, return link otherwise return the link text with special remarks (could be image/ coloring/ js)

Rocky Zairil
I implement many things from this great blog http://www.ryanmwright.com/tag/asp-net-mvc/
Rocky Zairil