Blocking an Admin Dashboard from Non Authorized Users in Rails
This guide explains how to manage access levels to the admin dashboard in a Rails application, including working with single table inheritance and the Administrate gem.
Guide Tasks
  • Read Tutorial

Now that we have our bright and shiny Admin Dashboard, now it's time for us to ensure that only authorized users are able to access it. If you remember back a few guides ago we implemented a new AdminUser class using single table inheritance. In this guide we'll use that class in order to have a clean way to interface with our permission structure. The components that we'll use to build this feature are:

  • RSpec for testing
  • AdminUser class for ensuring only authorized users access the dashboard
  • Administrate for the dashboard itself

In connecting each of these items it would be easy to create a messy implementation that would be difficult to scale. Therefore our goal should be to ensure that there is a low level of coupling between the modules. For example, if we decided to add another type of users that could access the dashboard, such as a SuperAdmin class. We should not have to make changes throughout the application. So let's keep this in mind while building.

Implementing Tests

Let's begin with writing RSpec tests for what we want our feature to accomplish. We'll start with ensuring that the dashboard can be reached, essentially giving our tests a base case. Let's first create a file that will keep all of the specs related to the admin dashboard:

touch spec/features/admin_dashboard_spec.rb

Finding the Admin Path in Administrate

Before we can create an expectation, we need to know what naming structure that the Administrate gem uses for the dashboard. We can find this by running the command rake routes | grep admin and we'll see the full set of routes.

large

That admin_root looks promising, let's try that in our expectation.

# spec/features/admin_dashboard_spec.rb

require 'rails_helper'

describe 'admin dashboard' do
  it 'can be reached successfully' do
    visit admin_root_path
    expect(page.status_code).to eq(200)
  end
end

Running the tests will show that it's working, so at this stage we're all green.

large

Next we'll create a test that checks that a user not of the type AdminUser is able to access the page.

# spec/features/admin_dashboard_spec.rb

  it 'cannot be reached by a non admin users' do
    user = FactoryGirl.create(:user)
    login_as(user, :scope => :user)

    visit admin_root_path

    expect(current_path).to eq(root_path)
  end

This test logs in a non admin user and then attempts to visit the admin dashboard. From that point it checks to see if the user is redirected to the homepage. As expected this test fails since right now we haven't implemented any permission structure for the dashboard route.

large

Integrating Permission Structure

Thankfully, the Administrate gem provides a way to easily check if a users is an admin or not. Let's open the Admin::ApplicationController file and add a check for the admin inside of the authenticate_admin method:

# app/controllers/admin/application_controller.rb

module Admin
  class ApplicationController < Administrate::ApplicationController
    before_filter :authenticate_admin

    def authenticate_admin
      unless current_user.try(:type) == 'AdminUser'
        flash[:alert] = "You are not authorized to access this page."
        redirect_to(root_path)
      end
    end

  end
end

This is one of the rare times when I'll use the Ruby unless conditional. However in this case I like how it reads, essentially explaining what we want to happen in nearly plain language. The method is saying unless the user is an AdminUser, give them this message and redirect them to the homepage.

This is a handy way to implement authentication for the full app dashboard. If you run the tests now you'll see that they're all passing. Let's also test this in the browser. Let's first log in as a regular user. As you'll see if we try to access the admin dashboard we'll discover that we're redirected to the homepage and have been informed that we're not authorized to access that page.

large

Now if we log out and log back in as an admin user we'll see that we can access the page, so this is working properly.

large

Updating the tests

Because this is such an important feature, let's add another test to ensure that authorized admins are able to access the dashboard. Right now we're only checking for the negative case: where an unauthorized user tries to access the admin site. But in order for our tests to be comprehensive we should test for both types of users. Let add in the following spec:

# spec/features/admin_dashboard_spec.rb

  it 'cannot be reached by a non admin users' do
    user = FactoryGirl.create(:admin_user)
    login_as(user, :scope => :user)

    visit admin_root_path

    expect(current_path).to eq(admin_root_path)
  end

After running rspec you'll see that all of our tests are passing.

Refactor

Everything is working nicely. I can only see one element that I want to refactor right now. If you remember to the start of the lesson, I said that our implementation should be scalable. I explicitly said that we should have the ability to have multiple admin types. However our current setup would require us to add multiple conditionals in the Admin::ApplicationController, which would be a poor practice. Let's create a new method that can store a list of admin types, and place it in the Admin module itself.

The ApplicationController is a data flow class, and therefore it would be a poor choice for placing a list of admin types, however the Admin module seems like a good fit for this method. As a rule of thumb, in OOP, classes should be able to be described in a single sentence. For example, our Admin::ApplicationController file can describe itself by saying:

"I control the data flow for the admin dashboard"

However if we choose to list our admin user types in it, the class would say:

"I control the data flow for the admin dashboard and I list the admin user types"

As soon as a class description includes an and or or, it means that it's probably time to create a new class or module. stepping off OOP design soapbox...

Next we'll want to have the authenticate_admin method look inside our new list of admin types to see if the current user matches any of them. The final code should look like this:

# app/controllers/admin/application_controller.rb

module Admin
  def self.admin_types
    ['AdminUser']
  end

  class ApplicationController < Administrate::ApplicationController
    before_filter :authenticate_admin

    def authenticate_admin
      unless Admin.admin_types.include?(current_user.try(:type))
        flash[:alert] = "You are not authorized to access this page."
        redirect_to(root_path)
      end
    end

  end
end

If you run the tests you'll see that they're all passing and now we have a more scalable approach to admin types.

What's Next?

Everything is looking good with this feature and our permission structure looks solid for the admin dashboard. In the next guide we'll walk through the various features and customizations available with the Administrate gem.

Resources