active_acl 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,255 @@
1
+ =Active Access Control Lists (ActiveAcl)
2
+ The ActiveAcl plugin implements a flexible, fast and easy to use generic access control system.
3
+
4
+ ==License
5
+ ActiveAcl is released under the LGPL[http://www.opensource.org/licenses/lgpl-license.php] (Gnu Lesser General Public License) - see the included LICENSE file, too.
6
+
7
+ ==New in 0.2
8
+ * API CHANGE! Renamed the Permission model to Privilege, to stick to common acl language and avoid confusion in the docs. This also means a schema change (simple column and table renaming should do it). has_permission?() is now has_privilege?().
9
+ * added migrations to the plugin
10
+ * look to limitations section for current issues
11
+ * tests completely refactored and moved out of the main distribution
12
+ * several bugfixes
13
+
14
+ ==Features
15
+ * ease of use - uses polymorphic collections for associations.
16
+ * advanced design - uses SQL nested sets for inheritance, thus only needs a single DB query to decide on a permission request.
17
+ * scalable - there are no real benchmarks yet. But the system design is based on http://phpgacl.sourceforge.net, adding object orientation, polymorphism and two levels of caching. PhpGacl claims "A real-world working version with many added layers of complexity supports over 60,000 Accounts, 200 Groups and 300 ACO's." Tests on my dev notebook show 10 - 30 times performance improvements compared to active_rbac.
18
+ * caching - uses instance caching and optionally stores permission results in memcached using timeouts.
19
+ * flexible - grants simple (2D, like <code>current_user.has_permission?(User::LOGIN)</code>) and object level (3D, like <code>admin.has_permission?(Forum::ADMIN, :on => team_forum)</code>) permissions. Can assign and request permissions to and from every ActiveRecord model. No "hardcoded" permissions - all permissions can be assigned at runtime.
20
+ * grouping - permissions are inherited at target and requester side through groups. Every model implementing an SQL nested set tree may be used as a group.
21
+ * ControllerAction model loader: It maps controller actions to the DB so they can be used in permission assignment. The access object is available via calling <code>current_action</code> on the controller.
22
+ * exchangeable DB interface: ActiveRecord and direct MySQL adapter available
23
+ * supports namespaced models and single table inheritance (STI)
24
+ * 100 % C0 code coverage on unit tests
25
+
26
+ ==Limitations
27
+ * At present only one grouping type per model is supported. This could be changed on request but I don't see a use case for it yet.
28
+ * The DBMS has to support subselects. So PostgreSQL, Sqllite and MySQL 5 work, but MySQL 4 does not.
29
+ * At present has_many_polymorphs has a bug that prevents usage with sqllite, hope this is fixed the next days.
30
+ * At present plugin_migrations and loaded_plugins - which are dependencies for active_acl - need edge rails to run. Obrie told me he would release a 1.1.6 version soon. ActiveAcl core is 1.1.6 compatible, but I felt migration support is critical so I decided to add it now.
31
+
32
+
33
+ ==Prerequisites/Installation
34
+ ActiveAcl uses the has_many_polymorphs[http://www.agilewebdevelopment.com/plugins/restful_authentication] plugin. Make shure you've got a recent version, old versions have bugs affecting active_acl.
35
+
36
+ Also required are plugin_migrations and loaded_plugins from <a href="http://pluginaweek.org/" target="_blank">http://pluginaweek.org</a>. Look at their website for installation instructions.
37
+
38
+ You can either download a tar.gz file of active_acl at http://rubyforge.org/projects/activeacl/ or use the subversion repository at svn://rubyforge.org/var/svn/activeacl/trunk for the development version or svn://rubyforge.org/var/svn/activeacl/tags/release-X.X.X for a particular release.
39
+
40
+ In case you used svn from rubyforge, rename the plugin folder from activeacl to active_acl - rubyforge doesn't allow underscores in project names.
41
+
42
+ To set up the needed tables, execute rake db:migrate:plugins PLUGIN=active_acl. You can set the table names by setting the table name in the options array before executing the task.
43
+
44
+
45
+ ==Short summary
46
+ The ActiveAcl system consists of access objects, organized by access groups, that request privileges on each other. Allowing or denying access to a privilege is controlled by ACL (access control list entry) objects. Access objects and access groups can be instances of arbitrary ActiveRecord model classes enhanced by acts_as_access_object and acts_as_access_group. They are associated to ACL entries via polymorphic associations.
47
+
48
+ ===Access objects
49
+ These are basically requesters and targets in the permission system, as for example a User or a Forum model object. In this case a user could act as a requester ("do I have privilege Y?") or target ("does access object X have privilege Y on me?"). A Forum would most certainly be only used as a target, but all access objects can theoretically be used as both requesters and targets. Access objects use the acts_as_access_object macro inside their definition. This registers the model with the ACL system and enhances it with methods like has_privilege?.
50
+
51
+ Every model class must specify an association that is used as the "grouping type" to it. So User may declare has_and_belongs_to_many :user_groups and use acts_as_access_object :grouped_by => :user_groups. You can use a has_and_belongs_to_many or a belongs_to association for this. Common mapping attributes (:join_table, :foreign_key etc.) are supported.
52
+
53
+
54
+ ===Access groups
55
+ The access group model needs to implement a tree with nested set semantics having a "left" and "right" column, e.g. by using the built-in acts_as_nested_set or the much more recommended acts_as_betted_nested_set plugin. Groups are declared with acts_as_access_group.
56
+
57
+ Groups may be used to specify inheritance hierarchies for permissions. So you could have a 'registered users' group as a subgroup of the 'users' group and assign the privilege to log in to this group via an ACL entry. Every user belonging to this group will now be granted the privilege to log in. Then you could add a subgroup to registerd users, 'banned users', and deny the log in privilege for this group. Every user added to this group would now be unable to log in, regardless of beeing in 'registered users' or not, as 'banned users' would override the permission settings of 'registered users'.
58
+
59
+
60
+ ===Privileges
61
+ A privilege object is an object for the thing we wish to define a permission for. So User::LOGIN could be a privilege object for checking a users permission to log in, while Forum::ADMIN might define administration rights on a forum.
62
+
63
+ A privilege object itself is little more than it’s name and id and is usually bound to a constant inside the application, as it is not expected to change at runtime. Privileges are usually created by the developer in the source code and not in the admin frontend, as creating new privilege objects that have no meaning (by code that checks for them) would be pointless.
64
+
65
+
66
+ ===Access Control List (ACL) entries
67
+ ACL entries are the glue between all these objects, defining which requesters and requester groups have access to which privileges, optionally defining target objects and target groups as well. ACL entries are organized by ACL sections, for better overview in the admin screens.
68
+
69
+
70
+ ==Usage
71
+ ===In short
72
+ "No access defined" for a privilege evaluates to "deny". This may be overriden by an explicit "allow" or "deny". Privileges are inherited in requestor and target groups, this means you can override them in subgroups again. Privileges directly assigned to an object always supercede those assigned to groups.
73
+
74
+ ===Simple (2D) permissions
75
+ We want all registered users to be able to log in. We create the User model, the UserGroup model and the User::LOGIN privilege object as described above. Then we create a new ACL entry, set 'allow' to true, add the "registered users" group as requester group, User::LOGIN as privilege and we are done. Every user assigned to "registered users" or a subgroup of it will now be granted access by calling <code>my_user.has_privilege?(User::LOGIN)</code>.
76
+
77
+ ====Simple permissions example
78
+
79
+ class UserGroup < ActiveRecord::Base
80
+ acts_as_nested_set
81
+ acts_as_access_group
82
+ has_and_belongs_to_many :users
83
+ end
84
+
85
+ class User < ActiveRecord::Base
86
+ has_and_belongs_to_many :user_groups
87
+ acts_as_access_object :grouped_by => :user_groups
88
+ privilege_const_set('LOGIN')
89
+ end
90
+
91
+ # assume 'registered_users' exists and users 'john' and 'dr_evil' are members of it but 'anonymous' is not.
92
+ registered_users = UserGroup.find_by_name('registered_users')
93
+
94
+ acl = ActiveAcl::Acl.create :section => ActiveAcl::AclSection.create(:description => 'users')
95
+
96
+ acl.allow = true # true is default
97
+ acl.privileges << User::LOGIN
98
+ acl.requester_groups << registered_users
99
+
100
+ acl.save
101
+
102
+ john.has_privilege?(User::LOGIN) #=> true
103
+ dr_evil.has_privilege?(User::LOGIN) #=> true
104
+
105
+ anonymous.has_privilege?(User::LOGIN) #=> false
106
+
107
+ ===Overriding permissions
108
+ We want to ban specific users from our site. We create another ACL entry, assign the User::LOGIN privilege object, set 'allow' to false and then assign these users as requesters to the ACL entry. The direct permission assignment on the objects overrides the 'allow login' ACL entry from above.
109
+
110
+ ====Overriding permissions example
111
+
112
+ ban_users = ActiveAcl::Acl.create :section => ActiveAcl::AclSection.find_by_description('users')
113
+
114
+ ban_users.allow = false
115
+ ban_users.privileges << User::LOGIN
116
+ ban_users.requesters << dr_evil
117
+
118
+ ban_users.save
119
+
120
+ john.has_privilege?(User::LOGIN) #=> true
121
+ dr_evil.has_privilege?(User::LOGIN) #=> false
122
+
123
+ ===Object level (3D) permissions
124
+ We want to assign forum permissions. We have several privileges (Forum::ADMIN, Forum::READ, Forum::POST etc.), the afore mentioned User and UserGroup models as well as a Forum and a Category model for grouping the forums.
125
+
126
+ If we want to check if a certain user may read in a certain forum, it is not sufficient to check <code>test_user.has_privilege?(Forum::READ)</code> as the target object - in this case a forum - is needed to make a decision. The code to do the check is like <code>test_user.has_privilege?(Forum::READ, :on => teamforum)</code>.
127
+
128
+ To make this work you create a new ACL entry, add Forum::POST and Forum::READ as privileges, set 'allow' to true, add the registered users group as a requester group and the public forums category as a target group to the acl. Now every user belonging to the registered users group or a subgroup of it gains post and read privileges on all forums of the public forums category or a subcategory of it.
129
+
130
+ ====Object level permissions example
131
+
132
+ # Assuming setup as in the above examples
133
+
134
+ class Category < ActiveRecord::Base
135
+ acts_as_nested_set
136
+ acts_as_access_group
137
+ has_many :forums
138
+ end
139
+
140
+ class Forum < ActiveRecord::Base
141
+ belongs_to :category
142
+ acts_as_access_object :grouped_by => :category
143
+ privilege_const_set 'READ' => 'read postings in forum',
144
+ 'POST' => 'reply to threads in a forum'
145
+ end
146
+
147
+ # assume there is a forum 'speakers corner' assigned to the category 'public'.
148
+
149
+ acl = ActiveAcl::Acl.create :section => ActiveAcl::AclSection.create(:description => 'forum')
150
+
151
+ acl.allow = true
152
+ acl.requester_groups << registered_users
153
+ acl.target_groups << Category.find_by_name('public')
154
+
155
+ acl.privileges << Forum::READ
156
+ acl.privileges << Forum::POST
157
+
158
+ acl.save
159
+
160
+ speakers = Forum.find_by_name('speakers corner')
161
+
162
+ john.has_privilege?(Forum::READ, :on => speakers) #=> true
163
+ john.has_privilege?(Forum::POST, :on => speakers) #=> true
164
+ anonymous.has_privilege?(Forum::READ, :on => speakers) #=> false
165
+
166
+ ==CAUTION
167
+ Do not create ACL entries that are on different branches of the inheritance hierarchy and have allow/deny set differently on the same privilege objects. This way it's impossible to tell which permission should take precedence. At present this is by creation date, later entries superceding older ones, but this is most certainly not what you want.
168
+
169
+ Error checking for conflicting ACL entries is high up on the ToDo list.
170
+
171
+ ==Controller Actions
172
+ Defining permissions on controller actions (like "may user x execute AdminController.list ?") is quite a common case but we are facing a problem here: Controller actions have no corresponding DB models so permissions on them can't be easily defined.
173
+
174
+ ActiveAcl solves this by adding a ControllerAction and ControllerGroup model. For every public controller method (=action) there is one ControllerAction object in the DB.
175
+
176
+ On application startup, the plugin loader checks all controller files in app/controllers and loads or creates a ControllerAction object for every action it finds. These objects get cached in a hash in the ActiveAcl module. Every controller now has a method <code>current_action</code> that looks up and returns the access object for the current action, so it can be used for access checks like <code>current_user.has_privilege?(ActiveAcl::ControllerAction::EXECUTE, :on => current_action)</code>.
177
+
178
+ This works nicely for before_filters with authorization checks.
179
+
180
+ A word on the load mechanism: If the action has no corresponding DB entry (it's looked up on method creation by controller and action name) the loader searches for a controller group with the same name as the controller. If it is found, the action is created and assigned to this group. Else the controller group is created as a subgroup to the "unassigned controller actions" group (this name can be changed in the options) and the unassigned action is added to the controller group.
181
+
182
+ So you are free on how to organize your controllers. Maybe create an admin group and a public group and move controllers as a subgroup inside them?
183
+
184
+ ==Caching
185
+ The plugin provides two levels of caching. The instance cache is a hash inside the access object. The object first tries to serve a permission request from the instance cache. If it is not found and a simple permission is requested, the query fetches all simple permissions of the object and puts them in the instance cache. The reason for this is that there is no noticable speed penalty in fetching all 2D permissions at once compared to fetching only one, so this will save time and DB IO later on. Complex 3D queries are fetched independently and also saved to the instance cache. The instance cache lives inside the access object, so it has it's lifetime, too - which in rails usually is no more than a single request.
186
+
187
+ The second level cache tries to overcome this limitation by putting the instance cache of an access object in an external cache. It tries to get the instance cache from there if it is not set, and sets it if it was changed. The only real implementation for now is with the memcache daemon. The second level cache uses a timeout (which can be defined in the options) to expire the cached permissions.
188
+
189
+ Instance and second level cache can be expired explicitly by calling <code>clear_cached_permissions</code> on the access object. Calling <code>reload</code> on the object also purges the caches.
190
+
191
+ See ActiveAcl::Cache::MemcacheAdapter on how to set it up.
192
+
193
+
194
+ ==Preloader
195
+ The plugin includes a <code>load_files_from filenames</code> function. It can be used to preload source files (and therefore the classes in it) from an application path and should be used from environment.rb.
196
+
197
+ load_files_from("#{RAILS_ROOT}/app/controllers/**/[^.]*.rb")
198
+ load_files_from("#{RAILS_ROOT}/app/models/**/[^.]*.rb")
199
+
200
+ will load all models and controllers inside these folders and their subfolders. This way you can be shure they are registered with the ACL system at rails boot time - else they will be registered when they are called for the first time. This means that new controllers will not show up in the admin screens until they were accessed if not using the preloader.
201
+
202
+ ==Options
203
+ <code>ActiveAcl::OPTIONS</code> is an array that can be used to override various options for the system by setting the values in environment.rb. <code>ActiveAcl::DEFAULT_OPTIONS</code> is as follows:
204
+
205
+ DEFAULT_OPTIONS = {
206
+ :acl_sections_table => 'acl_sections',
207
+ :acls_privileges_table => 'acls_privileges',
208
+ :acls_table => 'acls',
209
+ :privileges_table => 'privileges',
210
+ :requester_links_table => 'requester_links',
211
+ :target_links_table => 'target_links',
212
+ :requester_group_links_table => 'requester_group_links',
213
+ :target_group_links_table => 'target_group_links',
214
+ :controller_actions_table => 'controller_actions',
215
+ :controller_groups_table => 'controller_groups',
216
+
217
+ :controllers_group_name => 'unassigned_controller_actions', # the name of the base group that newly created controller groups get assigned to
218
+ :controller_group_name_suffix => '_controller', # name suffix for generated controller groups
219
+
220
+ :cache_permission_timeout => 10, # timeout in seconds for the second level cache
221
+
222
+ :db => ActiveAcl::DB::ActiveRecordAdapter, # the DB Adapter to use
223
+ :cache => ActiveAcl::Cache::NoCacheAdapter, # the Cache Adapter to use
224
+ }
225
+
226
+ ==Tests
227
+ * The tests were moved out of the main distribution as they include a custom rails root with external references and are not needed by most people anyway. Also the tests break the 'rake test:plugins' command, since all tests from the fake rails root plugins would be run also.
228
+
229
+ If you want to execute the tests, go to the active_acl plugin directory. Then check out svn://rubyforge.org/var/svn/activeacl/tests/trunk - or whatever your main branch is instead of trunk - to the test directory.
230
+
231
+ svn co svn://rubyforge.org/var/svn/activeacl/tests/trunk test
232
+
233
+ You can test different environments by setting RAILS_ENV before. Look to database.example for possible setups and copy it to database.yml for changes. Then use 'rake test' from inside the plugin directory. Using 'rake test:plugins PLUGIN=active_acl' from the root of your application will NOT work!
234
+
235
+ If you have questions about the setup for my tests, take a look at
236
+
237
+ http://www.pluginaweek.org/2006/11/24/plugin-tip-of-the-week-testing-your-plugins-the-right-way
238
+
239
+ ==Credits
240
+ * Evan for writing that great polymorph plugin and beeing so kind to add namespace and tablename support on my request.
241
+ * ReinH and markmeves for great support and suggestions at the rubyonrails channel on freenode.org.
242
+ * http://phpgacl.sourceforge.net as a great source of inspiration
243
+ * Obrie for writing plugin_migrations and loaded_plugins and also very nice support when I got stuck with using them.
244
+
245
+ ==ToDo
246
+
247
+ in no particular order, just a reminder...
248
+
249
+ * filtered result sets, implementing <code>MyTarget.find(:all, :privilege => my_privilege, :on => my_requester)</code>. This should return all targets of a special type that the requester has a certain privilege on and avoids having n permission queries for n objects in the result set.
250
+ * direct PostgreSQL interface
251
+ * example on how to integrate with acts_as_authenticated
252
+ * example on controller actions
253
+ * make grouping optional
254
+ * add interface generators (partially done)
255
+ * error checking for conflicting ACL entries
data/Rakefile ADDED
@@ -0,0 +1,97 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/contrib/sshpublisher'
6
+
7
+ # RCOV command, run as though from the commandline.
8
+ RCOV = "rcov"
9
+
10
+ PKG_NAME = "active_acl"
11
+ PKG_VERSION = "0.2.0"
12
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
13
+ RUBY_FORGE_PROJECT = "activeacl"
14
+ RUBY_FORGE_USER = "hildolfur"
15
+
16
+ spec = Gem::Specification.new do |s|
17
+ s.name = PKG_NAME
18
+ s.version = PKG_VERSION
19
+ s.platform = Gem::Platform::RUBY
20
+ s.summary = "Provides an unintrusive, scalable and very flexible approach to fine grained access control."
21
+ s.files = FileList["{lib,tasks,generators,db}/[^.]**/[^.]*"].to_a + %w(init.rb install.rb LICENSE Rakefile README CHANGELOG)
22
+ s.require_path = "lib"
23
+ s.autorequire = PKG_NAME
24
+ s.has_rdoc = true
25
+ s.add_dependency "rails", ">= 1.1.6"
26
+ s.author = "Gregor Melhorn"
27
+ s.email = "g.melhorn@web.de"
28
+ s.homepage = "http://activeacl.rubyforge.org"
29
+ end
30
+
31
+ Rake::GemPackageTask.new(spec) do |p|
32
+ p.gem_spec = spec
33
+ p.need_tar = true
34
+ p.need_zip = true
35
+ end
36
+
37
+ desc 'Default: run unit tests.'
38
+ task :default => :test
39
+
40
+ #desc "Publish the beta gem"
41
+ #task :pgem => [:package] do
42
+ # Rake::SshFilePublisher.new("pluginaweek@pluginaweek.org", "/home/pluginaweek/gems.pluginaweek.org/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
43
+ #end
44
+
45
+ desc "Publish the API documentation"
46
+ task :pdoc => [:rdoc] do
47
+ Rake::SshDirPublisher.new("hildolfur@rubyforge.org", "/var/www/gforge-projects/activeacl/api", "rdoc").upload
48
+ #Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
49
+ end
50
+
51
+ desc "Publish the API docs and gem"
52
+ task :publish => [:pdoc, :release]
53
+
54
+ desc "Publish the release files to RubyForge."
55
+ task :release => [:gem, :package] do
56
+ require 'rubyforge'
57
+ options = {"cookie_jar" => RubyForge::COOKIE_F}
58
+ options["password"] = ENV["RUBY_FORGE_PASSWORD"] if ENV["RUBY_FORGE_PASSWORD"]
59
+ ruby_forge = RubyForge.new("./config.yml", options)
60
+ ruby_forge.login
61
+ %w( gem tgz zip ).each do |ext|
62
+ file = "pkg/#{PKG_FILE_NAME}.#{ext}"
63
+ puts "Releasing #{File.basename(file)}..."
64
+ ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
65
+ end
66
+ end
67
+
68
+ desc "generate a coverage report"
69
+ task :coverage do
70
+ sh "#{RCOV} --rails -T -Ilib -x db/**/* --output ../../../coverage/active_acl test/all_tests.rb"
71
+ end
72
+
73
+ desc "generate a coverage report saving current state"
74
+ task :coverage_save do
75
+ sh "#{RCOV} --rails -T -Ilib -x db/**/* --output ../../../coverage/active_acl --save ../../../coverage/active_acl/coverage.info test/all_tests.rb"
76
+ end
77
+
78
+ desc "generate a diff coverage report on previously saved state"
79
+ task :coverage_diff do
80
+ sh "#{RCOV} --rails -T -Ilib -x db/**/* --text-coverage-diff ../../../coverage/active_acl/coverage.info --output ../../../coverage/active_acl test/all_tests.rb"
81
+ end
82
+
83
+ desc 'Test the active_acl plugin.'
84
+ Rake::TestTask.new(:test) do |t|
85
+ t.libs << 'lib'
86
+ t.pattern = 'test/unit/**/*_test.rb'
87
+ t.verbose = true
88
+ end
89
+
90
+ desc 'Generate documentation for the active_acl plugin.'
91
+ Rake::RDocTask.new(:rdoc) do |rdoc|
92
+ rdoc.rdoc_dir = 'rdoc'
93
+ rdoc.title = 'GaclBase'
94
+ rdoc.options << '--line-numbers' << '--inline-source'
95
+ rdoc.rdoc_files.include('README')
96
+ rdoc.rdoc_files.include('lib/**/*.rb')
97
+ end
@@ -0,0 +1,111 @@
1
+ class BaseTableSetup < ActiveRecord::Migration
2
+ def self.up
3
+ create_table ActiveAcl::OPTIONS[:acls_table] do |t|
4
+ t.column :section_id, :int
5
+ t.column :allow, :boolean, :null => false, :default => true
6
+ t.column :enabled, :boolean, :null => false, :default => true
7
+ t.column :note, :string, :null => true
8
+ t.column :updated_at, :datetime, :null => false
9
+ end
10
+
11
+ add_index ActiveAcl::OPTIONS[:acls_table], :enabled
12
+ add_index ActiveAcl::OPTIONS[:acls_table], :section_id
13
+ add_index ActiveAcl::OPTIONS[:acls_table], :updated_at
14
+ add_index ActiveAcl::OPTIONS[:acls_table], :note, :unique
15
+
16
+ create_table ActiveAcl::OPTIONS[:acl_sections_table] do |t|
17
+ t.column :description, :string, :limit => 230, :null => false
18
+ end
19
+
20
+ add_index ActiveAcl::OPTIONS[:acl_sections_table], :description, :unique
21
+
22
+ create_table ActiveAcl::OPTIONS[:privileges_table] do |t|
23
+ t.column :section, :string, :limit => 230, :null => false
24
+ t.column :value, :string, :limit => 230, :null => false
25
+ t.column :description, :string, :limit => 230, :null => true
26
+ end
27
+
28
+ add_index ActiveAcl::OPTIONS[:privileges_table], [:section, :value], :unique
29
+
30
+ create_table ActiveAcl::OPTIONS[:acls_privileges_table], :id => false do |t|
31
+ t.column :acl_id, :int, :null => false
32
+ t.column :privilege_id, :int, :null => false
33
+ end
34
+
35
+ add_index ActiveAcl::OPTIONS[:acls_privileges_table], [:acl_id, :privilege_id], :unique
36
+
37
+ create_table ActiveAcl::OPTIONS[:requester_links_table] do |t|
38
+ t.column :acl_id, :int, :null => false
39
+ t.column :requester_id, :int, :null => false
40
+ t.column :requester_type, :string, :null => false
41
+ end
42
+
43
+ add_index ActiveAcl::OPTIONS[:requester_links_table], [:acl_id, :requester_id, :requester_type], :unique => true, :name => 'requester_links_join_index_1'
44
+ add_index ActiveAcl::OPTIONS[:requester_links_table], [:requester_type, :requester_id], :name => 'requester_links_join_index_2'
45
+ add_index ActiveAcl::OPTIONS[:requester_links_table], [:requester_id]
46
+
47
+ create_table ActiveAcl::OPTIONS[:requester_group_links_table] do |t|
48
+ t.column :acl_id, :int, :null => false
49
+ t.column :requester_group_id, :int, :null => false
50
+ t.column :requester_group_type, :string, :null => false
51
+ end
52
+
53
+ add_index ActiveAcl::OPTIONS[:requester_group_links_table], [:acl_id, :requester_group_id, :requester_group_type], :unique => true, :name => 'requester_group_links_join_index_1'
54
+ add_index ActiveAcl::OPTIONS[:requester_group_links_table], [:requester_group_type, :requester_group_id], :name => 'requester_group_links_join_index2'
55
+
56
+ create_table ActiveAcl::OPTIONS[:target_group_links_table] do |t|
57
+ t.column :acl_id, :int, :null => false
58
+ t.column :target_group_id, :int, :null => false
59
+ t.column :target_group_type, :string, :null => false
60
+ end
61
+
62
+ add_index ActiveAcl::OPTIONS[:target_group_links_table], [:acl_id, :target_group_id, :target_group_type], :unique => true, :name => 'target_group_links_join_index_1'
63
+ add_index ActiveAcl::OPTIONS[:target_group_links_table], [:target_group_type, :target_group_id], :name => 'target_group_links_join_index_2'
64
+
65
+ create_table ActiveAcl::OPTIONS[:target_links_table] do |t|
66
+ t.column :acl_id, :int, :null => false
67
+ t.column :target_id, :int, :null => false
68
+ t.column :target_type, :string, :null => false
69
+ end
70
+
71
+ add_index ActiveAcl::OPTIONS[:target_links_table], [:acl_id, :target_id, :target_type], :unique => true, :name => 'target_links_join_index_1'
72
+ add_index ActiveAcl::OPTIONS[:target_links_table], [:target_type, :target_id], :name => 'target_links_join_index_2'
73
+ add_index ActiveAcl::OPTIONS[:target_links_table], [:target_id]
74
+
75
+ create_table ActiveAcl::OPTIONS[:controller_actions_table] do |t|
76
+ t.column :controller, :string, :null => false
77
+ t.column :action, :string, :null => false
78
+ t.column :controller_group_id, :integer, :null => false
79
+ end
80
+
81
+ add_index ActiveAcl::OPTIONS[:controller_actions_table], [:controller, :action], :unique
82
+
83
+ create_table ActiveAcl::OPTIONS[:controller_groups_table] do |t|
84
+ t.column :description, :string, :null => false
85
+ t.column :lft, :integer
86
+ t.column :rgt, :integer
87
+ t.column :parent_id, :integer
88
+ end
89
+
90
+ add_index ActiveAcl::OPTIONS[:controller_groups_table], :description
91
+ add_index ActiveAcl::OPTIONS[:controller_groups_table], :lft
92
+ add_index ActiveAcl::OPTIONS[:controller_groups_table], :rgt
93
+ add_index ActiveAcl::OPTIONS[:controller_groups_table], :parent_id
94
+
95
+ # create root node
96
+ execute("INSERT INTO #{ActiveAcl::OPTIONS[:controller_groups_table]}(description, lft, rgt) VALUES ('controllers', 1, 2)")
97
+ end
98
+
99
+ def self.down
100
+ drop_table ActiveAcl::OPTIONS[:acls_table]
101
+ drop_table ActiveAcl::OPTIONS[:acl_sections_table]
102
+ drop_table ActiveAcl::OPTIONS[:privileges_table]
103
+ drop_table ActiveAcl::OPTIONS[:acls_privileges_table]
104
+ drop_table ActiveAcl::OPTIONS[:requester_links_table]
105
+ drop_table ActiveAcl::OPTIONS[:target_links_table]
106
+ drop_table ActiveAcl::OPTIONS[:requester_group_links_table]
107
+ drop_table ActiveAcl::OPTIONS[:target_group_links_table]
108
+ drop_table ActiveAcl::OPTIONS[:controller_actions_table]
109
+ drop_table ActiveAcl::OPTIONS[:controller_groups_table]
110
+ end
111
+ end
@@ -0,0 +1,25 @@
1
+ class ActiveAclGenerator < Rails::Generator::Base
2
+ attr_accessor :privileges_class_name, :privileges_file_name, :privileges_view_dir
3
+
4
+ def initialize(*runtime_args)
5
+ super(*runtime_args)
6
+ @privileges_class_name = (args[0] || 'PrivilegesController')
7
+ @privileges_file_name = @privileges_class_name.underscore
8
+ @privileges_view_dir = File.join('app', 'views', @privileges_file_name.gsub('_controller', ''))
9
+ end
10
+
11
+ def manifest
12
+ record do |m|
13
+ # Stylesheet, controllers and public directories.
14
+ m.directory File.join('public', 'stylesheets')
15
+ m.directory File.join('app', 'controllers')
16
+ m.directory File.join('app', 'views')
17
+ m.directory privileges_view_dir
18
+
19
+ m.template 'controllers/privileges_controller.rb', File.join(RAILS_ROOT, 'app', 'controllers', "#{privileges_file_name}.rb")
20
+ m.file 'views/privileges/_privilege_form.rhtml', File.join(privileges_view_dir, '_privilege_form.rhtml')
21
+ m.file 'views/privileges/edit.rhtml', File.join(privileges_view_dir, 'edit.rhtml')
22
+ m.file 'views/privileges/list.rhtml', File.join(privileges_view_dir, 'list.rhtml')
23
+ end
24
+ end
25
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'active_acl'
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,39 @@
1
+ # This model is the "glue" :-). Every permission assignment uses an Acl
2
+ # model object, assigns objects, groups and privileges and setting
3
+ # 'allow' to "true" or "false" to grant or deny access.
4
+ class ActiveAcl::Acl < ActiveRecord::Base
5
+ set_table_name ActiveAcl::OPTIONS[:acls_table]
6
+
7
+ has_and_belongs_to_many :privileges, :uniq => true, :join_table => ActiveAcl::OPTIONS[:acls_privileges_table], :class_name => 'ActiveAcl::Privilege'
8
+
9
+ has_many :target_links, :dependent => :delete_all
10
+ has_many :requester_links, :dependent => :delete_all
11
+
12
+ validates_uniqueness_of :note
13
+ validates_presence_of :note
14
+
15
+ belongs_to :section, :class_name => 'ActiveAcl::AclSection', :foreign_key => 'section_id'
16
+
17
+ has_many :requester_group_links, :dependent => :delete_all
18
+ has_many :target_group_links, :dependent => :delete_all
19
+
20
+ has_many :requester_groups, :through => :requester_group_links, :source => :acl
21
+ has_many :target_groups, :through => :target_group_links, :source => :acl
22
+
23
+ def self.reloadable? #:nodoc:
24
+ return false
25
+ end
26
+
27
+ # used as instance description in admin screen
28
+ def active_acl_description
29
+ if note
30
+ if section
31
+ '/' + section.description + '/' + note
32
+ else
33
+ return note
34
+ end
35
+ else
36
+ return nil
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # Groups Acl model objects into different sections to provide better
2
+ # overview in the admin screens. Has no meaning in permission resolution.
3
+ class ActiveAcl::AclSection < ActiveRecord::Base
4
+ set_table_name ActiveAcl::OPTIONS[:acl_sections_table]
5
+
6
+ has_many :members, :class_name => 'ActiveAcl::Acl', :foreign_key => 'section_id'
7
+
8
+ validates_presence_of :description
9
+ validates_uniqueness_of :description
10
+
11
+ # Make shure there are no associated acls before destroying a section
12
+ def before_destroy
13
+ if members.empty?
14
+ true
15
+ else
16
+ errors.add_to_base("Can't delete a section with associated ACLs")
17
+ false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ require 'active_record'
2
+
3
+ module ActiveAcl #:nodoc:
4
+ module Acts #:nodoc:
5
+ module AccessGroup #:nodoc:
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ # Extend self with access group capabilites. See README for details
13
+ # on usage. Accepts the following options as a hash:
14
+ # left_column:: name of the left column for nested set functionality, default :lft
15
+ # right_column:: name of the right column for nested set functionality, default :rgt
16
+ # Don't use 'left' and 'right' as column names - these are reserved words in most DBMS.
17
+ def acts_as_access_group(options = {})
18
+ configuration = {:left_column => :lft, :right_column => :rgt,
19
+ :controller => ActiveAcl::OPTIONS[:default_group_selector_controller],
20
+ :action => ActiveAcl::OPTIONS[:default_group_selector_action]}
21
+ configuration.update(options) if options.is_a?(Hash)
22
+ ActiveAcl::GROUP_CLASSES[self.name] = configuration
23
+
24
+ from_classes = ActiveAcl::GROUP_CLASSES.keys.collect do |x|
25
+ x.split('::').join('/').underscore.pluralize.to_sym
26
+ end
27
+
28
+ ActiveAcl::Acl.instance_eval do
29
+ has_many_polymorphs :requester_groups, {:from => from_classes,
30
+ :through => :"active_acl/requester_group_links",
31
+ :join_table_name => ActiveAcl::OPTIONS[:requester_group_links_table],
32
+ :rename_individual_collections => true}
33
+
34
+ has_many_polymorphs :target_groups, {:from => from_classes,
35
+ :through => :"active_acl/target_group_links",
36
+ :join_table_name => ActiveAcl::OPTIONS[:target_group_links_table],
37
+ :rename_individual_collections => true}
38
+ end
39
+
40
+ include InstanceMethods
41
+ extend SingletonMethods
42
+
43
+ end
44
+ end
45
+
46
+ module SingletonMethods
47
+ # class description in engine interface
48
+ def active_acl_description
49
+ name
50
+ end
51
+ end
52
+
53
+ module InstanceMethods
54
+ # override this to customize the description in the interface
55
+ def active_acl_description
56
+ to_s
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
63
+
64
+ ActiveRecord::Base.send(:include, ActiveAcl::Acts::AccessGroup)