Simple Ruby on Rails Authorization

Dan Croak

The following describes a simple approach to Ruby on Rails authorization that re-uses the domain model to do the heavy lifting.

Routes:

resources :accounts, only: [:new, :create, :show]

resources :brands, only: [:new, :create, :show] do |brands|
  brands.resources :offers, only: [:new]
end

Brands belong to accounts. Offers belong to brands. Users belong to accounts.

I prefer flat routes (and no subdomains) when at all possible. It keeps the mental overhead low everywhere in the app.

Authentication

Users are authenticated using Clearance. They have a account_id foreign key.

With an authenticated user in a typical “account” application, we can lean on Clearance’s :authorize before filter and ActiveRecord finders.

class BrandsController < ApplicationController
  before_filter :authorize

  def new
    @brand = current_user.account.brands.build
  end

  def create
    @brand = current_user.account.brands.build(params[:brand])
    # ...
  end

  def show
    @brand = current_user.account.brands.find(params[:id])
  end
end

With this pattern, the user is restricted to interacting with brands to which they have access through their account.

Test at the controller level

it 'does not find brands not associated with user' do
  brand = create(:brand)
  sign_in_as create(:user)

  assert_raises(ActiveRecord::RecordNotFound) do
    get :new, brand_id: brand.to_param
  end
end

Rails returns a 404 when ActiveRecord::RecordNotFound is raised. This error will be raised in our access control scheme because there is no record of the current_user having a relationship to this brand.

Let’s get to green:

class OffersController < ApplicationController
  before_filter :authorize

  def new
    @brand = current_user.brands.find(params[:brand_id])
    @offer = @brand.offers.build
  end
end

User belongs to accounts and Account has many brands. I could have said current_user.account but I kept the chain from the perspective of the controller shorter using delegation:

class User < ActiveRecord::Base
  include Clearance::User

  belongs_to :account

  delegate :brands, to: :account
end

This will make my life easier when the rules around users’ relationship to brands get more complex.

Too lightweight

This authorization approach requires few lines of code and no extra gem dependencies beyond Rails and Clearance. It leans heavily on the framework, stays DRY, and uses normal authentication and RESTful conventions. It’s easy to test and I know where those tests should go.