bounav
bounav

Reputation: 5046

How to improve the responsiveness of MVC SiteMap Provider when Security Trimming is enabled

I have just noticed how having security trimming enabled with MVC Sitemap in an ASP.NET MVC 5.x site can considerably slow down web requests.

I've gathered from the documentation that when security trimming is turned on, MVC sitemap provider creates on instance of each controller in the sitemap to check if the node should be visible or not for the current user (per web request).

I also read that it is cached per web request to keep the impact as low as possible.

This behaviour used to go unnoticed for us when when most of our controller's dependencies where managed as singleton by our IoC framework (meaning getting an instance of a controller was very fast).

Recently, our requirements changed and most of our dependencies have a per web request lifestyle, which had a massive negative impact on MVC Sitemap provider's performance: On our dev machines, it takes over 5 seconds (!) for mvc sitemap to do its thing (instantiate all the constructors).

Can anything be done to speed things up when security trimming is enabled?

Notes:

It's probably worth mentioning we follow the best-practice of not doing anything in our constructor appart from assign the parameters into instance variables or properties) to avoid slowing things down further.

We also stick to the default Authorise attribute provided by Microsoft.

Upvotes: 0

Views: 281

Answers (2)

Paul Schroeder
Paul Schroeder

Reputation: 1601

@bounav, thank-you, thank-you, thank-you! Although not an ideal solution, it definitely works and the time/processing savings will really make a huge impact/difference for my client.

Although based on the solution above, the code I implemented varies significantly. I'll post the critical pieces here in hopes it helps others.

Summary of changes This implementation uses the thread principle (IPrincipal). There are a few tweaks to handle multiple roles as well as controllers that use [Authorize] attributes without any specific roles (user just has to be authenticated). I changed the name of the attribute from "restrict-to-roles" to "Roles". This allows me to simply copy/paste the values from each controller's [Authorize] attributes into the Mvc.sitemap XML file. For example this controller attribute:

[Authorize(Roles = "YourSecurityRole")]

becomes this in the Mvc.sitemap XML file:

<mvcSiteMapNode title="Your Title" controller="Yourcontroller" Roles = "YourSecurityRole" action="YourControllerAction" key="YourNodeKey">

Here is the pertinent class:

public class RolesInXmlFileSecurityTrimmingVisibilityProvider : SiteMapNodeVisibilityProviderBase
{
    public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
    {
        // If the sitemap node does not contain the 'roles' attribute, simply return true.
        if (!node.Attributes.ContainsKey("Roles"))
        {
            return true;
        }

        // When the sitemap node contains the 'roles' attribute, the user must at least be authenticated for the node to be visible.
        var currentUser = Thread.CurrentPrincipal;
        if (currentUser == null || currentUser.Identity == null || currentUser.Identity.IsAuthenticated == false)
        {
            return false;
        }

        var strRoles = Convert.ToString(node.Attributes["Roles"]);
        if (string.IsNullOrWhiteSpace(strRoles))
        {
            return true; // Use an empty Roles="" attribute/value for controllers with only the basic [Authorize] attribute
        }
        else
        {
            var arrRoles = strRoles.Split(',');
            foreach (var role in arrRoles)
            {
                if (currentUser.IsInRole(role))
                {
                    return true; // The authenticated user matches at least one of the roles required for visibility.
                }
            }
        }

        return false; // The authenticated user did not match any of the roles required for visibility.
    }
}

There are three additional noteworthy technical details I want to mention due to our use of Ninject DI.

  1. The "MvcSiteMapProviderModule" that uses NinjectModule as a base class is where I disabled security trimming. The overridden Load() method, excerpted:

    bool securityTrimmingEnabled = false;

    // Configure the builder sets .Kernel.Bind<ISiteMapBuilderSet>().To<SiteMapBuilderSet>().Named("siteMapBuilderSet1").WithConstructorArgument("instanceName", "default").WithConstructorArgument("securityTrimmingEnabled", securityTrimmingEnabled)...<removed for brevity>

  2. The class used for the 'ISiteMapNodeVisibilityProviderStrategy' had to be swapped out as follows:

    this.Kernel.Bind<ISiteMapNodeVisibilityProviderStrategy>().To<SiteMapNodeVisibilityProviderStrategy>() .WithConstructorArgument("defaultProviderName", "YourNamespace.RolesInXmlFileSecurityTrimmingVisibilityProvider, YourAssemblyName");

  3. Unless you want the extra "Roles" attribute to appear in the URL, you have to configure the "attributesToIgnore" value as such:

    // Prepare for our node providers this.Kernel.Bind<IXmlSource>().To<FileXmlSource>().Named("XmlSource1") .WithConstructorArgument("fileName", absoluteFileName);

    this.Kernel.Bind<ISiteMapXmlReservedAttributeNameProvider>().To<SiteMapXmlReservedAttributeNameProvider>().Named("xmlBuilderReservedAttributeNameProvider").WithConstructorArgument("attributesToIgnore", new string[1] { "Roles" });

Hope that helps!

Upvotes: 0

bounav
bounav

Reputation: 5046

This is the solution I ended up implementing:

  1. Turned security trimming off.
  2. Added a new attribute restrict-to-role to the MVC sitemap (in the XML file) nodes that were matching the controller actions that are decorated with the [Authorize(Roles="Role1,Role2")] attribute.
  3. Added a new visibility provider that checks if there is an attribute for the current node that has a restrict-to-roles attribute. If there is a value, I then call ASP.NET identity's IsInRole method to check the current user can see the node.
public class RolesInXmlFileSecurityTrimmingVisibilityProvider : SiteMapNodeVisibilityProviderBase
{
    public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
    {
        var isVisible = true;

        if (node.Attributes.ContainsKey("restrict-to-roles"))
        {
            var roles = Convert.ToString(node.Attributes["restrict-to-roles"]);

            if (!string.IsNullOrEmpty(roles))
            {
                 // Your implemenation may vary
                 isVisible = IsInRole(roles);
            }
        }

        return isVisible;
    }
}

Advantage:

This is very quick and you don't need to instantiate all the controllers to look for the [Authorize] attribute.

Drawback:

You have to manually maintain the restric-to-roles attributes in the MVC sitemap file on top of the MVC [Authorize] in the controllers.

Upvotes: 1

Related Questions