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:

  1. Authentication: Actions behind this layer of protection, first, require a user to be authenticated.
  2. Authorization: These actions assume a user has been authenticated but determine whether they have access to perform the requested actions.
  3. 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:

protected

  def require_authentication
    raise Exceptions::Security::AuthenticationRequired unless authenticated?
  end

private

  def authenticated?
    !current_user.nil?
  end

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.

protected

  def require_authorization
    raise Exceptions::Security::AuthorizationRequired unless authorized?
  end

private

  def authorized?
    # authorized_for just checks to see if that key value pair exists for that user based on their role
    current_user.authorized_for?(self.controller_name, self.action_name)
  end

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:

class UsersControllerTest < ActionController::TestCase
  context 'GET to show >'
    context 'as an admin >' do
      setup do
        login_as :admin
        get :show
      end
      
      should_do_all_sorts_of_great_stuff
    end
    
    context 'as a business owner >' do
      setup do
        login_as :business_owner
        get :show
      end
      
      should_do_all_sorts_of_great_stuff
    end
  end
end

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:

class UsersControllerTest < ActionController::TestCase
  context 'GET to show >'
    setup do
      login
      get :show
    end
    
    should_authenticate
    should_authorize :admin, :business_owner
    should_do_all_sorts_of_great_stuff
  end
end

So, much better! But how to make that work?

class ActionController::TestCase
  def self.should_authenticate
    should 'authenticate', :before => lambda { logout } do
      assert_response :unauthorized
    end
  end
  
  def self.should_authorize(*role_names)
    role_names.each do |role_name|
      should "authorize #{role_name}", :before => lambda { login(role_name) } do
        assert @response.response_code != :forbidden
      end
    end
  end
end

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.

def login(arg = nil)
  if arg.nil?
    unless @skip_next_login
      @controller.expects(:require_authentication).returns(true)
      @controller.expects(:require_authorization).returns(true)
    end
  elsif arg.is_a?(String) || arg.is_a?(Symbol)
    @controller.stubs(:current_user).returns(Factory(:user, :role_name => arg.to_s))
    @skip_next_login = true
  end
end

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.

Go back to part one or two.

Thanks for reading! I'm Avand.

I’ve been working on the web for over a decade and am passionate about building great products.

I devote most of my time to building Lopery, a free budgeting that helps people spend with confidence, recover from the unexpected, and achieve financial independence. I used simple (but time consuming) budgeting principles to buy my first home. Now, I'm codifying (pun intended) those same principles into an easy to use app that helps people achieve their financial goals.

My last job was with Airbnb, where I focused on internal products that helped teams measure the quality of the software they were building. I also built internal tools for employees to stay more connected, especially after the COVID-19 pandemic. Before that, I was lead engineer at Mystery Science, the #1 way in which science is now taught in U.S. elementary school classroms. For a while, I also taught with General Assembly, teaching aspiring developers the basics of front-end web development.

I was born in Boston, grew up in Salt Lake City, and spent many years living in Chicago. In 2013, I came out West to San Francisco, which I called home for almost a decade. Now, I’m based out Mariposa, in the foothills of the Sierras.

I enjoy the great outdoors and absolutely love music and dance. Cars have been an lifelong obsession of mine. I’m the proud owner of a 2002 E-250 Sportsmobile van, and he and I have enjoyed many trips to beautiful and remote parts of the Pacific North West spreading good vibes. I also have a very soft spot for magic (slight of hand, in particular). I love the feeling of being inspired and absolutely love inspiring others.

What can I do for you?

Read my other posts or get in touch: