This summer, I’m learning Ruby on Rails at Metis, a 12-week class taught by some great folks from thoughtbot. This post is part of a series sharing my experience and some of the things I’m learning.

Rails has several strategies to help us separate concerns in our applications. We use separate files and directories for models, views and controllers, for example, and we can nest routes based on how the resources are related to each other.

In addition, Rails permits the use of namespaces to organize our resources and prevent naming conflicts. In this post, we’ll take a look at why this feature is useful by implementing a namespace for admin actions in a sample Craigslist clone Rails app.

Routing

In our version of Craigslist, our app has many categories (i.e., “bikes,” “boats,” “missed connections,” etc.), each with many posts. To start out, our routes look something like this:

resources :categories, only: [:index, :show] do
  resources :posts, only: [:new, :create, :show]
end

We want to give admin users the ability to add categories, so we add the following to our routes.rb:

namespace :admin do
  resources :categories, only: [:new, :create]
end

This gives us routes under the path /admin/categories which are directed to Admin::CategoriesController.

The Controller

There are a few considerations we have for our namespaced controller.

First, Rails expects this controller file to exist in an admin/ folder within app/controllers/ (i.e., at app/controllers/admin/categories_controller.rb).

Second, because the controller is named within the admin namespace, we use the scope resolution operator to define the controller class:

class Admin::CategoriesController < ApplicationController
  # Methods omitted
end

From there, we can define our new and create actions as we normally would.

Note, also, that we still have our non-namespaced categories routes (for show and index) that direct requests to the standard CategoriesController in app/controllers/.

Views

Views that correspond to our Admin::CategoriesController actions are namespaced just like the controller is. When Rails needs these view files, it will look in the app/views/admin/ subfolder.

Additionally, we’ll need to point our path helpers to the right place for namespaced routes. For example, we’ll use paths like new_admin_category_path and form_for [:admin, @category] in our views.

Restricting Access to Admin Actions

So far, we have our admin actions working within the namespace, but we’re not actually restricting access to these actions.

There are a couple of approaches here. We’ll start by adding a simple before_action to the Admin::CategoriesController:

class Admin::CategoriesController < ApplicationController
  before_action :require_admin

  # Methods omitted

  def require_admin
    unless current_user.admin?
      redirect_to root_path
    end
  end
end

We have a current_user method already defined by our authentication system. (I’ve been quite happy using Monban recently.) Additionally, our User model has a boolean admin field, so we can simply redirect if the user isn’t an admin.

Refactoring

This approach works, but it’s not very flexible. If we add other admin actions (say, for filtering posts flagged as spam), we have to redefine this require_admin method in each namespaced controller.

One solution might be to define the require_admin method in our ApplicationController. That’s a step in the right direction, but we would have to remember to add the before_action to each admin controller; these controllers wouldn’t be secure by default.

The solution I prefer is to define a separate class (in app/controllers/) called AdminController that inherits from ApplicationController:

class AdminController < ApplicationController
  before_action :require_admin

  def require_admin
    unless current_user.admin?
      redirect_to root_path
    end
  end
end

Now, each namespaced admin controller can inherit directly (and quite appropriately) from AdminController. Since we’ve defined the before_action in AdminController, actions in any sub-classed controller will be restricted to admin users by default.

We no longer need to list the before_action or define a require_admin method in each of our namespaced controllers; they just inherit from AdminController:

class Admin::CategoriesController < AdminController
  # Methods omitted
end

Simple, effective, and scalable. By extracting the before_action and require_admin logic into a separate parent controller, we’ve made it startlingly easy to add new admin actions in the future.

We’ll thank ourselves later.

Resources