Custom security trimming in ASP.NET sitemaps

In ASP.NET you can create a sitemap to capture the pages that are available in your web application. The web.sitemap is an XML file that is a hierarchical list that is provided for data binding to controls via a sitemap provider. The Menu, SiteMapPath and TreeView control are common controls used for the binding to the sitemap data.

The fragment shows a bit of a web.sitemap.

<?xml version="1.0" encoding="utf-8" ?>

<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" enableLocalization="true">

  <siteMapNode title="Root" description="My Links" roles="*">

    <siteMapNode url="~/Default.aspx" title="Home" resourceKey="Home" description="Go to homepage" roles="*"/>

    <siteMapNode url="~/Admin/EditPosting.aspx" title="Add Entry" resourceKey="AddEntry" description="Adds a new posting"

              roles="BlogContributor;BlogOwner;BlogAdmin;BlogEditor" />

The default provider XmlSiteMapProvider has a built-in mechanism to trim the sitemap based on roles of an authenticated user. In the fragment you can see the roles attribute that lists the roles an authenticated user should have to "see" the corresponding siteMapNode. When everyone is allowed access you simply specify * as the role.

The security trimming from .NET's Role Based Security (RBS) is all nice and easy if you use roles in your security. But what if you use application rights instead of roles to validate access to parts of your app? You will need to build your own custom sitemap provider. This is a fairly trivial task. Derive your custom provider class from XmlSiteMapProvider if you think that the XML files like web.sitemap is your thing. Otherwise you can derive from the abstract base class SiteMapProvider and role your own provider from scratch. I assume that you are OK with the XML files.

namespace KillerApps.Web

{

  public class CustomSiteMapProvider: XmlSiteMapProvider

  { ... }

}

The key to creating custom security trimming is an override to the IsAccessibleToUser method. This method gives you two arguments for the current HTTP context of the request and the node to evaluate. This node is found by the base class after a lookup of the current URL. In the override we determine whether the current user has access to the given node. 

public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)

{

  // Security trimming should be enabled, otherwise everything is accessible

  if (!this.SecurityTrimmingEnabled)

    return true;

 

  // Default to no access when no rights are specified

  if (node["rights"] == null) return false;

  if (node["rights"] == "*") return true;

 

  // Split on separator and check if items exist

  string[] rights = node["rights"].Split(';', ',');

  if (rights == null || rights.Length == 0) return false;

 

  // Check each right against security manager

  foreach (string right in rights)

  {

    if (SecurityManager.IsAuthorized(context.User, right))

    {

      return true;

    }

  }

 

  return false;

}

This sample shows a possible implementation of the override IsAccessibleToUser. First it checks if security trimming is enabled and continues to check what if rights are defined. Note the bold items: we'll get to that in a minute. Next the rights are checked against your own security implementation. The SecurityManager mentioned is not part of the .NET Framework.

The bold items show a great extensibility point of sitemaps. You can define any attribute on a <siteMapNode>, even if it is not in the XML schema for sitemaps. Any attribute is available through the indexer property of the SiteMapNode class. The bold items show how you can read attributes from the sitemap nodes when defined like so:

    <siteMapNode url="~/Default.aspx" title="Home" resourceKey="Home" description="Go to homepage" rights="ViewHomePage" />

Side note: the XSD file for the sitemaps incorrectly lists the securityTrimmingEnabled attribute as part of <siteMapNode>. This is not correct, because this is an attribute in the web.config. Your web.config should contain a definition for the custom provider:

<siteMap enabled="true" defaultProvider="CustomProvider">

  <providers>

    <add name="CustomProvider" type="KillerApps.Web.CustomSiteMapProvider"

      description="Rights based security trimmed sitemap provider"

      siteMapFile="~/web.sitemap" securityTrimmingEnabled="true" />

  </providers>

</siteMap>

Notice how you can also specify another file as the sitemap. It is not uncommon to have multiple providers that offer sitemaps from different files.

Filed under: ,

Comments

# sholliday said:

Thanks for the tips and ideas.

The roles vs rights thing has confused me for sometime.

I am going to do something along the same route, but have a IPrincipal to work with.

Here is the interface definition of the it:

   public interface IRolesAndRightsPrincipal : System.Security.Principal.IPrincipal

   {

       bool IsInRole(System.Guid role);

       bool IsInAnyRole(System.Guid[] roles);

       bool IsInAllRoles(System.Guid[] roles);

       bool HasRight(System.Guid right );

       bool HasAnyRight(System.Guid[] rights );

       bool HasAllRights(System.Guid[] rights );

   }

You could swap out my Guids for strings as well.  But I'm going to a Guid based system since there could be alot of rights overlap from project to project.

Thanks for the article!

donderdag 28 juni 2007 23:18
# auxcom said:

Thanks for this wonderful article.

I trying to implement this but I'm just stuck when I add multiple sitemap file.

Below is the main site map (web.sitemap):

 <siteMapNode url="~/default.aspx" title="Home" description="">

   <siteMapNode siteMapFile="~/sitemaps/HelpDesk.sitemap" />

   <siteMapNode url="~/myrecent-dload.aspx" title="" />

   <siteMapNode url="~/common/download.aspx" title="" />

 </siteMapNode>

Below is my web.config

<siteMap defaultProvider="MySiteMapProvider" enabled="true">

     <providers>

       <clear />

       <add name="MySiteMapProvider" siteMapFile="~/web.sitemap" securityTrimmingEnabled="true" type="MySiteMapProvider" />

     </providers>

   </siteMap>

I just wonder why in the execution of the custom provider code especially IsAccessibleToUser method, I can only see nodes of the main site map like

<siteMapNode url="~/myrecent-dload.aspx" title="" />

<siteMapNode url="~/common/download.aspx" title="" />

but nodes inside "sitemaps/HelpDesk.sitemap" is not being evaluated or included?

Hope I'm clear with my problem and you could correct me.

Thanks!

Daniel

maandag 24 december 2007 3:01
# Richard's Rant said:

Bill Gates explains why Microsoft Needs Yahoo . Interesting indeed. Stephen Fry has now started Podcasting

donderdag 21 februari 2008 6:45