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