2008-12-10

Forward linking in Rails with Users and Roles

One of my gripes with Rails is that it doesn't come with a basic permissions system that incorporates users and groups or roles. Show me the modern web application that doesn't have at least an admin user and a public user. Anyway, away from that rant...
The most common way to incorporate users into Rails is to use Restful Authentication (RA). On top of that you can add roles with Role Requirement (RR). Together these two provide you with a very good base wich includes both users (login, logout etc) with roles in a Many-To-Many relationship. Sweet!
I was wondering how to do forward linking (don't know if that's the 100% correct term). And by that I mean, I want to link to a page (Controller/Action) from the current page. But depending on whether the current user (available as current_user within RA and RR) has got permssions to see the page I want to either disable the link or just hide it. I was facing a nightmare, but luckily I started studying the code, and Tim, the author of Role Requirement has already implemented this. Unfortunately Tim didn't name it very eloquently. In fact, I don't think there's a perfect way to name it. He called it 'url_options_authenticate?'. I've created an "alias" (not a real Rails alias; I tried, but failed, and I'm a bit lazy) of it and I've called it 'current_user_can_access?'.
To do this alias, I simply created a new method in /lib/role_requirement_system.rb like this:
def current_user_can_access?(params = {})
url_options_authenticate?(params)
end

But you also have to add this line to the top of the file:
klass.send :helper_method, :current_user_can_access?
Just below the line that activates 'url_options_authenticate?'. Now you should be able to do this in your views (I'm omitting the ERB tags as Blogger hates them):
if current_user_can_access? hash_for_contactss_url
and you can thus decide whether you should show something or not.
You can also use it in conjunction with link_to() and link_to_if()
link_to 'List contacts', :controller => :contacts, :action => :list if current_user_can_access? hash_for_contacts_url
and
link_to_if current_user_can_access?(hash_for_contacts_url), 'List contacts', :controller => :contacts, :action => :list
But as you can see, that's not very D.R.Y, so application_helper.rb to the rescue.
def access_link_to(name, url_hash)
link_to(name, url_hash) if current_user_can_access? url_hash
end

and
def access_link_to_if(name, url_hash)
link_to_if(current_user_can_access?(url_hash), name, url_hash)
end

Use access_link_to() if you only want to show the link when the user can access the page. If he/she can't access the page, there's nothing displayed. Use access_link_to_if() you want the link to be plain text if the user can't access the page, and naturally a link if the user can access the page.
I wanted the non-link to be "greyed out", and as I've already got a helper called "discrete()" my access_link_to_if() now looks like this:
def access_link_to_if(name, url_hash)
if current_user_can_access? url_hash
link_to(name, url_hash)
else
discrete(name)
end
end

Isn't that just happy-dandy-wonderfully-cute-and-cuddly!?
Slight disclaimer: In the code, Tim says that there might be some problems. He also verifies in this discussion. I tried to figure it out, couldn't be bothered, and what testing I've done, it seems to be just fine. Mileage may vary.

No comments: