This one's going to be a big one, be prepared for reading it in parts.

Base Auth's readme is of course available on its GitHub page, so make sure you have it open for reference.

Authorization By Controller

Guest User

First things first: make sure you have current_user method available in your controllers. If you're using Restful Authentication or Binarylogic's Authlogic for authentication - and you definitely should - you already have it and we can move to the next point.

Now, Base Auth generally uses instance methods of current_user, even for checking for guest user, and since implementing a "guest user" model is generally a good idea (default name like "Anonymous Coward" etc.), let's make it work:

app/models/user/guest.rb (yes, it's in user subdirectory - we're namespacing after all):

class User::Guest < User
  def validate
    errors.add(nil, 'Guest user cannot be saved.')
  end
end

app/models/user.rb:

class User < ActiveRecord::Base
  def is_guest?
    self.class == User::Guest
  end
end

Now, because we need to have a current_user always available, even if not logged in, let's add one more possibility of getting current_user. Here's the version for restful_authentication users - you can put it in ApplicationController class or AuthenticatedSystem module:

@current_user ||= (login_from_session || login_from_basic_auth || login_from_cookie || User::Guest.new) unless @current_user == false

Now we can begin our adventure with Base Auth by protecting one of our controllers from guest users:

class NotForGuestsController < ApplicationController
  deny :user => :is_guest?
end

Of course it's theoretically equal to before_filter :login_required, but instead of redirecting to login form, it throws an Authorization::PermissionDenied exception, with which you can do whatever you like. For example:

class ApplicationController < ActionController::Base
 
  rescue_from ActiveResource::ResourceNotFound, :with => :resource_not_found
  rescue_from ActiveRecord::RecordNotFound, :with => :resource_not_found
  rescue_from Authorization::PermissionDenied, :with => :permission_denied
  
  def permission_denied
    render :text => "You don't have permissions to access this page.", :status => 403, :layout => true
  end
  
  def resource_not_found
    render :text => "This page doesn't exist", :status => 404, :layout => true
  end
end

I must admit, guest user handling is not where Base Auth shines. It does when it comes to securing user-owned content though.

User-owned content

This can be accomplished in two ways. One of them is to explicitly call allow! (or deny! if our app has a twisted and negative verification) in given action for a given resource:

class TasksController < ApplicationController
  deny :user => :is_guest?
  def edit
    @task = Task.find(params[:id])
    allow! :user => :owns?
  end
end

We're RESTful, right? Our app's too, so it means our controllers are a very thin layer between client and our models, so we usually name our controllers after the models they expose.

Base Auth is going to automagically guess the name of instance variable to check against current user, by singularizing controller's name and prepending it with "@". This way in TasksController it's going to look for @task variable, but we can of course tell base_auth to authorize using different variable:

class TasksController < ApplicationController
  def edit
    @something = Task.find(params[:id])
    allow! :user => :owns?, :object => @something
  end
end

This one takes a lot of coding and we'll usually have to repeat it through (almost) all the actions, so it's better to use base_auth as before_filter and grab the resource in another before_filter. You probably did it already (DRY!), and base_auth can make good use of it:

class AccountsController < ApplicationController
  before_filter :get_resource
  allow :show, :edit, :update, :destroy, :user => :owns?
  deny :user => :is_guest?
  
  protected
  
  def get_resource
    @account = Account.find(params[:id]) if params[:id]
  end
end

There's still one thing to implement: User#owns?(object) method:

class User < ActiveRecord::Base
  def owns?(target)
    #return true if self.is_admin?
  
    if target.respond_to?( :owned_by? )
      target.owned_by?( self )
    elsif target.respond_to?( :owner_id )
      self.id == target.owner_id
    else
      false
    end
  end
  
  def owner_id
    self.id
  end
end

Of course you can implement it differently, or make use of user_id field instead owner_id. Or make owner_id an alias to user_id in owned resource... or do anything you like. User#owns?(target) has to work and that's the only constraint.

Not even that - you can use different method name passed to base auth's allow (like :user => :can_edit? and implement User#can_edit?(target)) to implement some more complex authorization logic.

Authorization By Model

This one has a pretty humble implementation at the moment, but lets you authorize by model and keep amounts of authorization code in your controllers as small as possible. That's basically the way it works:

class TasksController < ApplicationController
  deny :user => :is_guest?
  def edit
    @task = Task.find(params[:id]).authorize!(current_user)
  end
end

All you have to implement is Model#authorize(user) method in given model's class. For instance:

class Task < ActiveRecord::Base
  def authorize(user)
    user.user_id == self.owner_id
  end
end

And even that only if your Model#authorize method is going to differ from base_auth's default implementation:

module ModelAuthorization
  module InstanceMethods
    #re-implement this in your model if you check authorization in a different way
    def authorize(user)
      user.id == self.user_id
    end
  end
end

More complex authorization logic in authorize-by-model, like authorize_for!(:edit, current_user) about to become implemented in base_auth after some testing.

Ending

Base Auth is not perfect as in "drop in and use automagically without step 3 - perfect". We didn't want to overload it with convenience methods and defaults, as authorization is a pretty serious topic and working differently across different applications. That's why there has to be some code in the application, because everything that was common to our applications is already in base_auth. The rest has to be decided by application developer. We just wanted authorization to follow the KISS principle.

We're open to suggestions though, so feel free to comment and contact us with questions and ideas.

1 Response to “Base Auth - a complete tutorial to securing your Rails application”

  1. College Campus Says:

    great. very useful. we can use this to our site.

Leave a Reply