Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security Trimmed Node is causing misrendered menu levels #355

Closed
unklegwar opened this issue Sep 10, 2014 · 2 comments
Closed

Security Trimmed Node is causing misrendered menu levels #355

unklegwar opened this issue Sep 10, 2014 · 2 comments

Comments

@unklegwar
Copy link

I have a sitemap with a 4 level structure.

<mvcSiteMapNode title="Home" Description="Home" controller="Home" action="Index"    >
    <mvcSiteMapNode title="Data Management" controller="DataManagement" action="Index" securitycheckchildren="true" section="Main"  >
        <mvcSiteMapNode title="Dictionaries" controller="DataManagement" action="Dictionary" securityfeature="DataManagement:Dictionaries" rolecomponentrollup="DataAccess">
            <mvcSiteMapNode title="Dynamic List" action="Index" dynamicNodeProvider="*Redacted*">
                <mvcSiteMapNode title="Edit Dynamic"   action="Edit" from="grid" preservedRouteParameters="id, parentKey" dynamicNodeProvider="*Redacted*" />
                <mvcSiteMapNode title="Create Dynamic"   action="Create"   dynamicNodeProvider="*Redacted"/>

The scenario that's causing trouble is:

Security Trimming excludes the level 2 node (Dictionaries).
The user accesses the page represented by the level 4 node (specifically the CREATE one).
The menu renders incorrectly.

I render the 2 levels of the menu with:
@Html.MvcSiteMap().Menu(1, 2, true)
and
@Html.MvcSiteMap().Menu(2,1,true)

Problem is, in the scenario above, the first statement renders level 4, and the 2nd statement renders nothing.

image

I would expect to see levels 1 and 2 (minus the trimmed node) as requested, regardless of where the user is currently sitting within the site.

This is similar to a previous issue I submitted, I think.

@NightOwl888
Copy link
Collaborator

This is an unfortunate side effect of copying the security trimming behavior from Microsoft's original ASP.NET sitemap provider design. In ASP.NET, security was configured using directories so it was not possible to access a page if the user didn't have access to one of the directories above it, so Microsoft decided to replicate this behavior throughout the sitemap provider. In MVC that is no longer the case - security is handled on each action individually, but the same limitation is now still there in MvcSiteMapProvider and the realization that it was over-restrictive for MVC wasn't reached until long after the v4 release.

This behavior is something that cannot be changed until the next major version of MvcSiteMapProvider. However, as a workaround you can stop using security trimming and instead use the IAclModule in a custom visibility provider to take advantage of the VisibilityAffectsDescendants behavior. When you set VisibilityAffectsDescendants to false, it will toggle the visibility of each node on or off regardless of the visibility of the ancestor nodes.

I have created a demo project showing how this can be done. Note that you must use external DI for this to work.

First, create a visibility provider that accepts an IAclModule through its constructor.

using System;
using System.Collections.Generic;
using MvcSiteMapProvider;
using MvcSiteMapProvider.Security;
using MvcSiteMapProvider.Web.Mvc;
using MvcSiteMapProvider.Web.Mvc.Filters;

public class AclModuleVisibilityProvider
    : SiteMapNodeVisibilityProviderBase
{
    public AclModuleVisibilityProvider(
        IAclModule aclModule
        )
    {
        if (aclModule == null)
            throw new ArgumentNullException("aclModule");

        this.aclModule = aclModule;
    }
    private readonly IAclModule aclModule;

    public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
    {
        return this.aclModule.IsAccessibleToUser(node.SiteMap, node);
    }
}

Next, make sure both the securityTrimmingEnabled and visibilityAffectsDescendants settings are set to false in the DI module.

            bool enableLocalization = true;
            string absoluteFileName = HostingEnvironment.MapPath("~/Mvc.sitemap");
            TimeSpan absoluteCacheExpiration = TimeSpan.FromMinutes(5);
            bool visibilityAffectsDescendants = false; // <- Set to false
            bool useTitleIfDescriptionNotProvided = true;





            bool securityTrimmingEnabled = false; // <- Set to false
            string[] includeAssembliesForScan = new string[] { "MvcSiteMapProvider_355" };

Finally, use explicit DI to inject the visibility providers, so you can bundle each one with a AclModuleVisibilityProvider instance. This will ensure the IAclModule will still function everywhere.

// Visibility Providers

// Explicitly set the visibility providers, using CompositeSiteMapNodeVisibilityProvider to combine the AclModuleVisibilityProvider
// with all other ISiteMapNodeVisibilityProvider implementations.
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
    .EnumerableOf<ISiteMapNodeVisibilityProvider>().Contains(x =>
        {
            x.Type<CompositeSiteMapNodeVisibilityProvider>()
                .Ctor<string>("instanceName").Is("aclAndFilter")
                .EnumerableOf<ISiteMapNodeVisibilityProvider>().Contains(y =>
                    {
                        y.Type<AclModuleVisibilityProvider>();
                        y.Type<FilteredSiteMapNodeVisibilityProvider>();
                    });

            // TODO: Add additional combined visibility logic if using custom providers, as shown.
            //x.Type<CompositeSiteMapNodeVisibilityProvider>()
            //    .Ctor<string>("instanceName").Is("aclAndMyCustom")
            //    .EnumerableOf<ISiteMapNodeVisibilityProvider>().Contains(y =>
            //    {
            //        y.Type<AclModuleVisibilityProvider>();
            //        y.Type<MyCustomVisibilityProvider>();
            //    });
        })
    .Ctor<string>("defaultProviderName").Is("aclAndFilter");

If using custom visibility providers, make sure you bundle each one with a AclModuleVisibilityProvider instance as shown above and give it a unique instanceName. You can then use the instanceName to override the defaultProviderName from your nodes.

<mvcSiteMapNode title="About" controller="Home" action="About" visibilityProvider="aclAndMyCustom"/>
<mvcSiteMapNode title="Contact" controller="Home" action="Contact" visibilityProvider="aclAndFilter"/>
<!-- Implicitly use the default provider name (in this case aclAndFilter) -->
<mvcSiteMapNode title="Something" controller="Home" action="Something" />

@NightOwl888
Copy link
Collaborator

Did this solution work for you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants