MvcSiteMapProvider icon indicating copy to clipboard operation
MvcSiteMapProvider copied to clipboard

Problem creating menu

Open Tabgyn opened this issue 8 years ago • 7 comments

I do not know if it's really a problem. In my application I use the MvcSiteMapProvider to create my menu. Items that will be shown on the menu are in accordance with the permissions of each user. It turns out that the items displayed are always from the last User who accessed the application. For example, two users access the application, each with different permissions, the first with access to A, B, C and D. The second User then accesses the application with access to items A, B and C. The first updating the page goes to show then only the items A B and C.

This is a problem? Something related to cache? Or what?

Tabgyn avatar Mar 07 '16 18:03 Tabgyn

There is no caching involved with security trimming. You might be running into issues if you are using some sort of caching in a custom AuthorizeAttribute or if your security provider is using caching, but you didn't mention how you have your application and/or MvcSiteMapProvider configured.

Could you explain exactly how you have your security configured? It would be preferable if you could build a small demo application and either post it on GitHub or make it available for download.

There were shared caching issues with MvcSiteMapProvider v3 and before that could be the cause of the issue if you are using an older version.

NightOwl888 avatar Mar 07 '16 19:03 NightOwl888

I'm using version 4.6.20

My Dynamic provider

public class MyDynamicNodeProvider : IDynamicNodeProvider
    {
        private readonly SGComprasContext _context = new SGComprasContext();

        public IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
        {
            var usuarioLogado = ((MyPrincipal)HttpContext.Current.User);
            var permissoes = _context.Usuarios.Include(u => u.Perfil)
                .Include(u => u.Perfil.Permissoes)
                .First(u => u.Id == usuarioLogado.Id).Perfil.Permissoes;
            var nodeList = new List<DynamicNode>();

            foreach (var permissao in permissoes)
            {
                var dNode = new DynamicNode
                {
                    Key = permissao.Id.ToString(),
                    ParentKey = permissao.ParenteId.ToString(),
                    Title = permissao.Nome,
                    Description = permissao.Descricao,
                    Url = permissao.Url,
                    Action = permissao.Acao,
                    Controller = permissao.Controle,
                    Order = permissao.Ordem
                };

                //Quando for CRUD
                if (permissao.EhCRUD)
                {
                    var cList = permissao.Atributos.Split(';');
                    dNode.PreservedRouteParameters = cList;
                }

                dNode.Attributes.Add("icon", permissao.ClasseIcone);
                dNode.Attributes.Add("visibility", permissao.MostrarNoMenu ? string.Empty : "!MenuHelper");

                nodeList.Add(dNode);
            }

            return nodeList;
        }

        public bool AppliesTo(string providerName)
        {
            if (string.IsNullOrEmpty(providerName))
                return false;

            return GetType() == Type.GetType(providerName, false);
        }
    }

The nodes of the menu are obtained according to the user permissions.

MVC.sitemap

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
            xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">
  <mvcSiteMapNode key="0" title="root" clickable="false">
    <mvcSiteMapNode title="" dynamicNodeProvider="SGCompras.Web.Cliente.MyDynamicNodeProvider, SGCompras.Web.Cliente"/>
  </mvcSiteMapNode>
</mvcSiteMap>

in web.config

...
<appSettings>
    <add key="webpages:Version" value="3.0.0.0"/>
    <add key="webpages:Enabled" value="false"/>
    <add key="ClientValidationEnabled" value="true"/>
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
    <add key="MvcSiteMapProvider_CacheDuration" value="5" />
    <add key="MvcSiteMapProvider_SiteMapFileName" value="~/Mvc.sitemap"/>
    <add key="MvcSiteMapProvider_EnableLocalization" value="false"/>
    <add key="MvcSiteMapProvider_VisibilityAffectsDescendants" value="false"/>
    <add key="MvcSiteMapProvider_AttributesToIgnore" value="icone"/>
    <add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider"/>
  </appSettings>
...

So I call on _Layout.cshtml

@Html.MvcSiteMap().Menu(false)

I made some changes to the MenuHelperModel.cshtml

@model MenuHelperModel

<ul class="nav sidebar-menu">
    @ShowMenu(Model.Nodes)
</ul>

