Loading...
Loading...
Marco Franssen

When building a large web application with ASP.NET MVC 3, I ran into a problem: how could I secure it in a maintainable way? There are lots of examples using attributes, but I found those hard to maintain. I started searching for other solutions, and after finding more of the same attribute-based approaches, I finally found "FluentSecurity".
What does FluentSecurity offer you?
Fluent Security provides a fluent interface for configuring security in ASP.NET MVC. No attributes or nasty xml, just pure love. Go get it on NuGet!
What does that mean?
Well, it means you can bootstrap your security from Application_Start() and maintain it in a single place. You can also unit test your security configuration to check that it matches the policies you've defined in your tests. This is a huge advantage: you can check how your controller actions are secured without clicking through the entire web application. Of course, those checks depend on defining your tests correctly. That's not all… You can easily extend or modify the behavior by adding your own implementations of the interfaces.
Let me show you some code so you can see for yourself how easy it is! My example is based on a default MVC 3 application. I've added a CategoryController, shown below, to give us some extra actions to secure.
public class CategoryController
{
[HttpGet]
public ActionResult AddNewCategory()
{
return View(new CategoryModel());
}
[HttpPost]
public ActionResult AddNewCategory(CategoryModel model)
{
if (!ModelState.IsValid) return View(model);
//Save the data etc…
return RedirectToAction("AddNewCategory");
}
//Other actions
}First, I create a static class for my bootstrap code. I'll leave the implementation blank for now because I want to write some tests first.
public static class SecurityBootstrapper
{
public static void BootUp()
{
//Here we will configure our security later on…
}
}Then we can start writing tests for the security configuration. I'd advise you to be very explicit in your tests, even though you don't have to be. This makes the intended policies clear and lets you check that you've secured each action the way you want. Rather than writing one test for the whole controller, test each specific action.
[TestFixture]
public class FluenSecuritySetupTests
{
[SetUp]
public void SetUp()
{
BootStrapper.BootUp();
}
[Test]
public void anonymous_access_should_be_allowed_for_the_logon_and_home_index_actions()
{
var results = SecurityConfiguration.Current.Verify(expectations =>
{
expectations.Expect<UserController>(c => c.LogOn(string.Empty)).Has<IgnorePolicy>();
expectations.Expect<HomeController>(c => c.Index()).Has<IgnorePolicy>();
});
Assert.That(results.Valid(), results.ErrorMessages());
}
[Test]
public void adding_a_new_catogegory_requires_a_system_administrator_role()
{
var results = SecurityConfiguration.Current.Verify(expectations =>
{
expectations.Expect<CategoryController>(c => c.AddNewCategory()).Has(new RequireRolePolicy(AppRoles.SystemAdministrator));
expectations.Expect<CategoryController>(c => c.AddNewCategory(null)).Has(new RequireRolePolicy(AppRoles.SystemAdministrator));
});
Assert.That(results.Valid(), results.ErrorMessages());
}
}Once we've defined the tests for our security configuration, we can implement it.
public static class SecurityBootstrapper
{
public static void BootUp()
{
SecurityConfigurator.Configure(configuration =>
{
configuration.ResolveServicesUsing(type => BootStrapper.Container.ResolveAll(type).Cast<object>());
configuration.GetAuthenticationStatusFrom(() => HttpContext.Current.User.Identity.IsAuthenticated);
configuration.GetRolesFrom(Roles.GetRolesForUser);
configuration.ForAllControllersInAssembly(typeof(HomeController).Assembly).DenyAnonymousAccess();
configuration.For<HomeController>(c => c.Index()).Ignore();
configuration.For<UserController>(c => c.LogOn()).Ignore();
configuration.For<UserController>(c => c.ResetPassword()).RequireRole(AppRoles.UserAdministrator);
configuration.For<CategoryController>(c => c.AddNewCategory()).RequireRole(AppRoles.SystemAdministrator);
configuration.For<CategoryController>(c => c.AddNewCategory(null)).RequireRole(AppRoles.SystemAdministrator);
}
}
}As you've probably noticed, we configure security for both the GET and POST actions. Now we can run our tests to see whether the implementation matches the policies we defined. When all the tests pass, we're ready to enable it in the web application and define handlers for policy violations.
public class MvcApplication : HttpApplication
{
public static IWindsorContainer Container { get; private set; }
protected void Application_Start()
{
SecurityBootstrapper.BootUp();
Container = new WindsorContainer().Install(FromAssembly.This());
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
//Other members left for brevity…
}First, we call SecurityBootstrapper.BootUp() in Global.asax to configure security. Then I use Castle Windsor as my dependency injection container to register the dependencies. Of course, you can use your favorite IoC container or wire up your own implementation. To register my FluentSecurity dependencies, I implemented a Windsor installer.
public class WindsorInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(AllTypes.FromThisAssembly().BasedOn(typeof(IPolicyViolationHandler)).Configure(h => h.LifeStyle.Singleton));
}
}As you can see, I register all the policy violation handlers in my assembly. Here are a couple of example implementations.
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
//Log the violation, send mail etc. etc.
return new HttpUnauthorizedResult(exception.Message);
}
}
public class RequireRolePolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
//Log the violation, send mail etc. etc.
var rvd = new RouteValueDictionary(new
{
area = "",
controller = "Error",
action = "HttpForbidden",
statusDescription = exception.Message
});
return new RedirectToRouteResult(rvd);
}
}FluentSecurity maps these violation handlers by naming convention. For example, a RequireRolePolicy needs a RequireRolePolicyViolationHandler.
I hope this gives you a good impression of what FluentSecurity can do. There are lots of ways to create your own components when the defaults don't work for you. It's open source and available on GitHub, so you can contribute improvements. The documentation is pretty good, and Kristoffer Ahl really helped me with a few small issues when I first tried it. Please share the article if you liked it, and give FluentSecurity a try. Have fun!
Marco Franssen
Unblock downloaded files in Windows with PowerShell, preview changes with WhatIf, and use Recurse to include files in subfolders.
Marco Franssen
Use an old 256 MB USB stick to boot Windows PE, connect to a network share, and install Windows 8 without storing the full ISO on the USB drive.
Marco Franssen
Reduce repetitive C# equality comparer classes with a delegate-based IEqualityComparer and LINQ Distinct extension methods for property comparisons.
Marco Franssen
Install Windows 8 Consumer Preview on a virtual hard disk alongside Windows 7, from creating a bootable USB drive to configuring the first boot.