FluentSecurity + MvcSiteMapProvider = Better .Net Security Management

by Mark Wiseman on May 8th, 2012 | Posted in Revium Sandbox | Read the comments

In my continual quest for find better ways of doing anything and everything this week I decided to tackle on of my arch nemesis: Security in ASP.Net, specifically Security in ASP.Net MVC3. You might ask why? We all know that there are at least 2 easy ways to implement security in MVC3

  1. Authorize Attributes on Controllers and Actions
  2. web.config locations

Yuck! I hate both of them for the same reason: they become very difficult to manage. Authorize Attributes end up spread all over your application and web.config location elements require 7 lines of xml for 1 rule. So I went searching and came across a NuGet package called FluentSecurity.

The documentation covered all the examples I needed and you can define rules in only 1 line of code!

configuration.For<AccountController>.RequireRole("Administrator", "Legal");

I ran into 2 difficulties when trying to implement this:

  1. Implementing an Access Denied page and re-direct to Login
  2. Integrating FluentSecurity with MvcSiteMapProvider

If you want to skip all the work and get straight to a working example download a sample application here FluentSecurityAndMvcSiteMapProvider.zip

Implementing an Access Denied page and re-direct to Login

The current NuGet version (1.4 as at 05/2012) required that you use StructureMap or another IoC container to implement a custom IPolicyViolationHandler. I wasn’t using either but discovered that it is not a requirement in version 2 which is in alpha at the time of writing but works fine for me. So make sure either the Version you get from NuGet is at least 2.0 or got to GitHub to get the latest version.

First we need to create a class implementing IPolicyViolationHandler.
The class that we create will:

  • Send unauthenticated users to a shared view called AccessDenied
  • Redirect authenticated users without access to the current controller to the Account controllers LogIn action
using System.Web.Mvc;
using System.Web.Routing;
using FluentSecurity;

public class DefaultPolicyViolationHandler : IPolicyViolationHandler
{
    public string ViewName = "AccessDenied";

    public ActionResult Handle(PolicyViolationException exception)
    {
        if (SecurityHelper.UserIsAuthenticated())
        {
            return new ViewResult { ViewName = ViewName };
        }
        else
        {
            RouteValueDictionary rvd = new RouteValueDictionary();

            if (System.Web.HttpContext.Current.Request.RawUrl != "/")
                rvd["ReturnUrl"] = System.Web.HttpContext.Current.Request.RawUrl;

            rvd["controller"] = "Account";
            rvd["action"] = "LogOn";
            rvd["area"] = "";

            return new RedirectToRouteResult(rvd);
        }
    }
}

Now we need to set / tell FluentSecurity to use our DefaultPolicyViolationHandler. For this I created a SecurityHelper class.

using System.Collections.Generic;
using System.Linq;
using System.Web;

using FluentSecurity;
using FluentSecurityAndMvcSiteMapProvider.Areas.Admin.Controllers;
using FluentSecurityAndMvcSiteMapProvider.Controllers;

public static class SecurityHelper
{
    public static ISecurityConfiguration SetupFluentSecurity()
    {
        SecurityConfigurator.Configure(configuration =&gt;
        {
            // Let Fluent Security know how to get the authentication status of the current user
            configuration.GetAuthenticationStatusFrom(SecurityHelper.UserIsAuthenticated);
            configuration.GetRolesFrom(SecurityHelper.UserRoles);

            // It is reccommended not to use this setting. For all of our applications users must always be authenticated.
            configuration.IgnoreMissingConfiguration();

            configuration.DefaultPolicyViolationHandlerIs(() =&gt; new DefaultPolicyViolationHandler());

            //Make sure user must be authenticated but allow unauthenticated access to the logon screen
            configuration.ForAllControllers().DenyAnonymousAccess();
            configuration.For<AccountController>(ac =&gt; ac.LogOn()).Ignore();

            //first deny access to all users except Administrators
            configuration.ForAllControllersInNamespaceContainingType<AdminController>()
                .DenyAuthenticatedAccess()
                .RequireRole(RolesEnum.AdminRole.ToString());

            //If any users have access to part of the admin console they will access to the admin dashboard
            configuration.For<AdminController>().RequireRole(RolesEnum.AdminRole.ToString(), RolesEnum.LegalRole.ToString());

            //grant access to any other controllers in the admin area
            configuration.For<LegalController>().RequireRole(RolesEnum.AdminRole.ToString(), RolesEnum.LegalRole.ToString());

        });

        return SecurityConfiguration.Current;
    }

