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:
{% highlight ruby %}
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
{% endhighlight %}
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:
{% highlight ruby %}
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
{% endhighlight %}
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:
{% highlight ruby %}
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
{% endhighlight %}
Besides simplifying the authorization, since `has_right?` is a method of a `User`, any `user`'s rights can be simply interrogated, which is 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`:
{% highlight ruby %}
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
{% endhighlight %}
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.