An Admin page for Rails Role Based Authentication
Posted by Jim Morris Sat, 20 May 2006 20:19:00 GMT
This is an example of an admin page for the Rails Recipes book Role Based Authentication, using a tree control and checkboxes for HABTM.
I used the Silverstripe tree control to render two trees, on the left a list of all Roles and what Rights they have. On the right a tree with all controllers and a checkbox for each action for each controller, under that a set of checkboxes that allow you to apply the selected rights to the selected Roles.
This technique seems a good match where the lists are hierarchical so it can cut down on the screen real-estate for very large lists.
Example code...
In the model:
- The Rights class has_and_belongs_to_many :roles
- The Roles class has_and_belongs_to_many :rights
I setup the data by creating hashes in the controller which are available to the view...
In roles_controller.rb...
def edit
# roll everything up into a hierarchical tree structure
# top level is hash of roles
# next level is hash of permissions with controller as key and array of actions as value
@role_tree= {}
roles= Role.find_all
roles.each do |r|
ph= {}
rights= r.rights
rights.each do |p|
ph[p.controller] ||= []
ph[p.controller] << p.action
end
@role_tree[r.name] = ph
end
# creates a tree of controllers with the actions for each controller
# top level is hash of controllers
# next level is an array of actions
@rights_tree= {}
rights= Right.find_all
rights.each do |r|
name= r.controller
@rights_tree[name] ||= []
@rights_tree[name] << r.action
end
endThe view (edit.rhtml) is simple...
<h1>Role and Rights Editor</h1>
<table cellspacing="10" cellpadding="20%">
<tr>
<th align="left">Roles</th>
<th align="left">Rights</th>
<tr>
<td valign="top"><%= render_role_tree(@role_tree) %></td>
<td><%= start_form_tag :action => 'edit' %>
<%= render_rights_tree(@rights_tree) %>
<p>
The checked items above will replace the Rights of the following
Roles:<br>
<% @role_tree.each_key do |role| %>
<%= check_box_tag "role", role %> <%= role %>
<% end %>
<p>
<%= submit_tag "Allocate to selected Roles" %>
<%= end_form_tag %>
</td>
</table>
This part goes in helpers, and creates the html for the two trees this is specific for the silverstripe tree control...
module RoleHelper
def render_role_tree(tree)
ret = ''
ret += "<ul class='tree'>"
tree.each_key do |r|
ret += '<li><a href="#">' + r + '</a>' # list roles
h= tree[r] # get hash of controllers/[actions]
unless h.empty?
ret += '<ul>'
h.each_key do |c|
ret += '<li><a href="#">' + c + '</a>' # list controllers
a= h[c]
unless a.empty?
ret += '<ul>'
a.each do |an|
ret += '<li><a href="#">' + an + '</a></li>' # list actions
end
ret += '</ul>'
end
ret += '</li>'
end
ret += '</ul>'
end
ret += '</li>'
end
ret += '</ul>'
ret
end
def render_rights_tree(tree)
ret = ''
ret += "<ul class='tree'>"
tree.each_key do |r|
ret += "<li><a href=\"#\">#{r}</a>"# list controllers
aa= tree[r] # get array of actions
unless aa.empty?
ret += '<ul>'
ret += "<li><a href=\"#\" onclick=\"checkAll('right[#{r}][]'); return false;\">Check All</a>"
aa.each do |a|
ret += '<li>' + "<input type=\"checkbox\" name=\"right[#{r}][]\" value=\"#{a}\" />" + a + '</li>' # list actions
end
ret += '</ul>'
end
ret += '</li>'
end
ret += '</ul>'
ret
end
endthe action handler in the controller for the post basically goes through all the selected checkboxes and sets the relevant habtms.
def edit
if request.post?
#p params
unless params['role'].nil?
a_roles= params['role']
a_rights= params['right']
unless a_rights.nil?
a_roles.each { |ar|
role= Role.find_by_name(ar)
unless role.nil?
role.rights.clear
a_rights.each { |controller, aa|
aa.each { |action|
right= Right.find_by_controller_and_action(controller, action)
unless right.nil?
role.rights << right
else
puts "Right #{controller}/#{action} not found"
end
}
}
else
puts "Role #{ar} not found"
end
}
flash[:notice]= 'rights updated'
else
flash[:warning]= 'no rights were selected'
end
else
flash[:warning]= 'You need to select the role to allocate to'
end
end
end
Checking and unchecking all the boxes
This is done with a little bit of javascript.
The view code looks like...
<a href="#" onclick="checkAll('permissions_<%= controller_id %>[]'); return false;"\>all</a>
<a href="#" onclick="uncheckAll('permissions_<%= controller_id %>[]'); return false;"\>none</a>
The java script is...
function checkAll(name)
{
boxes = document.getElementsByName(name)
for (i = 0; i < boxes.length; i++)
boxes[i].checked = true ;
}
function uncheckAll(name)
{
boxes = document.getElementsByName(name)
for (i = 0; i < boxes.length; i++)
boxes[i].checked = false ;
}
Automatically adding Rights.
Lastly the question of how to get all those Rights into the table? I liked the approach of the UserEngine so I borrowed it from them (thanks:), Add this to the app/models/right.rb file, then whenever you need to get all the new actions and controllers populated call Right.synchronize_with_controllers from the console, (or create a button on your admin page to do it). Here is the code...
class Right < ActiveRecord::Base
has_and_belongs_to_many :roles
validates_presence_of :controller, :action, :name
validates_uniqueness_of :name
# Ensure that the table has one entry for each controller/action pair
def self.synchronize_with_controllers
# weird hack. otherwise ActiveRecord has no idea about the superclass of any
# ActionController stuff...
require RAILS_ROOT + "/app/controllers/application"
# Load all the controller files
controller_files = Dir[RAILS_ROOT + "/app/controllers/**/*_controller.rb"]
# we need to load all the controllers...
controller_files.each do |file_name|
require file_name #if /_controller.rb$/ =~ file_name
end
# Find the actions in each of the controllers, and add them to the database
subclasses_of(ApplicationController).each do |controller|
controller.public_instance_methods(false).each do |action|
next if action =~ /return_to_main|component_update|component/
if find_all_by_controller_and_action(controller.controller_path, action).empty?
self.new(:name => "#{controller}.#{action}", :controller => controller.controller_path, :action => action).save!
logger.info "added: #{controller} - #{controller.controller_path}, #{action}"
end
end
# The following thanks to Tom Styles
# Then check to make sure that all the rights for that controller in the database
# still exist in the controller itself
self.find(:all, :conditions => ['controller = ?', controller.controller_path]).each do |right_to_go|
unless controller.public_instance_methods(false).include?(right_to_go.action)
right_to_go.destroy
end
end
end
end
end
Awesome. This is going to save me a bunch of time. Thanks for putting this together.
Great stuff, I've made a slight amendment to the code that automatically populates the Right's table, so that it removes any actions that have been tidied up in the controllers. Here it is, I've chopped off the top and bottom to keep it tidy...
Feel free to include this in the tutorial. I'm now calling this function just before the rights page is displayed so I've always got an upto date set of rights to deal with.
Excellent thanks, I'll add it to the example.
Umm .... That piece of code sure looks ugly. Linus Torvalds once said in a heated debated regarding indentation "If your code has more than 3 level deep indentations, it's f**ked up." I gotta agree with him on that one. You need to rework your code. Otherwise, it's a helpful piece.
I have to agree its rather ugly, I don't even remember writing it! It must have been a bad day. Tree walking code is usually ugly unless you use recursion, so I guess one day I'll refactor it.
For complete numpties I would have liked to know exactly where the view code javascript went ... Also, to help, other numpties Type "ruby/script console" in your command prompt at the project root, this will launch irb. Then enter Right.synchronize_with_controllers to preload controllers into the rights table.
I had all kinds of troubles with loading the modules in order to get subclasses_of to work properly, not sure if this is because Rails was changed in the latest release or because we use subfolders for some controllers. Anyway, in the end I went for another approach, where I just get each controller class by parsing the controller files:
# Load all the controller files controller_files = Dir[RAILS_ROOT + /app/controllers/**/*_controller.rb"] controllers = [] # we need to load all the controllers... controller_files.each do |file_name| match = file_name.match(/\/app\/controllers\/(([a-z_]+)\/?[a-z_]+).rb$/) name = match[1].camelize controllers << eval(name) endHopefully this will be helpful for other people.
So I tried doing this, and I realize the post is almost 18 months old or something, I'm just curious. when I put this in my Rights model, I get the following error. Right.synchronize_with_controllers NoMethodError: undefined method `synchronize_with_controllers' for # if I create a Right object I get; rights = Right.new = # rights.synchronize_with_controllers NoMethodError: undefined method `find_all_by_controller_and_action' for # So I posted this in the Role object, added in the appropriate Model references "Right.find_all_by... " and I was able to run it. Has something changed in Rails?
Its quite possible as this was written for quite an old version of rails, and has not been tested with any newer versions.
If someone wants to update it, I'm sure others would appreciate it.
i have this running on rails 2.1
the only modifications to work are:
in roles_controller.rb change this roles= Role.find_all to this roles= Role.find(:all)
and this rights= Right.find_all to this rights= Right.find(:all)