An Admin page for Rails Role Based Authentication
Posted by Jim Morris on Sat May 20 13:19:00 -0700 2006
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 end
The 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 end
the 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 endShow