Wednesday, November 26, 2008

Users, Roles, Rights and Sights

Chad Fowler's Rails Recipes book lays out authorization as the interrogation of the many-to-many connections between users and roles, and between roles and rights, a right being a named controller-action pair. The many-to-many relationships are established using roles_users and rights_roles tables in the database.

This indirection makes bulk assignment of rights easy, simply by assigning roles to a user, and authorizing using nested detection:
class ApplicationController
before_filter :check_authorization
def check_authorization
unless @operator.roles.detect{|role|
role.rights.detect{|right|
right.action == action_name &&
right.controller == self.class.controller_path } }
flash[:notice] =
"You are not authorized to view the requested page"
request.env["HTTP_REFERER"] ?
(redirect_to :back) : (redirect_to home_url)
end
end
end
Here, @operator is the user that is operating the application.

This works fine, but the interrogation of rights seems a little too removed from the user. I prefer asking if a user has a particular right directly:
class User
def has_right?(controller,action)
rights =
User.find_by_sql [
"select * FROM rights ri, rights_roles rr, roles_users ru where"+
" ru.user_id = ? and ri.controller = ? and ri.action = ?"+
" and rr.role_id = ru.role_id and ri.id = rr.right_id",
id, controller, action ]
rights.size > 0
end
end
I also took the liberty of dropping the in-Ruby detection since a single sql query is faster than the multiple smaller queries that detection requires (look at the log files - one query for each role.)

So this is a bit faster and is a drop-in replacement for the detection in the original code:
class ApplicationController
def check_authorization
unless @operator.has_right?(self.class.controller_path,action_name)
flash[:notice] =
"You are not authorized to view the requested page"
request.env["HTTP_REFERER"] ?
(redirect_to :back) : (redirect_to home_url)
end
end
end
Besides simplifying the authorization, since has_right? is a method of a User, any user's rights can be simply interrogated, which useful in setting up rights administration for an application.

Formulating the right-checking in this way also leads to the notion of sight-checking, that is checking if a user has the right to see something. For instance, when building out a page if there is a question as to whether or not a user is allowed to see something, a sight can be established that allows it to be seen. The absence of a requested sight in the rights table for that user's roles implies that the user should not see the sight.

Sights don't necessarily depend on controller-actions; typically they're just checking to see that the user has a right with a particular name. The code to check sights is a simple as that of checking rights:
class User
def has_sight?(name)
sights =
User.find_by_sql [
"select * FROM rights ri, rights_roles rr, roles_users ru where"+
" ru.user_id = ? and ri.name = ?"+
" and rr.role_id = ru.role_id and ri.id = rr.right_id",
id, name ]
sights.size > 0
end
end
Checking for sights looks for a Right's name, while checking for rights looks for a Right's controller and action. Moving the interrogation of rights to the user and adding the notion of sights allows pages to be constructed more simply based on what a particular user is allowed to do.

No comments: