Unit Testing and Declarative Security, Part 1
Security is hard. If you think it isn't your software is probably just waiting to be exploited by this guy:
Knowing this, the guys who put together the .Net framework gave us a pretty brain-dead approach to adding security declaratively1 to our code using the PrincipalPermissionAttribute. Securing a method call becomes trivial this way.
[PrincipalPermission(SecurityAction.Demand, Authenticated = true, Role = "007")]
public Secret GetSuperSecretStuff(){
return Vault.SuperSecret;
}
Anybody who calls this method will have to be operating under a security context that is both authenticated and a member of the "007" role. However, calling this method without meeting the requirements will throw a big fat Exception. This can make unit testing a bit tricky since you most likely will not have the proper credentials.
For instance suppose the above method were defined on a SecretAgent type in my project and I wanted to write a unit test for it:
[TestMethod]
public void ShouldGetSuperSecretFromAgent()
{
var agent = new SecretAgent();
var secret = agent.GetSuperSecretStuff();
Assert.IsNotNull(secret);
}
Running this code will result in a big fat exception, and your test output will read:
Test method TechnoFattie.Lib.Tests.SecretAgentBehavior.ShouldGetSuperSecretFromAgent threw exception:
System.Security.SecurityException: Request for principal permission failed.
Bummer :(
Getting around this is pretty easy though if you understand how security works in the .Net framework.
Most security in .Net is centered around checking the Principal on the currently executing thread to see if it contains a specific role, and using the PrincipalPermissionAttribute is no exception.
You could think of using security declared in this way as having wrapped every call to GetSuperSecretStuff like this:
if(Thread.CurrentPrincipal.IsInRole("007")){
var secret = GetSuperSecretStuff();
}
else{
throw new SecurityException("Request for principal permission failed.");
}
What we need is a way to completely control the Principal. Fortunately for us, replacing the principal is as simple as setting a property. You can assign any principal you want to Thread.Current.Principal so long as it implements the IPrincipal interface.
If we look the IPrincipal interface, we will notice it is pretty darn simple:
public interface IPrincipal
{
IIdentity Identity { get; }
bool IsInRole(string role);
}
You will also notice that it contains an Identity property, another very simple interface:
public interface IIdentity
{
string AuthenticationType { get; }
bool IsAuthenticated { get; }
string Name { get; }
}
We could implement our own classes here and use them, but I generally prefer the path of laziness. And wouldn't you know it, the hard working guys at Microsoft decided they would spare us the grueling work of implementing three entire properties and one method. Having pity on the lesser developers like myself, they gave us...
GenericPrincipal and GenericIdentity
Standing on the shoulders of these giants we can rewrite our test method to take advantage of the no doubt hundreds of man hours required to not only implement these classes, but to carefully name them as well.
[TestMethod]
public void ShouldGetSuperSecretFromAgentUsingGenericPrincipalAndIdentity()
{
var awesomeSauce = new GenericPrincipal(
new GenericIdentity("jbond"), new[] { "007" }
);
Thread.CurrentPrincipal = awesomeSauce;
var agent = new SecretAgent();
var secret = agent.GetSuperSecretStuff();
Assert.IsNotNull(secret);
}
Now our test will happily pass, and life is good once again. Security can seem like a somewhat mystical thing in .Net, but as you can see the underlying mechanics are pretty simple. In the next installment I am actually going to implement a custom Principal type in order to be able to test more complicated security scenarios, and to reduce the amount of code needed to ease maintenance.
1. Don't mistake this approach for a real security solution however. Ideally you should have already checked the user's credentials before hand and the method would never get called. This method is really only good as a fail-safe.