Wolfmans Howlings

A programmers Blog about Ruby, Rails and a few other issues

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
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...

 &lt;a href="#" onclick="checkAll('permissions_<%= controller_id %>[]'); return false;"\>all&lt;/a>
 &lt;a href="#" onclick="uncheckAll('permissions_<%= controller_id %>[]'); return false;"\>none&lt;/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

Posted in  | Tags , ,  | 10 comments | no trackbacks