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.
(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 )
);
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
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
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
=begin
TODO: order :actions alphabetically
expects an array of strings like that output by
Introspector.find_loaded_controller_actions
=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
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
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.
(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 )
);
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
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
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
=begin
TODO: order :actions alphabetically
expects an array of strings like that output by
Introspector.find_loaded_controller_actions
=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
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