Use custom access right for MvcSiteMapProvider

Introduction

MvcSiteMapProvider is a tool that provides flexible menus, breadcrumb trails, and SEO features for the ASP.NET MVC framework, similar to the ASP.NET SiteMapProvider model. It’s support the base Acl function, you can just set the Roles to control the access right, but if you want the advance and complicated rights control, than you need to handle it by yourself. For example, I want to implement the below access rights:

The menu only can be see and access by below case:

Case 1:

the user in department ‘C’ and a supervisor.

(the expression should be:  dept == ‘C’ and level == ‘supervisor’)

Case 2:

the user in department ‘C’ and a supervisor or in department ‘E’. 

(the expression should be:  (dept == ‘C’ and level == ‘supervisor’) or dept == ‘E’)

For the above requirement, we can add a custom attribute to the menu item for the access right expression like below:

<mvcSiteMapNode title="About Me" controller="Home" action="About"  accessRightExpr="('{dept}'=='C' and '{level}'=='Supervisor') or '{dept}'=='E'" />

So that we can handle all of the complicated cases, and I will show you how to do that!

Using the code

  • We need to handle dynamic expression such like ((dept == ‘C’ and level == ‘supervisor’) or dept == ‘E’), so we need to create a runtime compiler. You can refer to here for that, I have modified for let it support web application.
    //how to use
    var compiler = new RuntimeCompiler<bool>(); //set the retunr to bool
    var result = compiler.ExecuteExpression("('a'=='a' and 1==1') or 'e'=='e'");//pass the dynamic expression
  • Create a custom Acl module for MvcSiteMap provider:
    public class SiteMapAclModule : IAclModule
    {
        #region IAclModule Members
    
        /// <summary>
        /// Determines whether node is accessible to user.
        /// </summary>
        /// <param name="siteMap">The site map.</param>
        /// <param name="node">The node.</param>
        /// <returns>
        ///       <c>true</c> if accessible to user; otherwise, <c>false</c>.
        /// </returns>
        public bool IsAccessibleToUser(ISiteMap siteMap, ISiteMapNode node)
        {
            try
            {
                bool userHasAccessToNode = true;
    
                if (node.Attributes.ContainsKey("accessRightExpr"))
                {
                    var compiler = new RuntimeCompiler<bool>();
                    userHasAccessToNode = compiler.ExecuteExpression(node.Attributes["accessRightExpr"].ToString().  //pass the dynamic expression from sitemap
                        Replace("{dept}", CoderBlogSession.Instance.Dept).  //replace the tag to variable
                        Replace("{level}", CoderBlogSession.Instance.Level));
                }
    
                return userHasAccessToNode;
            }
            catch (Exception ex)
            {
                return true;
            }
        }
    
        #endregion
    }
  • Enable the securityTrimming in the MvcSiteMapProvider, and add the DI container.
    I use the MvcSiteMapProvider.Mvc5.DI.StructureMap, you can install in from NuGet. After installed it, we need to update below file:
    DI/StructureMap/Registries/MvcSiteMapProviderRegistry.cs
    1. Set securityTrimming to true:

    bool securityTrimmingEnabled = true;  //need to set true for use custom acl module in line 34

    2. Add the custom Acl module to DI:

    // Configure Security
    this.For<IAclModule>().Use<CompositeAclModule>()
        .EnumerableOf<IAclModule>().Contains(x =>
        {
            x.Type<AuthorizeAttributeAclModule>();
            x.Type<XmlRolesAclModule>();
            x.Type<SiteMapAclModule>(); //add new custom acl moudle
        });
    

    3. Set attributesToIgnore for new custom attribute in site map item

    this.For<IReservedAttributeNameProvider>().Use<ReservedAttributeNameProvider>()
     .Ctor<IEnumerable<string>>("attributesToIgnore").Is(new string[] { "accessRightExpr" }); //ignore the custom attribute in line 128
  • Add the custom attribute to Mvc.sitemap. Now we can add the dynamic access right expression to sitemap item as below:
    <mvcSiteMapNode title="About Me" controller="Home" action="About"  accessRightExpr="('{dept}'=='C' and '{level}'=='Supervisor') or '{dept}'=='E'" />

    We use the {dept},{level} tag in the expression, and than just need to replace these to the corresponding variable.

  • For now, we can use the site map and can handle the complicated access right checking, but we still have a problem, we also need to handle the action in controller, otherwise when user input the url directly then they will can access the page. So we need to create a custom Acl Mvc attribute.
    But we have another problem, we can’t get the MvcSiteMap object in Acl attribute, because it still not create in this time, so we need to pass the access right expression in Acl attribute in coding like below:

    [Acl(Expression="...")]
    public class HomeController : Controller
    {
          ...
    }

    And the problem is that we need to handle the same expression in difference place, it will not easy to maintain especially you have a lot of menu items. I think the best way is read the expression from same place for difference functions. Because the Mvc.sitemap file is also a XML file, so the other approach is that we can use XML reader for get the file in Acl attribute, so that we can get all of the xml nodes and attribute for handle it.But if read the xml every time in each action, it will case a performance issue, so we need to use a cache, the code as below:

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        //for testing, set the current user rights in session (should be set this after login)
        CoderBlogSession.Instance.Dept = "B";
        CoderBlogSession.Instance.Level = "Supervisor";
        
    
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }
    
        var currentController = filterContext.RouteData.Values["Controller"].ToString();
        var currentAction = filterContext.RouteData.Values["Action"].ToString();
    
    
        //cache the authenticated status
        var cacheKey = CreateKey(filterContext).ToLower();
        var isCached = HttpRuntime.Cache.Get(cacheKey);
    
        //ignore the 'ajax' action url
        if (isCached != null || cacheKey.Contains("ajax")) return;
    
        //get the menu item from site-map xml for access right checking
        var siteMapDoc = RemoveAllNamespaces(XDocument.Load(HttpContext.Current.Server.MapPath("~/Mvc.sitemap")).Root);
    
        var currentNode = (from el in siteMapDoc.Descendants("mvcSiteMapNode")
                           where (string)el.Attribute("controller") == currentController &&
                           (string)el.Attribute("action") == currentAction && el.Attribute("clickable") == null
                           select el).FirstOrDefault();
    
        var isAuthenticated = true;
        //check AccessRightExpr
        if (currentNode != null)
        {
            isAuthenticated = CheckMenuItemRight(currentNode);
        }
        CacheDependency cd = new CacheDependency(HttpContext.Current.Server.MapPath("~/Mvc.sitemap"));
        HttpRuntime.Cache.Insert(cacheKey, true, cd);
    
    
        if (!isAuthenticated)
        {
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Home", action = "AccessDenied" }));
        }
    
    }
    
    /// <summary>
    /// Generate the cache key
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private string CreateKey(AuthorizationContext context)
    {
        // Or create some other unique key that allows you to identify 
        // the same request
        return
            context.RequestContext.HttpContext.User.Identity.Name + "|" +
            context.RequestContext.HttpContext.Request.Url.AbsoluteUri;
    }
  • Check the access right with dynamic expression in Acl attribute:
    /// <summary>
    /// Check the menu item access right with recursion
    /// </summary>
    /// <param name="node">current node</param>
    /// <param name="User"></param>
    /// <returns></returns>
    private bool CheckMenuItemRight(XElement node)
    {
        var isAuthenticated = true;
        if (node.Attribute("accessRightExpr") != null)
        {
            var compiler = new RuntimeCompiler<bool>();
            isAuthenticated = compiler.ExecuteExpression(node.Attribute("accessRightExpr").Value.
                    Replace("{dept}", CoderBlogSession.Instance.Dept). //replace the tag to variable
                    Replace("{level}", CoderBlogSession.Instance.Level));//replace the tag to variable
        }
    
        if (node.Attribute("roles") != null)
        {
            //TODO::if you have implement the ASP.NET identity Authorization, e.g
            //isAuthenticated = node.Attribute("roles").Value.Split(',').Any(User.IsInRole);
        }
    
    
        if (isAuthenticated && node.Parent != null)
        {
            isAuthenticated = CheckMenuItemRight(node.Parent);
        }
    
        return isAuthenticated;
    }
  • It’s ok, now just need to set the Acl attribute in the controller or action then will be done:
    [Acl]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    
        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";
    
            return View();
        }
    
        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";
    
            return View();
        }
    
        [AllowAnonymous]
        public ActionResult AccessDenied()
        {
            return View();
        }
    }
  • Test the website 🙂
  • You can find the full demo code in below:
    https://github.com/coderblog-winson/MvcSiteMapAccessright

 

24,633 total views, 29 views today

Do you like this post?
  • Fascinated
  • Happy
  • Sad
  • Angry
  • Bored
  • Afraid

winson

Leave a Reply

coder-blog-1
shares