@helper ShowMenu(IEnumerable<SiteMapNodeModel> menuItems)
{
foreach (var node in menuItems)
{
    var nodeclass = "";
    if (node.IsInCurrentPath)
    {
        nodeclass = "active";
    }
    if (node.Children.Any(n => n.IsInCurrentPath))
    {
        nodeclass = "active open";
    }
    else if (node.Children.Any())
    {
        foreach (var c in node.Children)
        {
            if (c.Children.Any())
            {
                if (c.Children.Any(n => n.IsInCurrentPath))
                {
                    nodeclass = "active open";
                }
            }
        }
    }
    <li class="@(!string.IsNullOrEmpty(nodeclass) ? Html.Raw(nodeclass) : null)">
        @if (node.Children.Any())
        {
            <a class="menu-dropdown" href="@(!string.IsNullOrEmpty(node.Url) ? Html.Raw(node.Url) : Html.Action(node.Action, node.Controller))">
                @if (!string.IsNullOrEmpty(node.Attributes["icon"].ToString()))
                {
                    <i class="menu-icon @Html.Raw(node.Attributes["icon"])"></i>
                }
                <span class="menu-text">@Html.Raw(node.Title)</span>
                <i class="menu-expand"></i>
            </a>
        }
        else
        {
            <a href="@(!string.IsNullOrEmpty(node.Url) ? Html.Raw(node.Url) : Html.Action(node.Action, node.Controller))">
                @if (!string.IsNullOrEmpty(node.Attributes["icon"].ToString()))
                {
                    <i class="menu-icon @Html.Raw(node.Attributes["icon"])"></i>
                }
                <span class="menu-text">@Html.Raw(node.Title)</span>
            </a>
        }
        @if (node.Children.Any())
        {
            <ul class="submenu">
                @ShowMenu(node.Children)
            </ul>
        }
    </li>
}
}

My custom AuthorizedAttribute

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
    public class MyAuthorizedAttribute : AuthorizeAttribute
    {
        private readonly SGComprasContext _context = new SGComprasContext();

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            var authorized = base.AuthorizeCore(httpContext);
            if (!authorized)
            {
                // The user is not authenticated
                return false;
            }

            var usuarioLogado = httpContext.User as MyPrincipal;

            if (usuarioLogado == null)
            {
                return false;
            }

            if (usuarioLogado.EhAdmin)
            {
                return true;
            }

            var acao = httpContext.Request.RequestContext.RouteData.Values["action"].ToString();
            var controle = httpContext.Request.RequestContext.RouteData.Values["controller"].ToString();
            var permissoes =
                _context.Usuarios.Include(u => u.Perfil)
                    .Include(u => u.Perfil.Permissoes)
                    .First(u => u.Id == usuarioLogado.Id).Perfil.Permissoes;

            authorized = permissoes.Any(permissao => permissao.Controle == controle && permissao.Acao == acao);

            return authorized;
        }
    }

Tabgyn avatar Mar 07 '16 19:03 Tabgyn

Your main issue is that dynamic node providers (despite the name) are not dynamically loaded per request. The data is loaded into the SiteMap, cached, and shared between all users. It would not be valid to load a dynamic node provider with nodes for a specific user.

Think of the SiteMap as sort of a hierarchal database. You must put all of the data you intend to use there. Then you can use visibility providers, security trimming, HTML helper templates, and/or custom HTML helpers to make views of the specific nodes you want to see in the page.

So, you should fix your dynamic node provider so it doesn't filter out any nodes per user. Then as long as you have setup your MyAuthorizedAttribute globally, on controllers, or on actions the only other thing you need to do is enable security trimming.

NightOwl888 avatar Mar 07 '16 20:03 NightOwl888

Related: http://stackoverflow.com/questions/26541338/load-an-xml-sitemap-into-mvcsitemapprovider-based-on-user-role

NightOwl888 avatar Mar 07 '16 20:03 NightOwl888

Okay, I understand that the function of the tool is not to control access. So by what you said, the right thing to do is to get all nodes in dynamicprovider and then control the node's visibility in a visibilityprovider.

Tabgyn avatar Mar 08 '16 13:03 Tabgyn

Yes, that is one way to do it.

Normally, when we are referring to user authorization, you can just enable security trimming, but since your AuthorizeAttribute is data-driven with no session caching that could be slow. I would recommend trying it (since it is far simpler) first, and if it is too slow try either storing the permissions in session state or using a visibility provider that does the same.

A possible alternative to session state would be to use caching with a key that is user-based, but session state will scale to server farms easier by using out of process session state.

NightOwl888 avatar Mar 08 '16 19:03 NightOwl888

Thank @NightOwl888. As always you are saving lives. As soon as possible I will study about session cache.

Tabgyn avatar Mar 09 '16 11:03 Tabgyn