The Big Bang Rewrite (Part 3): Authentication, Authorization, and Tests
One of the biggest deficiencies in the old application was being able to lock down access to the site based on users. Originally, there were two special users, Admin and Guest. The former had complete control, the later had none. Anyone else was a normal user and had the same level of access.
Since we have an opportunity to build this right from the ground up, we explored 3 levels of access control:
- Authentication: Actions behind this layer of protection, first, require a user to be authenticated.
- Authorization: These actions assume a user has been authenticated but determine whether they have access to perform the requested actions.
- Permissions: Assuming the first two layers pass, does the current user have permission to perform that action to that object?
I’ll go into the details of each section, then describe our testing strategy.
Step one: plug in Authlogic. This did a huge majority of the heavy lifting for us to ensure that a user session was persisted request to request.
Let’s take a look at some of the helpers in our base controller, ApplicationController:
Our custom exception
Exceptions::Security::AuthenticationRequired is systematically caught and returns a HTTP 401. Piece of cake. To lock down a controller now, we simply add a
before_filter to the actions we’d like restricted to users that are logged in.
Authentication was easy, but now that we know we have a user, should they be able to hit that action? To the end user, or a business analyst these may seem like the same question, but as a developer I think it makes sense to separate the concerns.
Because we followed REST conventions in this application, the abstraction for authorization was self apparent: restrict access on a controller/action basis. So we created roles which stored sets of controller action pairs. Then add another
before_filter just like only a bit different than our authentication layer.
Just like authentication, we add a
before_filter and catch the AuthorizationRequired exception, returning a HTTP 403 response to the client.
Our entire authorization system now covers us up to the controller action. But there’s still one more step. Does that user have permission to do that action on a particular object. In our case, can a user edit a specific business?
What we have actually looks a lot like Ryan Bate’s CanCan plugin.
My feelings on testing are a subject for another discussion, but in a nutshell, I believe it’s crucially important to test permissions. I think the reasons are obvious. But testing permissions can be a real pain in the neck.
We use Shoulda to assist our testing process for two reasons. Firstly, we enjoy the dry-ness and organization of contexts. Beyond organization, however, the macros allow your tests to be very expressive while staying super clean - this is especially true of controllers. As you can see in the samples below, however, we avoid the stresses surrounding assertion readability with breadcrumbs (e.g. a User unit test might read, “validations > first_name”).
As discussed above, each user belongs to a role. To satisfy our authorization layer, a role then specifies which controllers and actions may be accessed. So we need to be able to systematically hit all the controllers and actions that require authorization with each role and assert the appropriate behavior.
This could easily get out of hand:
There’s a few issues here. First of all, there’s a lot of duplication. More importantly, we duplicate the
get, which is really a common procedure to all these tests. Secondly, we’re retesting that this action
should_do_all_sorts_of_great_stuff and binding those assertions to the setup of the test. Gross.
Shoulda controller macros encourage you to avoid duplicating the setup block over and over again, so I dove into creating a custom macro, designing it by implementation first:
So, much better! But how to make that work?
Very cool, but if you’ve been paying close attention there’s one more method I haven’t touched on yet,
login. Let’s take a look at the test_helper.
This helper is probably the most robust part of the entire test. If you pass nothing in, as we do in the
setup block, it will simply fake it. This is great because the assertions become oblivious to the authorization mechanism. Sweet. But the authentication and authorization assertions do care, so we give ourselves the option to login as a specified role. Our
should blocks utilize the
:before option to perform the login before anything else happens. Importantly, though, it sets a flag so the second
login call (the one in the original
setup block) doesn’t then screw things up.