Base Auth - a complete tutorial to securing your Rails application
December 8th, 2008
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.
December 30th, 2009 at 06:54 PM
great. very useful. we can use this to our site.