Access Levels in Rails

Authorization in rails can be achieved in many ways. One of the most popular ways to achieve this is through can can. With can can, all permissions are defined in a single location, the Ability class.

I'm not a fan of this approach because:

  • I think the controllers should be the ones handling the authorization;
  • The DSL doesn't offer a lot of flexibility and makes it harder for nested resources;
  • It's not easy to keep everything organized as the number of resources and access levels increase.

There are other approaches and gems to handle authorization in Rails like declarative authorization and authlogic but they look too complicated and little bit outdated. Nevertheless you should, also, check them out.

My approach

In my opinion authorisation logic should be in the controllers. My approach is to put actions with different access levels in different controllers nested in different modules and use inheritance between the controllers to avoid duplication.

Next, I'll show an example with a small application where i'll put into practice this approach.

Message Board

Message Board is a small app that allows users to create message boards.

A message board is a private space where it's members can post stuff that
will be viewed by the other members.

Any user is allowed to create a new message board. In order to view or post
into a message board, a user has to be invited by one of the message board's
admins. The creator of a message board is automatically added as an admin of
the message board. After he or her invites other users to the message board,
he or her can promote any member to admin.

By reading the description of the app, we can already identify three access levels:

  • User - Any authenticated user.
    • Can create a new Board.
  • Board Member - A user that is a member of given board.
    • Can view the board page.
    • Can post a new message to the board.
  • Board Admin - A user that is an admin member of given board.
    • Can do everything that a Board Member can.
    • Can add new members to the board.
    • Can delete messages from the board.

Controllers

app/controllers
├── BoardAccess
│   ├── Admin
│   │   ├── base_controller.rb
│   │   ├── board_members_controller.rb
│   │   ├── boards_controller.rb
│   │   └── messages_controller.rb
│   ├── Normal
│   │   ├── base_controller.rb
│   │   ├── boards_controller.rb
│   │   └── messages_controller.rb
│   └── board_access_base_controller.rb
├── application_controller.rb
└── boards_controller.rb

The first access level is public so it's handled by the BoardsController located in app/controllers/boards_controller.rb.

The folder BoardAccess contatins a class BoardAccessBaseController that serves as superclass for every controller in the module BoardAccess.

module BoardAccess  
  class BoardAccessBaseController < ApplicationController
    before_filter :authenticate_user!
    before_filter :load_board
    before_filter :control_access!

  private
    def authenticate_user!
      permission_denied unless user_signed_in?
    end

    def load_board
      id = if params[:board_id]
        params[:board_id]
      else
        params[:id]
      end
      @board = Board.find id
    end

    def control_access!
      permission_denied unless grant_access?
    end

    def grant_access?
      raise 'Abstract method.'
    end

    def redirect_path
      board_path @board
    end
  end
end  

Here we define three before filters. authenticate_user! makes sure that the user accessing the action is properly authenticated. load_board creates an instance variable @board with the provided board id. This is possible because every action performed by the controllers who inherit from this class are either related to the boards resource or other resource that is nested under boards. Finally control_access! checks if the user has permission to perform a given action. It makes use of the grant_access? method that returns true if should be granted access to the current user. This last method should be defined in the BoardAccessBaseController subclasses.

We also have two nested modules, Normal and Admin. Each module represents an access level. Each one of these modules has a BaseController class. These classes implements the grant_access? method left blank in BoardAccessBaseController.

require_relative '../board_access_base_controller.rb'

module BoardAccess  
  module Normal
    class BaseController < BoardAccess::BoardAccessBaseController      

    private      
      def grant_access?
        @board.is_member? current_user
      end
    end
  end
end  

I the case of the Normal module, the BaseController class checks if the current_user is a member of the selected board.

require_relative '../board_access_base_controller.rb'

module BoardAccess  
  module Admin
    class BaseController < BoardAccess::BoardAccessBaseController

    private
      def grant_access?
        @board.is_admin? current_user
      end      
    end
  end
end  

On the Admin module, the BaseController class checks if the current_user is an admin of the selected board.

To complete the cycle, let's look at a controller defined within one of these modules. For example the MessagesController nested in the BoardAccess/Normal module.

module BoardAccess  
  module Normal
    class MessagesController < BaseController
      def create
        @message = @board.messages.create!(params[:message]){ |m|
          m.board_member = @board.member_from_user current_user
        }
        redirect_to redirect_path
      end
    end
  end
end  

Thanks to the top hierarchy, in this controller, we don't need any concern about authorisation and we can focus on the functional part of the application. The only think that we should take into account, is that the controller should inherit from BaseController instead of ApplicationController.

Routing

Now that our controllers are all setup, how do we handle the routing? We can either use namespaces or specify a module directly on the resource. I usually prefer the second approach because it gives us shorter url's but it depends on the use case.

MessageBoard::Application.routes.draw do  
  devise_for :users

  resources :boards, only: [:new, :create]

  # Normal Board Member Access Level
  resources :boards, only: [:show], module: 'board_access/normal' do
    resources :messages, only: [:create]
  end

  # Admin Board Member Access Level
  resources :boards, only: [:destroy], module: 'board_access/admin' do
    resources :messages, only: [:destroy]
    resources :board_members, only: [:create]
  end
end  

For the Message Board application, I went with the second approach.

Wrapping up

What I proposed here was a method to create access levels in a rails application using nested modules and controller inheritance. In the example application we defined a module BoardAccess to wrap the different access levels, namely Normal and Admin. In order to keep ourselves from writing repeated code we added some inheritance to our controllers and we used the top hierarchy classes to handle the authorization process.

PS: You can find the full source code for the Message Board application here.