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

Comments

  1. Andre said 14 days later:

    Awesome. This is going to save me a bunch of time. Thanks for putting this together.

  2. Tom Styles said 3 months later:

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

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

    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.

  3. wolfman said 3 months later:

    Excellent thanks, I'll add it to the example.

  4. wanderer said 6 months later:

    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.

  5. wolfmanjm said 6 months later:

    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.

  6. grace said 8 months later:

    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.

  7. Mythril said 9 months later:

    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)
           end
    
    

    Hopefully this will be helpful for other people.

  8. sw0rdfish said about 1 year later:
    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?
  9. wolfmanjm said about 1 year later:

    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.

  10. sgarza said over 2 years later:

    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)

Trackbacks

Use the following link to trackback from your own site:
http://blog.wolfman.com/articles/trackback/2

(leave url/email »)

   Comment Markup Help Preview comment