MvcSiteMapProvider icon indicating copy to clipboard operation
MvcSiteMapProvider copied to clipboard

Roles based Menu question

Open papyr opened this issue 7 years ago • 5 comments

Hi, I am following you instructions on an ASP MVC 5 (4.6.2) site and trying to understand this comment from Stack Overflow.

"Again, the roles attribute is only for interoperability with ASP.NET and should not be used for pure MVC, since it means you need to duplicate your roles on AuthorizeAttribute anyway."

can you please help, which roles attribute are you referring to, do you mean the extension?

If I implement this with ASP Identity V2.2 MVC 5, and simply enable the security trimming attribute, would that be enough or are you stating this additional extension is needed.

public static class ControllerContextExtensions
{
    public static IEnumerable<string> Roles(this ControllerContext controllerContext)
    {
        var controllerType = controllerContext.Controller.GetType();
        var controllerDescriptor = new ReflectedControllerDescriptor(controllerType);
        var actionName = controllerContext.RouteData.Values["action"] as string;
        var actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);

        var authorizeAttribute = FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor)
            .Where(f => typeof(AuthorizeAttribute).IsAssignableFrom(f.Instance.GetType()))
            .Select(f => f.Instance as AuthorizeAttribute).FirstOrDefault();

        string[] roles = { };
        if (authorizeAttribute != null && authorizeAttribute.Roles.Length > 0)
        {
            roles = Array.ConvertAll(authorizeAttribute.Roles.Split(','), r => r.Trim());
        }

        return roles;
    }
}

papyr avatar Aug 06 '16 19:08 papyr

What I mean is that there is a roles attribute in the .sitemap schema (and also on the ISiteMapNode). This attribute was carried over from Microsoft's ASP.NET implementation and is only meant for use if you have a hybrid MVC/ASP.NET website.

For a pure MVC website, you should do nothing more than configure your security with AuthorizeAttribute or a subclass of it, and then enable security trimming on MvcSiteMapProvider, which picks it up automatically.

NightOwl888 avatar Aug 06 '16 19:08 NightOwl888

@NightOwl888 awesomeness in a chocolate lava cake :+1:

Wow thats simpler with AuthorizeAttribute and security trimming=true, and I do not even need to implement the ControllerContextExtensions, correct?

Can you please recommend / clarify, a component or a way to implement/make the Mapping of Roles to ControllerAction Flexible-Dynamic, so that it could be administered from a View/Stored in config or DB and not having to touch the code on top of every Action Or Controller, (since this static code approach involves a recompile in production and it kills us.)

  • [ ] Some progress I have made is to reflect on the assembly and get the list controllers and actions, but I am stuck on creating the mapping between the Roles and Action Methods
  • [ ] also want to port that functionality to your MVCSiteMap lavacake goodness :yum:

And lastly a request, when I have a multi-tenant scenario is there a way to emit or output or render differently the menu, so it looks different for that client.

thanks

smartmeter avatar Aug 06 '16 19:08 smartmeter

Hi, will this work

@NightOwl888 will this satisfy the need for the filter context

protected override void HandleUnauthorizedRequest(AuthorizationContext filterCtx)
        {
            if (filterCtx.HttpContext.Request.IsAuthenticated)
            {
                filterCtx.Result = new HttpStatusCodeResult(403);
                return;
            }
            base.HandleUnauthorizedRequest(filterCtx);
        }

smartmeter avatar Aug 07 '16 18:08 smartmeter

If you are referring to putting that into a custom AuthorizeAttribute subclass, then yes that will work fine. The only requirement is that the filterCtx.Request be set to a non-null value if authorization fails.

I am not sure what approach is best for your application, as making a "mapping" between roles and actions might not scale well.

One approach is to just make a granular set of roles ("EditProject", "ViewProject", "DeleteProject", etc). But keep in mind the roles are normally set in a cookie and will result in more bandwidth usage if you overdo it (not to mention, there is a limit to how big a cookie can be).

Another would be to make a custom AuthorizeAttribute, override the AuthorizeCore method and return true whenever the authorization succeeds. It might not scale well to hit a database for this, though and keep in mind that MvcSiteMapProvider checks all of your visible Menu actions on each page load when you use the @Html.MvcSiteMap().Menu().

You can register AuthorizeAttribute as a global filter so you don't have to decorate every action method with one.

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new MyAuthorizationAttribute());
            filters.Add(new HandleErrorAttribute());

            FilterProviders.Providers.Insert(0, new GlobalFilterProvider(DependencyResolver.Current));
        }
    }

Note that if you are using DI you can also make a global filter provider so you can control the lifetime of your DbContext using the DI container.

NightOwl888 avatar Aug 07 '16 19:08 NightOwl888

Great thanks! :cake:

I am using this approach by @NightOwl888 - "Another would be to make a custom AuthorizeAttribute, override the AuthorizeCore method and return true whenever the authorization succeeds. It might not scale well to hit a database" just not to glad about the hitting the DB, wish I could cache it, but every users roles change, so it might be necessary.

 public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new MyAuthorizationAttribute());
            filters.Add(new HandleErrorAttribute());

            //no DI for me, so this should do.
            //FilterProviders.Providers.Insert(0, new GlobalFilterProvider(DependencyResolver.Current));
        }
    }

smartmeter avatar Aug 08 '16 03:08 smartmeter