Ruby on Rails
DiscoveringControllersAndActions (Version #17)

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 :
<pre> =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$/ =~ filename
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

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.

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

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 :
<pre> =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$/ =~ filename
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

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.

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