Sunday, 22 June 2008

Centralized Static Authorization Using Just a Hash

Deciding how your site authorization will be implemented is not a simple part. You can implement centralized authorization, distribute it over the controllers or let each model exposes its authorized resources. Of course that depends on how your authorization process will be (static, dynamic, roles appear at run time, etc).

Using centralized authorization:

  1. Define one entry point for authorization. So, you can easly disable/enable it.
  2. You will maintain only one scope of code without touching the other business logic code.
  3. If code failure happened, you can easily isolate it from the application process without dropping down the whole logic.
  4. Other business logic is independent and don't aware of the authorization. So, you can easily upgrade your authorization process if you need without touching other application parts.

In RoR, if your authorization will be static over a predefined list of roles, then you can easily implement your authorization check based on user/controller/action triple using the magic word "hash". This will done by creating a nested hash of the following levels:

  • Level one: Users Roles (Admin, Client, etc)
  • Level two: Controllers (Users, Products, Items, etc)
  • Level Three: Actions (Index, Create, Update, Destroy, etc)

Now, you need to catch the request, extract the request entities (requesting user role, controller and action) and then hash these keys to see whither this request is authorized or not.

Lets see small example. If you have two user roles in your application; Admin and User. And you have two controllers; Products and Users. Your hash will be as following:


@roles = {:Admin => {
:Users => :all,
:Products => :all
}
:User => {
:Users => {:update => :all}
:Products => {:index => :all, :show => :all}
}
}

The above hash gives the "Admin" user role the whole access to the both controllers and the whole controller actions. But, for "User" role, he can request "update" action in "Users" controller and "index" or "show" actions in the "Products" controller only.

Note I am using the value ":all" as the end of my roles permissions tree. So, it means "full access" either on the whole controller's actions or specific action.

The next step is putting this roles and the checking code into small module, include it in your ApplicationController then call the "authorize" method using before_filter.

module Authorization

@roles = {:Admin => {
:Users => :all,
:Products => :all
}
:User => {
:Users => {:update => :all}
:Products => {:index => :all, :show => :all}
}
}

def authorize
# check controllers list for the requested user
@controllers_list = @roles[@user.class.to_s.to_sym]
if (@controllers_list.nil?)
# no controllers permitted for this user role
render :text => "", :status => :unauthorized
return false
else
# check actions list inside the controller
@actions_list = @controllers_list[controller_name.to_sym]
if (@actions_list.nil?)
# the requested controller is not permitted
render :text => "", :status => :unauthorized
return false
elsif (@actions_list == :all)
return true
else
# check the called action
@called_action = @actions_list[action_name.to_sym]
if (@called_action.nil?)
render :text => "", :status => :unauthorized
return false
elsif (@called_action == :all)
return true
else
render :text => "", :status => :unauthorized
return false
end
end
end
end

end

class ApplicationController <>

include Authorization

before_filter :authorize

..............

end

You can enlarge or shrink the hash as you need (add/remove roles, controllers, etc) but the "authorize" method code will not change.

No comments: