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.