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.
Authentication
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.
Authorization
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.
Permissions
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.
Testing
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.