    public static bool UserIsAuthenticated()
    {
        var currentUser = HttpContext.Current.User;
        return !string.IsNullOrEmpty(currentUser.Identity.Name);
    }

    public static IEnumerable<object> UserRoles()
    {
        var currentUser = HttpContext.Current.User;
        return string.IsNullOrEmpty(currentUser.Identity.Name) ? null : System.Web.Security.Roles.GetRolesForUser(currentUser.Identity.Name);
    }
}

Finally configure our system to use FluentSecurity in Global.asax.cs

using System.Web.Mvc;
using System.Web.Routing;

using FluentSecurity;

namespace FluentSecurityAndMvcSiteMapProvider
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode,
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());

            //required for FluentSecurity
            filters.Add(new HandleSecurityAttribute(), 0);
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );
        }

        protected void Application_Start()
        {
            //Configure FluentSecurity
            SecurityHelper.SetupFluentSecurity();

            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

Integrating FluentSecurity with MvcSiteMapProvider

With security working I needed to filter my site map so unauthenticated users can’t see menu items they shouldn’t. Becase we love to user MvcSiteMapProvider I could have easily just added a role attribute to each mvcSiteMapNode but then we are doubling up on security definitions and I just can’t have that.

To hook into MvcSiteMapProvier and use our own Visibility provider we needed to do a few things

  • Create a new function in the SecurityHelper to check if a user has access to a particular action
  • Create a new class implementing ISiteMapNodeVisibilityProvider

Creating ActionIsAllowedForUser. The one thing that is important to note about this function is that GetContainerFor asks for a parameter named controllerName. This is actually after the controllerNamespace.

public static class SecurityHelper
{
    public static ISecurityConfiguration SetupFluentSecurity()
    {
        //...
    }

    public static bool UserIsAuthenticated()
    {
        //...
    }

    public static IEnumerable<object> UserRoles()
    {
        //...
    }

    public static bool ActionIsAllowedForUser(string controllerNamespace, string actionName)
    {
        var configuration = SecurityConfiguration.Current;

        var policyContainer = configuration.PolicyContainers.GetContainerFor(controllerNamespace, actionName);
        if (policyContainer != null)
        {
            var context = SecurityContext.Current;
            var results = policyContainer.EnforcePolicies(context);
            return results.All(x =&gt; x.ViolationOccured == false);
        }
        return true;
    }

Now we can implement a SiteMapNodeVisibilityProvider to call ActionIsAllowedForUser. Lucky MvcSiteMapProvider provides us with this wonderful function that will get the full controller namespace for us!

using System.Collections.Generic;
using System.Web;

using MvcSiteMapProvider;
using MvcSiteMapProvider.Extensibility;

public class ReviumVisibilityProvider : ISiteMapNodeVisibilityProvider
{
    public bool IsVisible(SiteMapNode node, HttpContext context, IDictionary sourceMetadata)
    {
        // Convert to MvcSiteMapNode
        var mvcNode = node as MvcSiteMapNode;
        if (mvcNode == null)
        {
            return true;
        }

        //First check the visibility based on user roles
        string controllerNamespace = (new DefaultControllerTypeResolver()).ResolveControllerType(mvcNode.Area, mvcNode.Controller).FullName;
        bool isVisible = SecurityHelper.ActionIsAllowedForUser(controllerNamespace, mvcNode.Action);
        if (!isVisible)
            return false;

        //Process Other Visibility Rules
        //...

        return true;
    }
}

And that’s it! We now have a site that full implements FluentSecurity for validation, displays the appropriate Access denied or login page and displays the correct site map foe each user.

Checkout a working version here FluentSecurityAndMvcSiteMapProvider.zip

Related posts:

  1. Creating a generic settings repository in C#
  2. Asp.Net MVC convert View to Word Document
  3. Modified properties in LINQ to SQL
  4. Sharepoint – Getting Photos from People and Groups
  5. ASP.NET MVC3 – Application_Error not firing log4net

Tags: asp.net, c#, FluentSecurity, MVC3, MvcSiteMapProvider

« Responsive Web Design

Creating a generic settings repository in C# »

9 Responses to “FluentSecurity + MvcSiteMapProvider = Better .Net Security Management”

  1. Scott says:
    July 3, 2012 at 11:26 pm

    Great post.

    However I have one question. I downloaded the example and implemented the same code into my solution. There is a method missing:

    DefaultPolicyViolationHandlerIs

    I have downloaded the same version of FluentSecurity and MvcSiteMapprovider from NuGet.

  2. Mark Wiseman says:
    July 4, 2012 at 9:57 am

    If you used nuget you need to make sure you use 2.0.0-alpha1 for this example
    http://nuget.org/packages/FluentSecurity/2.0.0-alpha1

  3. Numen says:
    August 15, 2012 at 11:42 am

    Great article ! It meets all my requirement ,execellent!
    just I could not find the last class file of “ReviumVisibilityProvider ” ?why
    I get the file from the link you provided in the middle of the article ”

    FluentSecurityAndMvcSiteMapProvider.zip “

  4. Sumit says:
    August 15, 2012 at 12:44 pm

    I have a requirement where roles are not fixed, it is defined in DB. A role is nothing but a set of action to which the user has access to. In MVC terms action is the controller action.

    A action should be fetched from DB and checked when a user request a controller action. Which means it does not matter which role I belong, I should always check if the user has access to perform that action.

    Say for example AddEmployee(), UpdateEmployee() are two action in AdminController. A custom role can be created which have AddEmployee and UpdateEmployee rights. Again a custom role can be created which has access to only AddEmployee.

    How can we implement this in Fluent Security

  5. Mark Wiseman says:
    August 15, 2012 at 1:49 pm

    @Numen: ReviumVisibilityProvider can be found in the MvcSiteMapHelper.cs file

  6. Mark Wiseman says:
    August 15, 2012 at 2:02 pm

    @Sumit
    You could probably do it by enhancing SecurityHelper.SetupFluentSecurity().

    Without trying to get into reflection you could just define a configuration.For for each action and get the required roles from the database

    string[] roles = GetRolesForAction(“AddEmployee”);
    configuration.For().RequireRole(roles);

    Update SecurityHelper.UserRoles to return your dynamic role names for the current user
    return GetFolesForUser(HttpContext.Current.User.Identity.Name);

    And you should be fine. Each time you create a new role you would need to call SecurityHelper.SetupFluentSecurity(); again. I’m not sure how that will work though.

  7. Sumit says:
    August 16, 2012 at 1:05 am

    Thanks. Let me try this and will let you know hows that works. Thanks

  8. Naheeda says:
    August 17, 2012 at 10:49 pm

    I have requirement in which i have apply authorization on control. For e.g. button and link should be disable as per the user role and action.

  9. Mark Wiseman says:
    August 22, 2012 at 9:23 am

    FluentSecurity is really aimed at securing routes and not individual elements in a view. I perform those security checks separately.

Leave a Reply

Click here to cancel reply.

Recent Articles

  • ISAF Sailing World Cup
  • Revium Supports the Prostate Cancer Foundation of Australia
  • Kentico FAQ Module
  • Advanced Visitor Tracking in Analytics
  • Kentico, Smart Search and filtering attachments
  • Enhancing JIRA’s Issue Navigator
  • Mobile Browsing
  • Revium help win gold for Australia

Twitter

  • All things Gold! http://t.co/9DkkjmAr 2012-09-13
  • Mat Belcher - our favourite London Gold Medalist dropped into the office to say thank you. http://t.co/TxHbe2y6 2012-09-13
  • You beauty - http://t.co/1kbcBZwg #london2012 @belcherpage2012 2012-08-10
  • More updates...

Revium Logo

  • Home
  • About
  • Expertise
  • Showcase
  • Contact

  • news
  • blog
  • sandbox
  • twitter
  • rss
  • visit our facebook page

We are Revium, hear us roar!

The news.

26 Feb

Revium Supports the Prostate Cancer Foundation of Australia

We here at Revium are proud to say that we are supporting the Prostate Cancer Foundation of Australia, this month we have been able…

Continue reading
View archive

The blog.

11 Apr

ISAF Sailing World Cup

Mat Belcher and his team have been successful in taking out round 3 of the ISAF Sailing World Cup in Palma de Mallorca a…

Continue reading
View archive

The sandbox.

20 Dec

Kentico, Smart Search and filtering attachments

We had a scenario recently where we had 2 indexes set up in Kentico to search different folders within our site. Everything worked as…

Continue reading
View archive

eNewsletter.

 

© Copyright 2013. All Rights Reserved.

Revium Pty Ltd

info@revium.com.au Work +61 3 9429 2000

10 Harvey Street
Richmond, Victoria, 3121 Australia
View map

Disclaimer and privacy Revium Pty Ltd

Find us: web development, seo

 
Partner logos