maartenba / MvcSiteMapProvider

An ASP.NET MVC SiteMapProvider implementation for the ASP.NET MVC framework.
Microsoft Public License
538 stars 218 forks source link

How to generate sitemap for not current user? #444

Open thedoctorde opened 8 years ago

thedoctorde commented 8 years ago

Hi! I got a question. I use MvcSiteMapProvider on my site and it works fine! But i cant find solution for the problem: my site have 1 sitemap, but users with different roles (Identity roles) see sitemap in different ways due to security trimming (it is ok). Admin wants to know, what nodes of sitemap is shown to concrete user (with roles A, B, C) or group of users with same roles. Is there any way to render sitemap using only list of roles?

NightOwl888 commented 8 years ago

You can configure the roles on the roles attribute/property of the node. This is generally only recommended for interop with ASP.NET because it essentially means you have to duplicate your roles (once in the AuthorizeAttribute and once in the SiteMap).

<mvcSiteMapNode title="About" controller="Home" action="About" roles="A,B,C">

It is recommended to instead configure the roles on AuthorizeAttribute so your security is defined in only one place in the application. Note that if you need to make advanced role logic, you can subclass AuthorizeAttribute.

[Authorize(Roles = "A,B,C")]
public ActionResult Index()
{
    return View();
}

Also see: http://stackoverflow.com/a/26558590/181087

thedoctorde commented 8 years ago

Thank you for reply! Your link and issue #102 was very helpful!

I found the way how to generate and show sitemap which contains only nodes accessible for users with roles, which I send to Html Helper. Implementation of my intention was forced me to turn off security trimming, create custom visibility provider, change sitemap:

  1. Web.config
<add key="MvcSiteMapProvider_SecurityTrimmingEnabled" value="false" />
<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MyCompanyName.Helpers.SiteMap.MyCustomVisibilityProvider, MyCompanyName" />
  1. Implement custom visibility provider
public class MyCustomVisibilityProvider : SiteMapNodeVisibilityProviderBase
    {
        private Type[] types;
        public MyCustomVisibilityProvider()
        {
            types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes();
        }
        public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
        {
            try
            {
                string visibility = "";
                var vis = node.Attributes["visibility"];
                if (vis != null) visibility = (string) vis;
                if (string.IsNullOrEmpty(visibility))
                {
                    return true;
                }
                visibility = visibility.Trim();
                var mode = sourceMetadata["mode"] as string;
                switch (visibility)
                {
                    case "ClientConstructor":
                    {
                            if (mode == @"ClientConstructor")
                            {
                                var givenRoles = (sourceMetadata["roles"] as string).Split(',').ToList();
                                givenRoles.Add("");
                                var controller = types.FirstOrDefault(x => x.Name.Contains(node.Controller + "Controller"));
                                if (controller != null)
                                {
                                    var controllerAttributes = controller.GetCustomAttributes(typeof(AuthorizeAttribute), true);
                                    var controllerAttrs = (AuthorizeAttribute) controllerAttributes[0];
                                    var controllerRoles = controllerAttrs.Roles.Replace(" ", "").Split(',').ToList();
                                    if (givenRoles.Any(role => controllerRoles.Contains(role)))
                                    {
                                        if (node.Clickable)
                                        {
                                            return true;
                                        }
                                        var childNodes = node.ChildNodes;
                                        return childNodes == null || childNodes.Any(c => c.IsVisible(sourceMetadata));
                                    }
                                }
                                return false;
                            }      
                            return true;
                    }
                }   
            }
            catch {}
            return true;
        }

    }
  1. Add some changes to Mvc.sitemap (add attribute 'visibility="ClientConstructor"' to every node )
<mvcSiteMapNode title="Main" controller="Home" action="Index" visibility="ClientConstructor">
    <mvcSiteMapNode title="Node0" clickable="false" visibility="ClientConstructor">
      <mvcSiteMapNode title="Node1" action="Index" controller="Administrators" area="Admin" visibility="ClientConstructor"/>
      <mvcSiteMapNode title="Node2" action="Index" controller="Users" area="Admin" visibility="ClientConstructor"/>
      <mvcSiteMapNode title="Node3" action="Index" controller="Clients" area="Admin" visibility="ClientConstructor"/>
    </mvcSiteMapNode>
</mvcSiteMapNode>
  1. Configure html helper in View page (@)
@model string // model contains roles divided by ',' 
@Html.MvcSiteMap().Menu(new
{
    name = "MainMenuHelperModel",
    mode = "ClientConstructor",
    roles = @Model
})

So that way I dynamically change sitemap menu on site. One problem that i dont know how to solve: Is there a way to turn on security trimming in runtime? All pages of my site have such helper for building menu: @Html.MvcSiteMap().Menu("MainMenuHelperModel") And I want to make that helper construct sitemap with enabled security trimming.

NightOwl888 commented 8 years ago

Is there a way to turn on security trimming in runtime?

No. And I really don't understand why you would want to, since when you have security trimming enabled it automatically runs the code in the AuthorizeAttribute to see if the user is authorized (which checks whether they are logged in/authenticated).

The way you have made your visibility provider is fine, but it basically means you are duplicating all of the code that is already in the AuthorizeAttribute. If you want to change your security in the future, you have to change both your visibility provider and make a custom AuthorizeAttribute and keep them in sync at all times.

Note that since you are reading the attribute instead of running the filter (which is what actually does the security check) that this approach won't work if you register AuthorizeAttribute globally (which is the recommended way to use AuthorizeAttribute for most applications since new action methods are secure by default). Also, you are missing the check for whether the user is authenticated.

bool isAuthenticated = HttpContext.Current.User.Identity.IsAuthenticated;

If you need Security Trimming to work without actually removing nodes from the API, see #355 for an alternate way to plug in the IAclModule by making it into a visibility provider. Also note that you can register multiple visibility providers per node so you can reuse individual visibility rules as separate pieces that can be combined together in different combinations.

NightOwl888 commented 8 years ago

Is there a way to turn on security trimming in runtime?

No. And I really don't understand why you would want to, since when you have security trimming enabled it automatically runs the code in the AuthorizeAttribute to see if the user is authorized (which checks whether they are logged in/authenticated).

Actually, there is a way to turn it on and off at runtime - see option 2 in this comment on #102. The "special administrative mode" is basically the same thing as toggling security trimming on and off. You would need to set security trimming to true globally, and then you could use this runtime setting to switch on and off the behavior in a wrapper IAclModule.

thedoctorde commented 8 years ago

And I really don't understand why you would want to, since when you have security trimming enabled it automatically runs the code in the AuthorizeAttribute to see if the user is authorized (which checks whether they are logged in/authenticated).

I try to explain it using a screenshot: sitemap

But I found some bugs in my implementation (e.g method's authorize attributes are ignored). I will try to understand how to make "special administrative mode" in my provider.