Wolfmans Howlings

A programmers Blog about Programming solutions and a few other issues

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

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

the action handler in the controller for the post basically goes through all the selected checkboxes and sets the relevant habtms.

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

Posted in Rails  |  Tags ruby,rails,rbac  |  10 comments

Comments

  1. Andre said on Sun Jun 04 11:33:11 -0700 2006
    Awesome. This is going to save me a bunch of time. Thanks for putting this together.
  2. Tom Styles said on Fri Sep 15 18:19:28 -0700 2006
    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 on Fri Sep 15 21:45:37 -0700 2006
    Excellent thanks, I'll add it to the example.
  4. wanderer said on Sun Dec 10 18:38:56 -0800 2006
    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 on Sun Dec 10 19:33:19 -0800 2006
    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 on Thu Feb 01 11:24:52 -0800 2007
    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 on Tue Feb 27 01:16:06 -0800 2007
    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:
    <pre>

           # 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

    </pre>
    Hopefully this will be helpful for other people.
  8. sw0rdfish said on Wed Feb 13 12:42:10 -0800 2008
    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 #<Class:0x545a38>

    if I create a Right object I get;
    > rights = Right.new
    => #<Right id: nil, name: nil, controller: nil, action: nil>
    >> rights.synchronize_with_controllers
    NoMethodError: undefined method `find_all_by_controller_and_action' for #<Right id: nil, name: nil, controller: nil, action: nil>

    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 on Wed Feb 13 21:35:05 -0800 2008
    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 on Tue Jul 01 17:11:51 -0700 2008
    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)

(leave email »)