Fluent Security configuration
for ASP.NET MVC

FluentSecurity provides a fluent interface for configuring security in ASP.NET MVC.
No attributes or nasty xml, just pure love.
Go get it on NuGet!

PM> Install-Package FluentSecurity

Version 2.0.0
ASP.NET MVC 3 and ASP.NET MVC 4

Getting started

The bare essentials

Add the following at the end of Application_Start in your Global.asax and you're good to go!
The example is based on the Internet Application template for ASP.NET MVC 3.

SecurityConfigurator.Configure(configuration =>
{
	// Let FluentSecurity know how to get the authentication status of the current user
	configuration.GetAuthenticationStatusFrom(() => HttpContext.Current.User.Identity.IsAuthenticated);

	// This is where you set up the policies you want FluentSecurity to enforce on your controllers and actions
	configuration.For<HomeController>().Ignore();
	configuration.For<AccountController>().DenyAuthenticatedAccess();
	configuration.For<AccountController>(x => x.ChangePassword()).DenyAnonymousAccess();
	configuration.For<AccountController>(x => x.LogOff()).DenyAnonymousAccess();
});

GlobalFilters.Filters.Add(new HandleSecurityAttribute(), -1);

An extended example

The example is based on the Internet Application template for ASP.NET MVC 3 with the addition of a Blog controller and a security helper class.

SecurityConfigurator.Configure(configuration =>
{
	// Let FluentSecurity know how to get the authentication status of the current user
	configuration.GetAuthenticationStatusFrom(() => HttpContext.Current.User.Identity.IsAuthenticated);

	// Let FluentSecurity know how to get the roles for the current user
	configuration.GetRolesFrom(() => MySecurityHelper.GetCurrentUserRoles());

	// This is where you set up the policies you want FluentSecurity to enforce
	configuration.For<HomeController>().Ignore();

	configuration.For<AccountController>().DenyAuthenticatedAccess();
	configuration.For<AccountController>(x => x.ChangePassword()).DenyAnonymousAccess();
	configuration.For<AccountController>(x => x.LogOff()).DenyAnonymousAccess();

	configuration.For<BlogController>(x => x.Index()).Ignore();
	configuration.For<BlogController>(x => x.AddPost()).RequireRole(BlogRole.Writer);
	configuration.For<BlogController>(x => x.AddComment()).DenyAnonymousAccess();
	configuration.For<BlogController>(x => x.DeleteComments()).RequireRole(BlogRole.Writer);
	configuration.For<BlogController>(x => x.PublishPosts()).RequireRole(BlogRole.Owner);
});

GlobalFilters.Filters.Add(new HandleSecurityAttribute(), -1);

Policies

FluentSecurity provides a small set of policies that should be enough for the basic scenarios.

DenyAnonymousAccessPolicy
The user must be authenticated. Requires no specific role.
DenyAuthenticatedAccessPolicy
The user must be anonymous.
RequireAnyRolePolicy
The user must be authenticated with one or more of the specified roles.
RequireAllRolesPolicy
The user must be authenticated with all of the specified roles.
IgnorePolicy
All users are allowed.
DelegatePolicy
The specified delegate must return true or a success result (PolicyResult).

Create your own policy

If needed, you can create your own policies by implementing the interface ISecurityPolicy. You can then add the policy to your configuration like this: configuration.For<SomeController>().AddPolicy(new MyCustomPolicy());.

Configuration

Bootstrapping

The part that enforces your policies is the action filter called HandleSecurityAttribute. When you add it to your global filters collection, make sure that you put your call to SecurityConfigurator.Configure(...) before registering the action filter or it will blow up.

GlobalFilters.Filters.Add(new HandleSecurityAttribute(), -1);

Ignoring missing configurations

By default FluentSecurity will throw an exception if a missing configuration is encountered for a controller action. If you don't want FluentSecurity to handle security for all controllers you can tell it to ignore missing configurations. You can do this by adding configuration.IgnoreMissingConfiguration(); to your configuration expression.

Testing

The FluentSecurity.TestHelper project will help you get the configuration you want and enables you to testdrive your applicaiton like you would with any other part of your system. It can be used with any of the major testing frameworks (like NUnit, MbUnit, xUnit.net and MS Test).

To start writing tests for your FluentSecurity configuration you need to add a reference to FluentSecurity.TestHelper or just install the FluentSecurity.TestHelper NuGet package. Next you'll want to add a test fixture. Below is an example using NUnit.

[TestFixture]
public class When_security_has_been_configured
{
	[Test]
	public void Should_have_expected_configuration()
	{
		// Arrange
		Bootstrapper.ConfigureSecurity();

		// Act
		var results = SecurityConfiguration.Current.Verify(expectations =>
		{
			expectations.Expect<HomeController>().Has<IgnorePolicy>();
			expectations.Expect<AccountController>().Has<DenyAuthenticatedAccessPolicy>();
			expectations.Expect<AccountController>(x => x.LogOff())
						.Has<DenyAnonymousAccessPolicy>()
						.DoesNotHave<DenyAuthenticatedAccessPolicy>();
		});

		// Assert
		Assert.That(results.Valid(), results.ErrorMessages());
	}
}

You need to make sure that FluentSecurity is configured before you can get to the current configuration so that is the first thing you'll do. In this case Bootstrapper.ConfigureSecurity() is just a wrapper around SecurityConfigurator.Configure(...) and could be named whatever you want. Putting your configuration in a bootstrapper class like this will make it easier for you to test FluentSecurity in isolation.

Now we can get to the current configuration using SecurityConfiguration.Current. Next we use the Verify(expectations => {}) extensions method passing in our expectations like shown in the above example.

The Verify extension returns an IEnumerable<ExpectationResult> with an expectation result for each expectation. To find out if our expectations are met we use the extension method Valid(). The extension method ErrorMessages() will give you a string representation of any expectations that has not been met.

Using your testing framework of choice you should assert that results.Valid() is true and provide results.ErrorMessages() as the failure message for that assertion.