The following describes a simple approach to Ruby on Rails authorization that re-uses the domain model to do the heavy lifting.
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.
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
: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.
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.