peterpunk-acl9 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module Acl9
2
+ module Helpers
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def access_control(method, opts = {}, &block)
9
+ subject_method = opts.delete(:subject_method) || Acl9::config[:default_subject_method]
10
+ raise ArgumentError, "Block must be supplied to access_control" unless block
11
+
12
+ generator = Acl9::Dsl::Generators::HelperMethod.new(subject_method, method)
13
+
14
+ generator.acl_block!(&block)
15
+ generator.install_on(self, opts)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,132 @@
1
+ require File.join(File.dirname(__FILE__), 'model_extensions', 'subject')
2
+ require File.join(File.dirname(__FILE__), 'model_extensions', 'object')
3
+
4
+ module Acl9
5
+ module ModelExtensions #:nodoc:
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ # Add #has_role? and other role methods to the class.
12
+ # Makes a class a auth. subject class.
13
+ #
14
+ # @param [Hash] options the options for tuning
15
+ # @option options [String] :role_class_name (Acl9::config[:default_role_class_name])
16
+ # Class name of the role class (e.g. 'AccountRole')
17
+ # @option options [String] :join_table_name (Acl9::config[:default_join_table_name])
18
+ # Join table name (e.g. 'accounts_account_roles')
19
+ # @example
20
+ # class User < ActiveRecord::Base
21
+ # acts_as_authorization_subject
22
+ # end
23
+ #
24
+ # user = User.new
25
+ # user.roles #=> returns Role objects, associated with the user
26
+ # user.has_role!(...)
27
+ # user.has_no_role!(...)
28
+ #
29
+ # # other functions from Acl9::ModelExtensions::Subject are made available
30
+ #
31
+ # @see Acl9::ModelExtensions::Subject
32
+ #
33
+ def acts_as_authorization_subject(options = {})
34
+ role = options[:role_class_name] || Acl9::config[:default_role_class_name]
35
+ join_table = options[:join_table_name] || Acl9::config[:default_join_table_name] ||
36
+ join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(role))
37
+
38
+ has_and_belongs_to_many :role_objects, :class_name => role, :join_table => join_table
39
+
40
+ cattr_accessor :_auth_role_class_name
41
+ self._auth_role_class_name = role
42
+
43
+ include Acl9::ModelExtensions::Subject
44
+ end
45
+
46
+ # Add role query and set methods to the class (making it an auth object class).
47
+ #
48
+ # @param [Hash] options the options for tuning
49
+ # @option options [String] :subject_class_name (Acl9::config[:default_subject_class_name])
50
+ # Subject class name (e.g. 'User', or 'Account)
51
+ # @option options [String] :role_class_name (Acl9::config[:default_role_class_name])
52
+ # Role class name (e.g. 'AccountRole')
53
+ # @example
54
+ # class Product < ActiveRecord::Base
55
+ # acts_as_authorization_object
56
+ # end
57
+ #
58
+ # product = Product.new
59
+ # product.accepted_roles #=> returns Role objects, associated with the product
60
+ # product.users #=> returns User objects, associated with the product
61
+ # product.accepts_role!(...)
62
+ # product.accepts_no_role!(...)
63
+ # # other functions from Acl9::ModelExtensions::Object are made available
64
+ #
65
+ # @see Acl9::ModelExtensions::Object
66
+ #
67
+ def acts_as_authorization_object(options = {})
68
+ subject = options[:subject_class_name] || Acl9::config[:default_subject_class_name]
69
+ subj_table = subject.constantize.table_name
70
+ subj_col = subject.underscore
71
+
72
+ role = options[:role_class_name] || Acl9::config[:default_role_class_name]
73
+ role_table = role.constantize.table_name
74
+
75
+ sql_tables = <<-EOS
76
+ FROM #{subj_table}
77
+ INNER JOIN #{role_table}_#{subj_table} ON #{subj_col}_id = #{subj_table}.id
78
+ INNER JOIN #{role_table} ON #{role_table}.id = #{role.underscore}_id
79
+ EOS
80
+
81
+ sql_where = <<-'EOS'
82
+ WHERE authorizable_type = '#{self.class.base_class.to_s}'
83
+ AND authorizable_id = #{id}
84
+ EOS
85
+
86
+ has_many :accepted_roles, :as => :authorizable, :class_name => role, :dependent => :destroy
87
+
88
+ has_many :"#{subj_table}",
89
+ :finder_sql => ("SELECT DISTINCT #{subj_table}.*" + sql_tables + sql_where),
90
+ :counter_sql => ("SELECT COUNT(DISTINCT #{subj_table}.id)" + sql_tables + sql_where),
91
+ :readonly => true
92
+
93
+ include Acl9::ModelExtensions::Object
94
+ end
95
+
96
+ # Make a class an auth role class.
97
+ #
98
+ # You'll probably never create or use objects of this class directly.
99
+ # Various auth. subject and object methods will do that for you
100
+ # internally.
101
+ #
102
+ # @param [Hash] options the options for tuning
103
+ # @option options [String] :subject_class_name (Acl9::config[:default_subject_class_name])
104
+ # Subject class name (e.g. 'User', or 'Account)
105
+ # @option options [String] :join_table_name (Acl9::config[:default_join_table_name])
106
+ # Join table name (e.g. 'accounts_account_roles')
107
+ #
108
+ # @example
109
+ # class Role < ActiveRecord::Base
110
+ # acts_as_authorization_role
111
+ # end
112
+ #
113
+ # @see Acl9::ModelExtensions::Subject#has_role!
114
+ # @see Acl9::ModelExtensions::Subject#has_role?
115
+ # @see Acl9::ModelExtensions::Subject#has_no_role!
116
+ # @see Acl9::ModelExtensions::Object#accepts_role!
117
+ # @see Acl9::ModelExtensions::Object#accepts_role?
118
+ # @see Acl9::ModelExtensions::Object#accepts_no_role!
119
+ def acts_as_authorization_role(options = {})
120
+ subject = options[:subject_class_name] || Acl9::config[:default_subject_class_name]
121
+ join_table = options[:join_table_name] || Acl9::config[:default_join_table_name] ||
122
+ join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(subject))
123
+
124
+ has_and_belongs_to_many subject.demodulize.tableize.to_sym,
125
+ :class_name => subject,
126
+ :join_table => join_table
127
+
128
+ belongs_to :authorizable, :polymorphic => true
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,59 @@
1
+ module Acl9
2
+ module ModelExtensions
3
+ module Object
4
+ ##
5
+ # Role check.
6
+ #
7
+ # @return [Boolean] Returns true if +subject+ has a role +role_name+ on this object.
8
+ #
9
+ # @param [Symbol,String] role_name Role name
10
+ # @param [Subject] subject Subject to add role for
11
+ # @see Acl9::ModelExtensions::Subject#has_role?
12
+ def accepts_role?(role_name, subject)
13
+ subject.has_role? role_name, self
14
+ end
15
+
16
+ ##
17
+ # Add role on the object to specified subject.
18
+ #
19
+ # @param [Symbol,String] role_name Role name
20
+ # @param [Subject] subject Subject to add role for
21
+ # @see Acl9::ModelExtensions::Subject#has_role!
22
+ def accepts_role!(role_name, subject)
23
+ subject.has_role! role_name, self
24
+ end
25
+
26
+ ##
27
+ # Free specified subject of a role on this object.
28
+ #
29
+ # @param [Symbol,String] role_name Role name
30
+ # @param [Subject] subject Subject to remove role from
31
+ # @see Acl9::ModelExtensions::Subject#has_no_role!
32
+ def accepts_no_role!(role_name, subject)
33
+ subject.has_no_role! role_name, self
34
+ end
35
+
36
+ ##
37
+ # Are there any roles for the specified +subject+ on this object?
38
+ #
39
+ # @param [Subject] subject Subject to query roles
40
+ # @return [Boolean] Returns true if +subject+ has any roles on this object.
41
+ # @see Acl9::ModelExtensions::Subject#has_roles_for?
42
+ def accepts_roles_by?(subject)
43
+ subject.has_roles_for? self
44
+ end
45
+
46
+ alias :accepts_role_by? :accepts_roles_by?
47
+
48
+ ##
49
+ # Which roles does +subject+ have on this object?
50
+ #
51
+ # @return [Array<Role>] Role instances, associated both with +subject+ and +object+
52
+ # @param [Subject] subject Subject to query roles
53
+ # @see Acl9::ModelExtensions::Subject#roles_for
54
+ def accepted_roles_by(subject)
55
+ subject.roles_for self
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,150 @@
1
+ module Acl9
2
+ module ModelExtensions
3
+ module Subject
4
+ ##
5
+ # Role check.
6
+ #
7
+ # @return [Boolean] Returns true if +self+ has a role +role_name+ on +object+.
8
+ #
9
+ # @param [Symbol,String] role_name Role name
10
+ # @param [Object] object Object to query a role on
11
+ # @see Acl9::ModelExtensions::Object#accepts_role?
12
+ def has_role?(role_name, object = nil)
13
+ !! if object.nil?
14
+ self.role_objects.find_by_name(role_name.to_s) ||
15
+ self.role_objects.member?(get_role(role_name, nil))
16
+ else
17
+ role = get_role(role_name, object)
18
+ role && self.role_objects.exists?(role.id)
19
+ end
20
+ end
21
+
22
+ ##
23
+ # Add specified role on +object+ to +self+.
24
+ #
25
+ # @param [Symbol,String] role_name Role name
26
+ # @param [Object] object Object to add a role for
27
+ # @see Acl9::ModelExtensions::Object#accepts_role!
28
+ def has_role!(role_name, object = nil)
29
+ role = get_role(role_name, object)
30
+
31
+ if role.nil?
32
+ role_attrs = case object
33
+ when Class then { :authorizable_type => object.to_s }
34
+ when nil then {}
35
+ else { :authorizable => object }
36
+ end.merge( { :name => role_name.to_s })
37
+
38
+ role = self._auth_role_class.create(role_attrs)
39
+ end
40
+
41
+ self.role_objects << role if role && !self.role_objects.exists?(role.id)
42
+ end
43
+
44
+ ##
45
+ # Free +self+ from a specified role on +object+.
46
+ #
47
+ # @param [Symbol,String] role_name Role name
48
+ # @param [Object] object Object to remove a role on
49
+ # @see Acl9::ModelExtensions::Object#accepts_no_role!
50
+ def has_no_role!(role_name, object = nil)
51
+ delete_role(get_role(role_name, object))
52
+ end
53
+
54
+ ##
55
+ # Are there any roles for +self+ on +object+?
56
+ #
57
+ # @param [Object] object Object to query roles
58
+ # @return [Boolean] Returns true if +self+ has any roles on +object+.
59
+ # @see Acl9::ModelExtensions::Object#accepts_roles_by?
60
+ def has_roles_for?(object)
61
+ !!self.role_objects.detect(&role_selecting_lambda(object))
62
+ end
63
+
64
+ alias :has_role_for? :has_roles_for?
65
+
66
+ ##
67
+ # Which roles does +self+ have on +object+?
68
+ #
69
+ # @return [Array<Role>] Role instances, associated both with +self+ and +object+
70
+ # @param [Object] object Object to query roles
71
+ # @see Acl9::ModelExtensions::Object#accepted_roles_by
72
+ # @example
73
+ # user = User.find(...)
74
+ # product = Product.find(...)
75
+ #
76
+ # user.roles_for(product).map(&:name).sort #=> role names in alphabetical order
77
+ def roles_for(object)
78
+ self.role_objects.select(&role_selecting_lambda(object))
79
+ end
80
+
81
+ ##
82
+ # Unassign any roles on +object+ from +self+.
83
+ #
84
+ # @param [Object,nil] object Object to unassign roles for. +nil+ means unassign global roles.
85
+ def has_no_roles_for!(object = nil)
86
+ roles_for(object).each { |role| delete_role(role) }
87
+ end
88
+
89
+ ##
90
+ # Unassign all roles from +self+.
91
+ def has_no_roles!
92
+ # for some reason simple
93
+ #
94
+ # self.roles.each { |role| delete_role(role) }
95
+ #
96
+ # doesn't work. seems like a bug in ActiveRecord
97
+ self.role_objects.map(&:id).each do |role_id|
98
+ delete_role self._auth_role_class.find(role_id)
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def role_selecting_lambda(object)
105
+ case object
106
+ when Class
107
+ lambda { |role| role.authorizable_type == object.to_s }
108
+ when nil
109
+ lambda { |role| role.authorizable.nil? }
110
+ else
111
+ lambda do |role|
112
+ role.authorizable_type == object.class.base_class.to_s && role.authorizable == object
113
+ end
114
+ end
115
+ end
116
+
117
+ def get_role(role_name, object)
118
+ role_name = role_name.to_s
119
+
120
+ cond = case object
121
+ when Class
122
+ [ 'name = ? and authorizable_type = ? and authorizable_id IS NULL', role_name, object.to_s ]
123
+ when nil
124
+ [ 'name = ? and authorizable_type IS NULL and authorizable_id IS NULL', role_name ]
125
+ else
126
+ [
127
+ 'name = ? and authorizable_type = ? and authorizable_id = ?',
128
+ role_name, object.class.base_class.to_s, object.id
129
+ ]
130
+ end
131
+
132
+ self._auth_role_class.first :conditions => cond
133
+ end
134
+
135
+ def delete_role(role)
136
+ if role
137
+ self.role_objects.delete role
138
+
139
+ role.destroy if role.send(self.class.to_s.demodulize.tableize).empty?
140
+ end
141
+ end
142
+
143
+ protected
144
+
145
+ def _auth_role_class
146
+ self.class._auth_role_class_name.constantize
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,338 @@
1
+ require 'test_helper'
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'acl9')
3
+ require 'support/controllers'
4
+
5
+ #######################################################################
6
+
7
+ class Admin
8
+ def has_role?(role, obj = nil)
9
+ role == "admin"
10
+ end
11
+ end
12
+
13
+ class OwnerOfFoo
14
+ def has_role?(role, obj)
15
+ role == 'owner' && obj == MyDearFoo.instance
16
+ end
17
+ end
18
+
19
+ class Bartender
20
+ def has_role?(role, obj)
21
+ role == 'bartender' && obj == ACLIvars::VenerableBar
22
+ end
23
+ end
24
+
25
+ class TheOnlyUser
26
+ include Singleton
27
+
28
+ def has_role?(role, subj)
29
+ role == "the_only_one"
30
+ end
31
+ end
32
+
33
+ class Beholder
34
+ def initialize(role)
35
+ @role = role.to_s
36
+ end
37
+
38
+ def has_role?(role, obj)
39
+ role.to_s == @role
40
+ end
41
+ end
42
+
43
+ #######################################################################
44
+
45
+ module BaseTests
46
+ # permit anonymous to index and show and admin everywhere else
47
+ def self.included(klass)
48
+ klass.class_eval do
49
+ [:index, :show].each do |act|
50
+ it "should permit anonymous to #{act}" do
51
+ get act
52
+ @response.body.should == 'OK'
53
+ end
54
+ end
55
+
56
+ [:new, :edit, :update, :delete, :destroy].each do |act|
57
+ it "should forbid anonymous to #{act}" do
58
+ get act
59
+ @response.body.should == 'AccessDenied'
60
+ end
61
+ end
62
+
63
+ [:index, :show, :new, :edit, :update, :delete, :destroy].each do |act|
64
+ it "should permit admin to #{act}" do
65
+ get act, :user => Admin.new
66
+ @response.body.should == 'OK'
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ module ShouldRespondToAcl
74
+ def self.included(klass)
75
+ klass.class_eval do
76
+ it "should add :acl as a method" do
77
+ @controller.should respond_to(:acl)
78
+ end
79
+
80
+ it "should_not add :acl? as a method" do
81
+ @controller.should_not respond_to(:acl?)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ #######################################################################
88
+
89
+ class ACLBlockTest < ActionController::TestCase
90
+ tests ACLBlock
91
+
92
+ include BaseTests
93
+ end
94
+
95
+ class ACLMethodTest < ActionController::TestCase
96
+ tests ACLMethod
97
+
98
+ include BaseTests
99
+ include ShouldRespondToAcl
100
+ end
101
+
102
+ class ACLMethod2Test < ActionController::TestCase
103
+ tests ACLMethod2
104
+
105
+ include BaseTests
106
+ include ShouldRespondToAcl
107
+ end
108
+
109
+ class ACLArgumentsTest < ActionController::TestCase
110
+ tests ACLArguments
111
+
112
+ include BaseTests
113
+ end
114
+
115
+ class ACLBooleanMethodTest < ActionController::TestCase
116
+ tests ACLBooleanMethod
117
+
118
+ include BaseTests
119
+ end
120
+
121
+ class ACLIvarsTest < ActionController::TestCase
122
+ tests ACLIvars
123
+
124
+ it "should allow owner of foo to destroy" do
125
+ delete :destroy, :user => OwnerOfFoo.new
126
+ @response.body.should == 'OK'
127
+ end
128
+
129
+ it "should allow bartender to destroy" do
130
+ delete :destroy, :user => Bartender.new
131
+ @response.body.should == 'OK'
132
+ end
133
+ end
134
+
135
+ class ACLSubjectMethodTest < ActionController::TestCase
136
+ tests ACLSubjectMethod
137
+
138
+ it "should allow the only user to index" do
139
+ get :index, :user => TheOnlyUser.instance
140
+ @response.body.should == 'OK'
141
+ end
142
+
143
+ it "should deny anonymous to index" do
144
+ get :index
145
+ @response.body.should == 'AccessDenied'
146
+ end
147
+ end
148
+
149
+ class ACLObjectsHashTest < ActionController::TestCase
150
+ tests ACLObjectsHash
151
+
152
+ it "should consider objects hash and prefer it to @ivar" do
153
+ get :allow, :user => OwnerOfFoo.new
154
+ @response.body.should == 'OK'
155
+ end
156
+
157
+ it "should return AccessDenied when not logged in" do
158
+ get :allow
159
+ @response.body.should == 'AccessDenied'
160
+ end
161
+ end
162
+
163
+ class ACLActionOverrideTest < ActionController::TestCase
164
+ tests ACLActionOverride
165
+
166
+ it "should allow index action to anonymous" do
167
+ get :check_allow, :_action => :index
168
+ @response.body.should == 'OK'
169
+ end
170
+
171
+ it "should deny show action to anonymous" do
172
+ get :check_allow, :_action => :show
173
+ @response.body.should == 'AccessDenied'
174
+ end
175
+
176
+ it "should deny edit action to regular user" do
177
+ get :check_allow_with_foo, :_action => :edit, :user => TheOnlyUser.instance
178
+
179
+ @response.body.should == 'AccessDenied'
180
+ end
181
+
182
+ it "should allow edit action to owner of foo" do
183
+ get :check_allow_with_foo, :_action => :edit, :user => OwnerOfFoo.new
184
+
185
+ @response.body.should == 'OK'
186
+ end
187
+ end
188
+
189
+ class ACLHelperMethodTest < ActionController::TestCase
190
+ tests ACLHelperMethod
191
+
192
+ it "should return OK checking helper method" do
193
+ get :allow, :user => OwnerOfFoo.new
194
+ @response.body.should == 'OK'
195
+ end
196
+
197
+ it "should return AccessDenied when not logged in" do
198
+ get :allow
199
+ @response.body.should == 'AccessDenied'
200
+ end
201
+ end
202
+
203
+ #######################################################################
204
+
205
+ module ACLQueryMixin
206
+ def self.included(base)
207
+ base.class_eval do
208
+ describe "#acl_question_mark" do # describe "#acl?" doesn't work
209
+ before do
210
+ @editor = Beholder.new(:editor)
211
+ @viewer = Beholder.new(:viewer)
212
+ @owneroffoo = OwnerOfFoo.new
213
+ end
214
+
215
+ [:edit, :update, :destroy].each do |meth|
216
+ it "should return true for editor/#{meth}" do
217
+ @controller.current_user = @editor
218
+ @controller.acl?(meth).should == true
219
+ @controller.acl?(meth.to_s).should == true
220
+ end
221
+
222
+ it "should return false for viewer/#{meth}" do
223
+ @controller.current_user = @viewer
224
+ @controller.acl?(meth).should == false
225
+ @controller.acl?(meth.to_s).should == false
226
+ end
227
+ end
228
+
229
+ [:index, :show].each do |meth|
230
+ it "should return false for editor/#{meth}" do
231
+ @controller.current_user = @editor
232
+ @controller.acl?(meth).should == false
233
+ @controller.acl?(meth.to_s).should == false
234
+ end
235
+
236
+ it "should return true for viewer/#{meth}" do
237
+ @controller.current_user = @viewer
238
+ @controller.acl?(meth).should == true
239
+ @controller.acl?(meth.to_s).should == true
240
+ end
241
+ end
242
+
243
+ it "should return false for editor/fooize" do
244
+ @controller.current_user = @editor
245
+ @controller.acl?(:fooize).should == false
246
+ end
247
+
248
+ it "should return true for foo owner" do
249
+ @controller.current_user = @owneroffoo
250
+ @controller.acl?(:fooize, :foo => MyDearFoo.instance).should == true
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ class ACLQueryMethodTest < ActionController::TestCase
258
+ tests ACLQueryMethod
259
+
260
+ it "should respond to :acl?" do
261
+ @controller.should respond_to(:acl?)
262
+ end
263
+
264
+ include ACLQueryMixin
265
+ end
266
+
267
+ class ACLQueryMethodWithLambdaTest < ActionController::TestCase
268
+ tests ACLQueryMethodWithLambda
269
+
270
+ it "should respond to :acl?" do
271
+ @controller.should respond_to(:acl?)
272
+ end
273
+
274
+ include ACLQueryMixin
275
+ end
276
+
277
+ #######################################################################
278
+
279
+ class ACLNamedQueryMethodTest < ActionController::TestCase
280
+ tests ACLNamedQueryMethod
281
+
282
+ it "should respond to :allow_ay" do
283
+ @controller.should respond_to(:allow_ay)
284
+ end
285
+
286
+ include ACLQueryMixin
287
+ end
288
+
289
+ #######################################################################
290
+
291
+ class ArgumentsCheckingTest < ActiveSupport::TestCase
292
+ def arg_err(&block)
293
+ lambda do
294
+ block.call
295
+ end.should raise_error(ArgumentError)
296
+ end
297
+
298
+ it "should raise ArgumentError without a block" do
299
+ arg_err do
300
+ class FailureController < ApplicationController
301
+ access_control
302
+ end
303
+ end
304
+ end
305
+
306
+ it "should raise ArgumentError with 1st argument which is not a symbol" do
307
+ arg_err do
308
+ class FailureController < ApplicationController
309
+ access_control 123 do end
310
+ end
311
+ end
312
+ end
313
+
314
+ it "should raise ArgumentError with more than 1 positional argument" do
315
+ arg_err do
316
+ class FailureController < ApplicationController
317
+ access_control :foo, :bar do end
318
+ end
319
+ end
320
+ end
321
+
322
+ it "should raise ArgumentError with :helper => true and no method name" do
323
+ arg_err do
324
+ class FailureController < ApplicationController
325
+ access_control :helper => true do end
326
+ end
327
+ end
328
+ end
329
+
330
+ it "should raise ArgumentError with :helper => :method and a method name" do
331
+ arg_err do
332
+ class FailureController < ApplicationController
333
+ access_control :meth, :helper => :another_meth do end
334
+ end
335
+ end
336
+ end
337
+ end
338
+