-
Notifications
You must be signed in to change notification settings - Fork 218
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
How to explore all nodes regardless the security trimming ? #102
Comments
There is support for multiple sitemaps in current build. |
I think this functionality is inherently buggy (probably because of StaticSiteMapProvider): two sitemaps cannot have the same node URL. So, it can't work for this scenario. If I'm wrong, I thank any good soul to illustrate with an example to the contrary ... |
In v4 we have reworked the way sitemaps are implemented (no longer dependent on System.Web.SiteMapProvider), so you should now be able to pull up 1 sitemap with security trimming enabled and 1 with it disabled so you can see all nodes. Security trimming can be turned on and off at the XML file level now, so in order to do this you will need to inherit XmlSiteMapBuilder and override the SetEnableLocalization method (or alternatively create your own ISiteMapBuilder implementation). There are a few hoops you need to jump through to make the current request map to more than one sitemap, so I am interested to see if this specific use case is covered by the new design. |
Hi, Thanks very much for your explanation. I'm always interested by this use case, so i'l take a look at v4 as soon as possible (probably not this week though). Actually, I do not want to map the current request on multiple sitemaps. I want to use this functionality in an admin area. I need some sort of logic to browse the sitemap tree (only one tree) regardless of current user rights. The goal is not to provide a kind of menu linked to the sitemap nodes. I want to allow an admin user to administer some aspects tied to the sitemap nodes. Thanks again for your time |
Are you saying that you want to be able to administer the position of nodes within the SiteMap from the Administration screen? If so, that is the exact scenario I had in mind when I came up with this design. There is a working demo of what I would like to achieve here: http://www.bigace.de/demo.html. Have a look at the v3 administration interface and go to Pages > Administration from the menu. The pane on the left allows for creating and moving of pages. All changes are reflected in the main menu of the site and the site map path. Unfortunately, all of that is done in PHP (except for the drag and drop part, which I plan to utilize). There is now a new interface, ISiteMapBuilder that allows you to build a site map from any source, and a couple of interfaces to determine which set of builders belong to a given request. The per request logic can be based on URL, cookie, user session, or whatever else you can imagine. Essentially, you can now implement whatever logic is necessary to enable this scenario and inject it. |
Not exactly. I have a project with user rights management by application. An application is represented by a node in the sitemap. Actually, the project is an intranet. I want to allow administrators to manage some rights based on application scope. So I use the sitemap to discover all applications. But the admin not necessarily has access to all nodes. It's purely a functionality needed for the work of a controller. It's not used to present some links to the user. Your scenario is interesting too. It is a core functionality needed by any CMS. |
I see. Well, the core hasn't changed that much from Microsoft's model. The IsAccessibleToUser() method controls which nodes are available in the tree. Currently, the check for security trimming is the only thing in there. The rest is injected as an IAclModule (which can be multiple with the composite pattern). Anyway, I don't see any reason why some sort of switch can't be put in the IAclModule to enable all (or some) of the nodes under a specific condition so they can be used from the public interface. Well, when you have time take a look and let us know what you think. |
Yes, I will. Most probably before end of this month. |
FYI - If you haven't looked already, v4 is now available on the public Nuget feed as a prerelease. |
Hi, Thanks for this news. I haven't tried yet, the project on which I had this issue is closed now. I just tried on a sample web project, and I probably did something wrong because I didn't manage to enable security trimming. What I've done :
When I run the site, the sitemap shows all pages, regardless the "role" XML attribute and the "AuthorizeAttribute". I can navigate to "Page2" (protected with a role XML attribute), but I can't access to "Page3" (protected with AuthorizeAttribute). What did I do wrong ? This new release looks promising. Thanks for this very usefull project. |
Ahh, I see you are still in v3 mode :). First of all, we have cut ties with the ASP.NET sitemap provider model completely - it was impossible to work around that for multi-tenant applications because of its dependency on web.config. So, you can completely remove the You are probably now wondering how to configure the provider if not in that element. If not using an external DI setup, many of those settings have been moved to the appSettings section and correspond exactly with the settings in v3. <appSettings>
<add key="MvcSiteMapProvider_UseExternalDIContainer" value="false"/>
<add key="MvcSiteMapProvider_SiteMapFileName" value="~/Mvc.sitemap"/>
<add key="MvcSiteMapProvider_ScanAssembliesForSiteMapNodes" value="false"/>
<add key="MvcSiteMapProvider_ExcludeAssembliesForScan" value=""/>
<add key="MvcSiteMapProvider_IncludeAssembliesForScan" value=""/>
<add key="MvcSiteMapProvider_AttributesToIgnore" value=""/>
<add key="MvcSiteMapProvider_CacheDuration" value="5"/>
<add key="MvcSiteMapProvider_ExcludeNamespacesForResolver" value=""/>
<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value=""/>
</appSettings>
All of these settings are completely optional. If not provided, the defaults are exactly what is configured above. Note that if no DefaultSiteMapNodeProvider is configured, visibility will always be true. If you are using an external DI container, the You probably also are wondering how to set the securityTrimmingEnabled setting. That is now a part of the Mvc.sitemap schema - you have to add it to the <mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
enableLocalization="true"
securityTrimmingEnabled="true"> Actually, it looks like that is what you are missing here. However, we'd appreciate if you report anything else that looks like it doesn't work as there has been a lot of major refactoring (which is why we are still in beta). |
wow ok :-) I thought to load multiple instances of the same sitemap file, one with security trimming enabled, another one without security trimming. Now, with the new architecture, I suppose I can load the sitemap regardless of the security trimming. I have this :
The sitemap menu is correctly trimmed in regard of security. Is there any documentation to learn about the new architecture ? |
You are correct, in order to walk the sitemap without security trimming, you can use a second sitemap tree with security trimming disabled. To accomplish this, you will need to do a couple of things. First of all, note that this is an advanced scenario that will require an external DI setup in order to configure it. You can use any DI container you want, but we have provided the default wiring code for Autofac, Ninject, StructureMap, Unity, and Castle Windsor. Only the most basic setup - a single sitemap using a single file with a few options - can be accomplished without an external DI container. You need to decide how requests will be sent to one sitemap vs the other. This can be accomplished by implementing either 1 or 2 interfaces. ISiteMapCacheKeyGenerator is what controls when to create a new cached sitemap - a new key will yield a new sitemap. Then ISiteMapCacheKeyToBuilderSetMapper (what a mouthful) is what, well, maps a sitemap cache key to an implementation of IBuilderSet. There can be 1 or many builder sets - all depends on how many different ways the application will need to build a tree. Each builder set is given a name in the configuration and ISiteMapCacheKeyToBuilderSetMapper is what decides which key calls which builder set/cache. The builder set is what controls the cache as well as way a site map tree is built. The other thing you will need to do is set up a custom implementation of ISiteMapBuilder to actually build the second site map tree with security trimming disabled. Unfortunately, SecurityTrimmingEnabled is literally the only setting that is locked down - once it is enabled, it cannot be disabled again, otherwise there would be a quick and easy shortcut. However, what you will need to do is copy the entire implementation of XmlSiteMapBuilder and set it up so it gets the SecurityTrimmingEnabled value through a constructor parameter rather than the XML file so you can control it via dependency injection config. Once you have your classes implemented then it is simply a matter of altering the DI configuration to wire them up. Here is how that would be done in StructureMap: // Setup new cache key generator
this.For<ISiteMapCacheKeyGenerator>()
.Use<CustomSiteMapCacheKeyGenerator>();
// Setup the new mapper
this.For<ISiteMapCacheKeyToBuilderSetMappper>()
.Use<CustomSiteMapCacheKeyToBuilderSetMapper>();
// Setup cache
this.For<System.Runtime.Caching.ObjectCache>()
.Use(s => System.Runtime.Caching.MemoryCache.Default);
this.For<ICacheProvider<ISiteMap>>().Use<RuntimeCacheProvider<ISiteMap>>();
var cacheDependency =
this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
.Ctor<string>("fileName").Is(absoluteFileName);
var cacheDetails =
this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(cacheDependency);
// Configure the visitors
this.For<ISiteMapNodeVisitor>()
.Use<UrlResolvingSiteMapNodeVisitor>();
// Register the sitemap builders
var xmlSource = this.For<IXmlSource>().Use<FileXmlSource>()
.Ctor<string>("fileName").Is(absoluteFileName);
var reservedAttributeNameProvider = this.For<ISiteMapXmlReservedAttributeNameProvider>()
.Use<SiteMapXmlReservedAttributeNameProvider>()
.Ctor<IEnumerable<string>>("attributesToIgnore").Is(new string[0]);
var builder1 = this.For<ISiteMapBuilder>().Use<CompositeSiteMapBuilder>()
.EnumerableOf<ISiteMapBuilder>().Contains(y =>
{
y.Type<XmlSiteMapBuilder>()
.Ctor<ISiteMapXmlReservedAttributeNameProvider>().Is(reservedAttributeNameProvider)
.Ctor<IXmlSource>().Is(xmlSource);
y.Type<VisitingSiteMapBuilder>();
});
// This is where you would put your custom builder
var builder2 = this.For<ISiteMapBuilder>().Use<CompositeSiteMapBuilder>()
.EnumerableOf<ISiteMapBuilder>().Contains(y =>
{
y.Type<CustomXmlSiteMapBuilder>()
.Ctor<ISiteMapXmlReservedAttributeNameProvider>().Is(reservedAttributeNameProvider)
.Ctor<IXmlSource>().Is(xmlSource)
.Ctor<bool>("securityTrimmingEnabled").Is(false);
y.Type<VisitingSiteMapBuilder>();
});
// Configure the builder sets
this.For<ISiteMapBuilderSetStrategy>().Use<SiteMapBuilderSetStrategy>()
.EnumerableOf<ISiteMapBuilderSet>().Contains(x =>
{
x.Type<SiteMapBuilderSet>()
.Ctor<string>("instanceName").Is("default")
.Ctor<ISiteMapBuilder>().Is(builder1)
.Ctor<ICacheDetails>().Is(cacheDetails);
x.Type<SiteMapBuilderSet>()
.Ctor<string>("instanceName").Is("noSecurityTrimming")
.Ctor<ISiteMapBuilder>().Is(builder2)
.Ctor<ICacheDetails>().Is(cacheDetails);
}); If you compare this against the MvcSiteMapProviderRegistry.cs file that we ship as the default, you can more clearly see what has changed from a default setup. Note you will need to drag along the other stuff from that file too and replace everything after the comment "setup cache" with the above, I abbreviated it here for clarity. And no, there is currently no documentation on configuration. Please open a new issue if you see something specific that should have documentation that currently doesn't. Also, I highly recommend the book Dependency Injection in .NET by Mark Seemann, as the approach I followed to DI is precisely what he is preaching. Not only is it a good DI book, but it also has some really good advice about how to refactor code, which makes it invaluable. |
Actually, I gave this more thought and there are 2 options you have that are easier than re-implementing an entire class. Option 1:Following the above approach, it is not possible to turn off security trimming, but it is possible to turn it on. You can therefore set public class EnableSecurityTrimmingSiteMapBuilder
: ISiteMapBuilder
{
#region ISiteMapBuilder Members
public virtual ISiteMapNode BuildSiteMap(ISiteMap siteMap, ISiteMapNode rootNode)
{
siteMap.SecurityTrimmingEnabled = true;
return rootNode;
}
#endregion
} You can then inject this class after the standard implementation and it will turn on security trimming for the "normal" case. Naturally, you would also want to have an "administrator" builder set where this class is not injected: var builder1 = this.For<ISiteMapBuilder>().Use<CompositeSiteMapBuilder>()
.EnumerableOf<ISiteMapBuilder>().Contains(y =>
{
y.Type<XmlSiteMapBuilder>()
.Ctor<ISiteMapXmlReservedAttributeNameProvider>().Is(reservedAttributeNameProvider)
.Ctor<IXmlSource>().Is(xmlSource);
y.Type<VisitingSiteMapBuilder>();
});
// This is where you would put your custom builder
var builder2 = this.For<ISiteMapBuilder>().Use<CompositeSiteMapBuilder>()
.EnumerableOf<ISiteMapBuilder>().Contains(y =>
{
y.Type<XmlSiteMapBuilder>()
.Ctor<ISiteMapXmlReservedAttributeNameProvider>().Is(reservedAttributeNameProvider)
.Ctor<IXmlSource>().Is(xmlSource);
y.Type<EnableSecurityTrimmingSiteMapBuilder>();
y.Type<VisitingSiteMapBuilder>();
}); I failed to mention before that you need to use the SiteMaps static class to access a sitemap with a specific site map cache key: ISiteMap siteMap = SiteMaps.GetSiteMap("mySiteMapKey"); Then of course, you can use the siteMap object to iterate the nodes recursively. Note you can also invalidate the site map cache using a similar call (in case you made a change to some data that you want to appear in the site map immediately). SiteMaps.ReleaseSiteMap("mySiteMapKey"); Option 2:This is even simpler. Rather than implementing 3 interfaces, you would only need to implement 1 - IAclModule. This assumes that you can determine whether you are in your "special administrative mode" from ambient context (httpContext, file system, database, etc). You can use the decorator pattern to make your rule apply to everything really easily, like this: public class SpecialAdministrativeAclModule
: IAclModule
{
public SpecialAdministrativeAclModule(
IAclModule innerAclModule
)
{
if (innerAclModule == null)
throw new ArgumentNullException("innerAclModule");
this.innerAclModule = innerAclModule;
}
private readonly IAclModule innerAclModule;
#region IAclModule Members
public virtual bool IsAccessibleToUser(ISiteMap siteMap, ISiteMapNode node)
{
bool isSpecialAdministrativeMode = false;
// Put your logic here - set isSpecialAdministrativeMode to true if we are in the mode.
if (isSpecialAdministrativeMode)
return true;
else
return innerAclModule.IsAccessibleToUser(siteMap, node);
}
#endregion
} And then inject this to wrap the "normal" ACL logic: // Configure Security
var normalAclModule =
this.For<IAclModule>().Use<CompositeAclModule>()
.EnumerableOf<IAclModule>().Contains(x =>
{
x.Type<AuthorizeAttributeAclModule>();
x.Type<XmlRolesAclModule>();
});
this.For<IAclModule>().Use<SpecialAdministrativeAclModule>()
.Ctor<IAclModule>().Is(normalAclModule); |
It works :-). Note that, due to a lack of time, I did it on a very basic sample project. I followed your initial recipe which includes :
In my DI container, I registered two CompositeSiteMapBuilder : the first is the default implementation, the second uses my "FullAccessXmlSiteMapBuilder" in place of default XmlSiteMapBuilder.
Finally, an excerpt of my DI setup :
I tested it with the Html helper : @Html.MvcSiteMap().SiteMap() // default The whole process seems to be a bit complicated from scratch. But I have to admit that from an existing project that already defines an external DI setup, it's incredibly simple. Thank you also for the note about the book of Mark Seemann that I did'nt read. I'm already an advocate of this approach. It's always nice to have reading recommandations from other developers. I have new questions if you have some more time :
Finally, I wish to share a thought : in your place, I would have replaced the constructors that take as a parameter an array of instances with an IEnumerable (instead of T[]). I do not know if it is a major change, but this would have facilitated Simple Injector integration and it would have permitted, I think, better performances where all instances are eventually not used. It is the case in my advanced scenario : only some administration pages need to walk all the sitemap regardless security trimming. However, this is surely not significant in term of performances (in my use case). Thank you again for your time. |
That is a good point, perhaps this is something that should be added since now it is technically possible to run it without the XmlSiteMapBuilder completely, in which case one of the other builders must set it. But actually there may be another bug here, it would make more sense if turning this setting on were moved to a later step after building is complete. This is because depending on the permission of the user that ends up building the cache, the builders might not be able to completely work because of access restrictions. However, the VisitingSiteMapBuilder is just reusing the ISiteMapBuilder interface to do something different. Maarten wanted to have a "final phase" where a visitor could do last minute clean up on the nodes. One such task is to pre-resolve the URLs, which is why that is in there. In any case, there is no point in setting the security trimming in this stage, because there is actually no building going on.
Actually, it would probably be best to set everything up as singleton EXCEPT XmlSiteMapController, since all controllers must be transient or HTTP request scoped. Although, the MvcSiteMapProvider still performs well if all objects are transient, it is most important that the providers (ISiteMapNodeUrlResolver, ISiteMapNodeVisibilityProvider, and IDynamicNodeProvider) be singleton so they are shared between all of the nodes - that could save a good piece of memory depending on how many nodes there are (or how many sitemaps there are).
The decision to go this route actually stemmed from the fact that some DI containers require extra setup to inject into an IEnumerable<> than an array. Autofac, Ninject, and StructureMap natively inject all registered instances into a constructor parameter of type T[] and there is not any configuration code required other than registering the objects. Before you mentioned it, I had never heard of Simple Injector, but if it suits your taste that is fine. We chose the DI containers to include by their popularity (Ninject is the 9th most popular download on Nuget gallery), but if you would like to donate some of your time to contribute another DI setup for Simple Injector we have no objections. Anyway, thanks for the valuable feedback on your experience. |
Ok, so this is something to keep in mind. My advanced scenario should work in most simple cases (xml sitemap) :-).
Thanks, it's very clear.
I understand. Unfortunately, it's exactly the contrary for Simple Injector, so I have to register constructors which contain array parameters with a delegate. I will don't hesitate to contribute if I have the time to make a clean Simple Injector registration. FYI, a good IoC container benchmark : http://www.palmmedia.de/Blog/2011/8/30/ioc-container-benchmark-performance-comparison |
If you have any bright ideas, I am open to suggestions. I can't think of a reasonable way to support both approaches. It is much easier to design an API targeted to a specific DI container than one that is open to any of them.
Just follow the conventions used for the other DI containers and it should be pretty straightforward.
Thanks for the link. I sure hope we don't have to support all of those containers :). I had no idea that Ninject was the slowest container. But then, the charts don't really consider that much. I chose StructureMap because of its ability to use conventions on setter injection, and nice clean separation of concerns when it comes to customizing conventions and making modules. Not to mention, its complete feature set including custom lifetimes and interception. One really great feature is the ability to return a "smart instance" from registering one component and adding it as a variable to the constructor of another component. In most other containers, you have to use a "named instance" to achieve the same, but you have to wait until runtime to see if that is going to fail. However, admittedly the documentation for StructureMap is not very good and finding the correct syntax to use can sometimes be challenging. That has been my experience with MVC as well - it seems like most of the documentation out there is on prerelease versions where the proposed solution no longer works. |
…lse because the IsReadOnly functionality blocks this anyway. Changed IsAccessibleToUser to always return true while the site map is being built. Fixes #102.
Ok, I will follow these rules. I think I will be able to submit something in a few days. |
What do you think about the idea of moving the SecurityTrimmingEnabled and EnableLocalization settings to the SiteMap constructor, in which case you would need to provide these settings in the DI setup to the SiteMapBuilderSet (which would pass them on down during the loader phase). Both of the settings could then just be read-only on the SiteMap object. This would fix any security concerns about the security trimming setting being changed during runtime, and also means there is a way to set the settings 1 time per site map. The builders wouldn't have anything to do with these settings in this case. |
Can't we have something simple like SiteMaps.Current.GetAllNodes(true/false); when true could return with security trimmed and when false return all nodes? Why? Because the current approach forces us to use external DI |
Yes it can be done, but no, it is not simple. For one, the SiteMap is a hierarchy, not a list. If you return all of the nodes using a query like this it would not be possible to tell how the nodes are related to one another. There was another request similar to this (to view the nodes in a list instead of a hierarchy), but is that really what you are after? Secondly, the security trimming is enforced at every level of the hierarchy (in several methods of the SiteMap class), so passing a single boolean parameter like this would effectively mean changing the interface of a large number of methods in the SiteMap class in order to pass this value through. Microsoft's model (which this is based on) didn't allow for ways around the security. In fact, even if you return the nodes in a list (as mentioned above) the ParentNode and ChildNodes properties will still enforce security trimming, so that really won't be a complete solution anyway. On top of this, the HTML helpers also repeat the check for security trimming. I am not sure why that was done, as that "feature" was carried over from v3. So even if the problem is solved with the SiteMap object, it would require custom HTML helper models to be complete (assuming you are looking for a way to display them on the UI). Despite these problems, I think it is a good idea to add the ability to view the whole hierarchy (and will need to be done anyway to support #188), so I am reopening this issue. That said, it is not likely to be implemented anytime soon because of the number of breaking changes that will be required. For now, implementing a custom IAclModule is the simplest way. A possible alternative is to use a special role for the case that you need to view all of the nodes and assign that role to every AuthorizeAttribute. |
I am also in need of the hierarchy. GetNodes can return the entire hierarchy by traversing the sitemap once without applying security trimming or taking the current user in context. And whether the node is accessible to current user has to be done by passing the node to the SiteMaps.Current.IsAccessibleToUser. I understand this is a nice to have feature request and not expecting it to be available immediately. Also, would like to Thank you for such a library and taking time to support it. |
Hello,
I'm not sur if it's the right place to ask questions... If it's not, I apologize in advance.
I'm looking for a way to explore all nodes (SiteMapNode.GetAllNodes()) of a sitemap with security trimming enabled.
I tried to duplicate the default sitemap into another wi security trimming disabled but it does not seem to be the right way: MvcSiteMapProvider manages a single XmlSiteMapProvider regardless the number of initial sitemaps configured.
Thanks in advance.
Cheers,
The text was updated successfully, but these errors were encountered: