Ruby on Rails
DiscoveringControllersAndActions

The LoginGeneratorAccessControlList entry says that the Permissions table should have entries with “controllername/actionname” for permitting or disallowing access to a particular action, but doesn’t discuss how to create such entries.

One way is to manually type them into some form. I feel that this is prone to missing certain actions. My admin system instead has the system discover what controllers and actions are available (introspection or inspection on the system), so that I can present them to the user in a multi-select.

Limitations
  1. The following code only works for controllers directly in the ‘app/controllers’ directory, not those in subfolders. (See the revised code at the bottom of the page for a solution)
  2. I haven’t found a good way to run the following code once per app launch, so instead I have the Permissions table update its entry list each time the admin form to edit a role is opened up.

DB Schema

(I’m using PostgreSQL here)

create table Users (
  id serial primary key,
  /*...*/
);

create table Roles (
  id serial primary key,
  name varchar(100) not null
);

create table Permissions (
  id serial primary key,
  name varchar(100) not null
);

create table Roles_Users (
  role_id integer references Roles on delete cascade,
  user_id integer references Users on delete cascade,
  primary key ( role_id, user_id )
);

create table Permissions_Roles (
  permission_id integer references Permissions on delete cascade,
  role_id integer references Roles on delete cascade,
  primary key ( permission_id, role_id )
);

Model Setup

class User < ActiveRecord::Base
  has_and_belongs_to_many :roles
  #...
end

class Role < ActiveRecord::Base
  has_and_belongs_to_many :users
  has_and_belongs_to_many :permissions
end

class Permission < ActiveRecord::Base
  has_and_belongs_to_many :roles

  # Ensure that the table has one entry for each controller/action pair
  def self.synchronize_with_controllers
    # Load all the controller files
    # ToDo: hunt sub-directories
    Find.find( RAILS_ROOT + '/app/controllers' ) do |file_name|
      load file_name if /_controller.rb$/ =~ file_name
    end

    # Find the actions in each of the controllers, 
    # resulting in an array of strings named like "foo/bar" 
    # representing the controller/action
    all_actions = ObjectSpace.subclasses_of( ApplicationController )
    all_actions.collect! do |controller|
      controller.action_methods.collect do |method_name|
        controller.name.gsub( /Controller$/, '' ).downcase + '/' + method_name
      end
    end.flatten!

    # Find all the 'action_path' columns currently in my table
    all_records = self.find_all
    known_actions = all_records.collect{ |permission| permission.name }

    # If controllers/actions exist that aren't in the db
    # then add new entries for them
    missing_from_db = all_actions - known_actions
    missing_from_db.each do |action_path|
      self.new( :name => action_path ).save
    end

    # Clear out any entries in the table that do not
    # correspond to an existing controller/action
    bogus_db_actions = known_actions - all_actions
    unless bogus_db_actions.empty?
      #Create a mapping of path->Act instance for quick deletion lookup
      records_by_action_path = { }
      all_records.each do |permission|
        records_by_action_path[ permission.name ] = permission
      end

      bogus_db_actions.each do |action_path|
        records_by_action_path[ action_path ].destroy
      end
    end
  end

end

Defining the #subclasses_of Method

The synchronization code uses a method I wrote, which must be included somewhere that is loaded at the right time. I have it in my /lib/basiclibrary.rb file.

def ObjectSpace.subclasses_of( parent_class )
  subclasses = []
  self.each_object( Class ) do |klass|
    subclasses << klass if klass.ancestors.include?( parent_class )
  end
  subclasses
end

Synchronizing the Permissions Table

class RoleController < ApplicationController
  def edit
    #...
    Permission.synchronize_with_controllers
    @all_actions = Permission.find_all.sort_by{ |perm| perm.name }
    #...
  end
end

See also HowToMakeSitemapWithIntrospection


Question: after invoking synchronize_with_controllers via role/edit, in the immediate next action I invoke I am getting the following error:

A copy of ApplicationController has been removed from the module tree but is still active!

I have narrowed it down to the first line where load (also tried require) is called for each file in the controllers folder. If I comment this line, I don’t get the error, but it does not find the new controllers and actions.

Is anybody else getting this error? I started getting this error when I updated to Rails 1.2.5. It used to work fine before that. Thanks in advance to anybody answering this…


It seems that you have forgotten to write the code of the methods :
action_methods.
It would be very cool if you post it soon.
Thanks a lot


Replace action_methods with public_instance_methods. This will give you both the public and hidden methods.

Instead of:

controller.action_methods.collect do

I did:


methods = controller.public_instance_methods - controller.hidden_actions

methods.collect do

After some blood sweat and tears, I added the sub-directory functionality. The main change that needed to take place was changing the subclasses_of method. I don’t have a basiclib.rb library so I just added the function to my permission model. This probably isn’t the best way to do it so if you know a better way, make it happen. Here’s the revised permission model:


class Permission < ActiveRecord::Base
    has_and_belongs_to_many :roles

    # Ensure that the table has one entry for each controller/action pair
    def self.synchronize_with_controllers
        # Load all the controller files
        Find.find( RAILS_ROOT + '/app/controllers' ) do |file_name|
            if File.basename(file_name)[0] == ?.
                Find.prune
            else
                if /_controller.rb$/ =~ file_name
                    load file_name 
                end
            end
        end
        # Find the actions in each of the controllers, 
        # resulting in an array of strings named like "foo/bar" 
        # representing the controller/action
        all_actions = Object.subclasses_of(ApplicationController)
        all_actions.collect! do |controller|
            methods = controller.public_instance_methods - controller.hidden_actions
            methods.collect do |method_name|
                #ignore methods ending with a ? and the 'l' method
                if /\?$/ =~ method_name || "l" == method_name
                    nil
                else
                    controller.name.gsub( /Controller$/, '' ).downcase.gsub(/::/, '/') + '/' + method_name
                end
            end
        end.flatten!.compact!

        all_actions

        # Find all the 'action_path' columns currently in my table
        all_records = self.find_all
        known_actions = all_records.collect{ |permission| permission.name }

        # If controllers/actions exist that aren't in the db
        # then add new entries for them
        missing_from_db = all_actions - known_actions
        missing_from_db.each do |action_path|
            self.new( :name => action_path ).save
        end

        # Clear out any entries in the table that do not
        # correspond to an existing controller/action
        bogus_db_actions = known_actions - all_actions
        unless bogus_db_actions.empty?
            #Create a mapping of path->Act instance for quick deletion lookup
            records_by_action_path = { }
            all_records.each do |permission|
                records_by_action_path[ permission.name ] = permission
            end

            bogus_db_actions.each do |action_path|
                records_by_action_path[ action_path ].destroy
            end
        end
    end

    # This method was taken from:
    # dev.rubyonrails.com/file/trunk/activesupport/lib/active_support/core_ext/object_and_class.rb
    # I removed the condition for finding '::' so that the sub directory classes would work
    def Object.subclasses_of(*superclasses)  
         subclasses = []
        ObjectSpace.each_object(Class) do |k|
          next if (k.ancestors & superclasses).empty? || superclasses.include?(k) || subclasses.include?(k)  
            subclasses << k
         end  
        subclasses
    end
end

Introspector Class

Thanks to the author above. I was doing much the same thing by actually parsing the controller files; this approach seems much nicer so I’ve adopted it.

It seems to me that a nice library class is in order, to get some of that (tasty and reusable) stuff out of model and into a library. That way we could use the same code to say, build an ACL permissions administration page, or a sitemap.

Note: this is ‘very beta’ code but works for what I’m using it for so far. I’ll repost as I refine it. It’s built to work with the LoginGeneratorACLSystem / LoginGenerator combo. Some of this code is based on those wiki pages, some on code above.

In lib/introspector.rb :

 =begin
  support function required by Permission model.
  This method was taken from:
  dev.rubyonrails.com/file/trunk/activesupport/lib/active_support/core_ext/object_and_class.rb
  Removed the condition for finding '::' so that the sub directory classes would work
=end
def Object.subclasses_of(*superclasses)  
  subclasses = []
  ObjectSpace.each_object(Class) { |k|
    next if (k.ancestors & superclasses).empty? || superclasses.include?(k) || subclasses.include?(k)  
    subclasses << k
  }  
  subclasses
end

=begin
  this class loads & examines all controllers, and produces data structures
  telling us what public methods they contain; useful for sitemap generation
  and ACL administration.

  TODO: test. Test with controllers in subdirectories.
=end
class Introspector

=begin
  Finds and loads all controllers in the application. Likely to be expensive; 
  use with restraint.
=end
  def self.load_all_controllers
    require 'find'
    # will this find controllers in subfolders? It's advertised as doing so
    Find.find( RAILS_ROOT + '/app/controllers' ) do |file_name|
      if File.basename(file_name)[0] == ?. # what's this idiom? : ?.  Ans: ? gets the character code for the following character, so in ascii ?. is 46
        Find.prune
      else
        if /_controller.rb$/ =~ file_name
          load file_name 
        end
      end
    end 
  end

=begin
  Find the actions in each loaded controller, resulting in an array of 
  strings named like 'foo/bar' representing the controller/action
=end
  def self.find_loaded_controller_actions
    all_actions = Object.subclasses_of(ApplicationController)
    all_actions.collect! { |controller|
      # TODO : public_only logic fork to be added here ...
      methods = controller.public_instance_methods - controller.hidden_actions
      methods.collect { |method_name|
        #ignore methods ending with a ? and the 'l' method
        if /\?$/ =~ method_name || "l" == method_name
          nil
        else
          controller.name.gsub( /Controller$/, '' ).downcase.gsub(/::/, '/') + '/' + method_name
        end
      }
    }.flatten!.compact!

    all_actions
  end

  def self.find_all_controller_actions
    self.load_all_controllers
    self.find_loaded_controller_actions
  end

=begin
  TODO: order :actions alphabetically

  expects an array of strings like that output by 
  Introspector.find_loaded_controller_actions

  returns an array of hashes containing the :name of each controller, 
  as well as a list of its :actions; ie,
  [ 
    { :name => 'foo', :actions => [ 'index', 'list' ... ] }, 
    { etc... } 
  ]
=end 
  def self.order_action_list_by_controller(action_list)
    ordered_list = []
    action_list.each{ |actionpath|
      splitpath = actionpath.split('/')
      controllername, actionname = splitpath.first, splitpath.last
      if item = ordered_list.select{ |i| i[:name] == controllername }.first
        item[:actions] << actionname
      else
        ordered_list << { 
          :name => controllername, 
          :actions => [actionname]
        }
      end
    }
    ordered_list
  end 

end

then you can have less code in the model, and keep that code concerned with the model-specific logic. My Permission model is exactly as above, except that the functions now performed by the Introspector class are trimmed down to a method call or two.

Note: while I’ve tested and used the Introspector class, I’ve not yet done the same for the Permission.synchronize_with_controllers method below.

require 'introspector'

class Permission < ActiveRecord::Base
  has_and_belongs_to_many :roles

  # Ensure that the table has one entry for each controller/action pair
  def self.synchronize_with_controllers
    all_actions = find_all_controller_actions

    # Find all the 'action_path' columns currently in permissions table
    all_records = self.find_all
    known_actions = all_records.collect{ |permission| permission.name }

    # If controllers/actions exist that aren't in the db
    # then add new entries for them
    missing_from_db = all_actions - known_actions
    missing_from_db.each { |action_path|
      self.new( :name => action_path ).save
    }

    # Clear out any entries in the table that do not
    # correspond to an existing controller/action
    bogus_db_actions = known_actions - all_actions
    unless bogus_db_actions.empty?
      #Create a mapping of path->Act instance for quick deletion lookup
      records_by_action_path = { }
      all_records.each { |permission|
        records_by_action_path[ permission.name ] = permission
      }

      bogus_db_actions.each { |action_path|
        records_by_action_path[ action_path ].destroy
      }
    end
  end  
end

- DaveLee : david [at] davelee.com.au

Hi,

Note that the line above


controller.name.gsub( /Controller$/, '' ).downcase.gsub(/::/, '/') + '/' + method_name

won’t work. You need to use


controller.name.underscore.gsub( /_controller$/, '' ) + '/' + method_name

-BenHoskins