ae_declarative_authorization 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Appraisals +31 -21
- data/CHANGELOG +189 -189
- data/Gemfile +7 -7
- data/Gemfile.lock +68 -60
- data/LICENSE.txt +20 -20
- data/README.md +620 -620
- data/README.rdoc +597 -597
- data/Rakefile +35 -33
- data/authorization_rules.dist.rb +20 -20
- data/declarative_authorization.gemspec +24 -24
- data/gemfiles/rails4252.gemfile +10 -10
- data/gemfiles/rails4252.gemfile.lock +126 -0
- data/gemfiles/rails4271.gemfile +10 -10
- data/gemfiles/rails4271.gemfile.lock +126 -0
- data/gemfiles/rails507.gemfile +11 -11
- data/gemfiles/rails507.gemfile.lock +136 -0
- data/gemfiles/rails516.gemfile +11 -0
- data/gemfiles/rails516.gemfile.lock +136 -0
- data/gemfiles/rails521.gemfile +11 -0
- data/gemfiles/rails521.gemfile.lock +144 -0
- data/init.rb +5 -5
- data/lib/declarative_authorization.rb +18 -18
- data/lib/declarative_authorization/authorization.rb +821 -821
- data/lib/declarative_authorization/helper.rb +78 -78
- data/lib/declarative_authorization/in_controller.rb +713 -713
- data/lib/declarative_authorization/in_model.rb +156 -156
- data/lib/declarative_authorization/maintenance.rb +215 -215
- data/lib/declarative_authorization/obligation_scope.rb +348 -345
- data/lib/declarative_authorization/railsengine.rb +5 -5
- data/lib/declarative_authorization/reader.rb +549 -549
- data/lib/declarative_authorization/test/helpers.rb +261 -261
- data/lib/declarative_authorization/version.rb +3 -3
- data/lib/generators/authorization/install/install_generator.rb +77 -77
- data/lib/generators/authorization/rules/rules_generator.rb +13 -13
- data/lib/generators/authorization/rules/templates/authorization_rules.rb +27 -27
- data/lib/tasks/authorization_tasks.rake +89 -89
- data/log/test.log +15246 -0
- data/pkg/ae_declarative_authorization-0.7.1.gem +0 -0
- data/pkg/ae_declarative_authorization-0.8.0.gem +0 -0
- data/test/authorization_test.rb +1121 -1121
- data/test/controller_filter_resource_access_test.rb +573 -573
- data/test/controller_test.rb +478 -478
- data/test/database.yml +3 -3
- data/test/dsl_reader_test.rb +178 -178
- data/test/functional/filter_access_to_with_id_in_scope_test.rb +88 -88
- data/test/functional/no_filter_access_to_test.rb +79 -79
- data/test/functional/params_block_arity_test.rb +39 -39
- data/test/helper_test.rb +248 -248
- data/test/maintenance_test.rb +46 -46
- data/test/model_test.rb +1840 -1840
- data/test/profiles/access_checking +20 -0
- data/test/schema.sql +60 -60
- data/test/test_helper.rb +174 -174
- data/test/test_support/minitest_compatibility.rb +26 -26
- metadata +17 -5
data/init.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
begin
|
2
|
-
require File.join(File.dirname(__FILE__), 'lib', 'declarative_authorization') # From here
|
3
|
-
rescue LoadError
|
4
|
-
require 'declarative_authorization' # From gem
|
5
|
-
end
|
1
|
+
begin
|
2
|
+
require File.join(File.dirname(__FILE__), 'lib', 'declarative_authorization') # From here
|
3
|
+
rescue LoadError
|
4
|
+
require 'declarative_authorization' # From gem
|
5
|
+
end
|
@@ -1,18 +1,18 @@
|
|
1
|
-
require File.join(%w{declarative_authorization helper})
|
2
|
-
require File.join(%w{declarative_authorization in_controller})
|
3
|
-
if defined?(ActiveRecord)
|
4
|
-
require File.join(%w{declarative_authorization in_model})
|
5
|
-
require File.join(%w{declarative_authorization obligation_scope})
|
6
|
-
end
|
7
|
-
|
8
|
-
min_rails_version = '4.2.5.2'
|
9
|
-
if Rails::VERSION::STRING < min_rails_version
|
10
|
-
raise "ae_declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
|
11
|
-
end
|
12
|
-
|
13
|
-
require File.join(%w{declarative_authorization railsengine}) if defined?(::Rails::Engine)
|
14
|
-
|
15
|
-
ActionController::Base.send :include, Authorization::AuthorizationInController
|
16
|
-
ActionController::Base.helper Authorization::AuthorizationHelper
|
17
|
-
|
18
|
-
ActiveRecord::Base.send :include, Authorization::AuthorizationInModel if defined?(ActiveRecord)
|
1
|
+
require File.join(%w{declarative_authorization helper})
|
2
|
+
require File.join(%w{declarative_authorization in_controller})
|
3
|
+
if defined?(ActiveRecord)
|
4
|
+
require File.join(%w{declarative_authorization in_model})
|
5
|
+
require File.join(%w{declarative_authorization obligation_scope})
|
6
|
+
end
|
7
|
+
|
8
|
+
min_rails_version = '4.2.5.2'
|
9
|
+
if Rails::VERSION::STRING < min_rails_version
|
10
|
+
raise "ae_declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
|
11
|
+
end
|
12
|
+
|
13
|
+
require File.join(%w{declarative_authorization railsengine}) if defined?(::Rails::Engine)
|
14
|
+
|
15
|
+
ActionController::Base.send :include, Authorization::AuthorizationInController
|
16
|
+
ActionController::Base.helper Authorization::AuthorizationHelper
|
17
|
+
|
18
|
+
ActiveRecord::Base.send :include, Authorization::AuthorizationInModel if defined?(ActiveRecord)
|
@@ -1,821 +1,821 @@
|
|
1
|
-
# Authorization
|
2
|
-
require File.dirname(__FILE__) + '/reader.rb'
|
3
|
-
require "set"
|
4
|
-
require "forwardable"
|
5
|
-
|
6
|
-
module Authorization
|
7
|
-
# An exception raised if anything goes wrong in the Authorization realm
|
8
|
-
class AuthorizationError < StandardError ; end
|
9
|
-
# NotAuthorized is raised if the current user is not allowed to perform
|
10
|
-
# the given operation possibly on a specific object.
|
11
|
-
class NotAuthorized < AuthorizationError ; end
|
12
|
-
# AttributeAuthorizationError is more specific than NotAuthorized, signaling
|
13
|
-
# that the access was denied on the grounds of attribute conditions.
|
14
|
-
class AttributeAuthorizationError < NotAuthorized ; end
|
15
|
-
# AuthorizationUsageError is used whenever a situation is encountered
|
16
|
-
# in which the application misused the plugin. That is, if, e.g.,
|
17
|
-
# authorization rules may not be evaluated.
|
18
|
-
class AuthorizationUsageError < AuthorizationError ; end
|
19
|
-
# NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
|
20
|
-
# The exception is raised to ensure that the entire rule is invalidated.
|
21
|
-
class NilAttributeValueError < AuthorizationError; end
|
22
|
-
|
23
|
-
AUTH_DSL_FILES = [Pathname.new(Rails.root || '').join("config", "authorization_rules.rb").to_s] unless defined? AUTH_DSL_FILES
|
24
|
-
|
25
|
-
# Controller-independent method for retrieving the current user.
|
26
|
-
# Needed for model security where the current controller is not available.
|
27
|
-
def self.current_user
|
28
|
-
Thread.current["current_user"] || guest_user
|
29
|
-
end
|
30
|
-
|
31
|
-
# Controller-independent method for setting the current user.
|
32
|
-
def self.current_user=(user)
|
33
|
-
Thread.current["current_user"] = user
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.guest_user
|
37
|
-
@@guest_user ||= AnonymousUser.new
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.non_guest_current_user
|
41
|
-
current_user unless current_user.is_a?(AnonymousUser)
|
42
|
-
end
|
43
|
-
|
44
|
-
# For use in test cases only
|
45
|
-
def self.ignore_access_control(state = nil) # :nodoc:
|
46
|
-
Thread.current["ignore_access_control"] = state unless state.nil?
|
47
|
-
Thread.current["ignore_access_control"] || false
|
48
|
-
end
|
49
|
-
|
50
|
-
@@dot_path = "dot"
|
51
|
-
def self.dot_path
|
52
|
-
@@dot_path
|
53
|
-
end
|
54
|
-
|
55
|
-
def self.dot_path=(path)
|
56
|
-
@@dot_path = path
|
57
|
-
end
|
58
|
-
|
59
|
-
@@default_role = :guest
|
60
|
-
def self.default_role
|
61
|
-
@@default_role
|
62
|
-
end
|
63
|
-
|
64
|
-
def self.default_role=(role)
|
65
|
-
@@default_role = role.to_sym
|
66
|
-
end
|
67
|
-
|
68
|
-
def self.is_a_association_proxy?(object)
|
69
|
-
object.respond_to?(:proxy_association)
|
70
|
-
end
|
71
|
-
|
72
|
-
# Authorization::Engine implements the reference monitor. It may be used
|
73
|
-
# for querying the permission and retrieving obligations under which
|
74
|
-
# a certain privilege is granted for the current user.
|
75
|
-
#
|
76
|
-
class Engine
|
77
|
-
extend Forwardable
|
78
|
-
attr_reader :reader
|
79
|
-
|
80
|
-
def_delegators :@reader, :auth_rules_reader, :privileges_reader, :load, :load!
|
81
|
-
def_delegators :auth_rules_reader, :auth_rules, :roles, :omnipotent_roles, :role_hierarchy, :role_titles, :role_descriptions
|
82
|
-
def_delegators :privileges_reader, :privileges, :privilege_hierarchy
|
83
|
-
|
84
|
-
# If +reader+ is not given, a new one is created with the default
|
85
|
-
# authorization configuration of +AUTH_DSL_FILES+. If given, may be either
|
86
|
-
# a Reader object or a path to a configuration file.
|
87
|
-
def initialize(reader = nil)
|
88
|
-
#@auth_rules = AuthorizationRuleSet.new reader.auth_rules_reader.auth_rules
|
89
|
-
@reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
|
90
|
-
end
|
91
|
-
|
92
|
-
def initialize_copy(from) # :nodoc:
|
93
|
-
@reader = from.reader.clone
|
94
|
-
end
|
95
|
-
|
96
|
-
# {[priv, ctx] => [priv, ...]}
|
97
|
-
def rev_priv_hierarchy
|
98
|
-
if @rev_priv_hierarchy.nil?
|
99
|
-
@rev_priv_hierarchy = {}
|
100
|
-
privilege_hierarchy.each do |key, value|
|
101
|
-
value.each do |val|
|
102
|
-
@rev_priv_hierarchy[val] ||= []
|
103
|
-
@rev_priv_hierarchy[val] << key
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
@rev_priv_hierarchy
|
108
|
-
end
|
109
|
-
|
110
|
-
# {[priv, ctx] => [priv, ...]}
|
111
|
-
def rev_role_hierarchy
|
112
|
-
if @rev_role_hierarchy.nil?
|
113
|
-
@rev_role_hierarchy = {}
|
114
|
-
role_hierarchy.each do |higher_role, lower_roles|
|
115
|
-
lower_roles.each do |role|
|
116
|
-
(@rev_role_hierarchy[role] ||= []) << higher_role
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
@rev_role_hierarchy
|
121
|
-
end
|
122
|
-
|
123
|
-
# Returns true if privilege is met by the current user. Raises
|
124
|
-
# AuthorizationError otherwise. +privilege+ may be given with or
|
125
|
-
# without context. In the latter case, the :+context+ option is
|
126
|
-
# required.
|
127
|
-
#
|
128
|
-
# Options:
|
129
|
-
# [:+context+]
|
130
|
-
# The context part of the privilege.
|
131
|
-
# Defaults either to the tableized +class_name+ of the given :+object+, if given.
|
132
|
-
# That is, :+users+ for :+object+ of type User.
|
133
|
-
# Raises AuthorizationUsageError if context is missing and not to be inferred.
|
134
|
-
# [:+object+] An context object to test attribute checks against.
|
135
|
-
# [:+skip_attribute_test+]
|
136
|
-
# Skips those attribute checks in the
|
137
|
-
# authorization rules. Defaults to false.
|
138
|
-
# [:+user+]
|
139
|
-
# The user to check the authorization for.
|
140
|
-
# Defaults to Authorization#current_user.
|
141
|
-
# [:+bang+]
|
142
|
-
# Should NotAuthorized exceptions be raised
|
143
|
-
# Defaults to true.
|
144
|
-
#
|
145
|
-
def permit!(privilege, options = {})
|
146
|
-
return true if Authorization.ignore_access_control
|
147
|
-
options = {
|
148
|
-
:object => nil,
|
149
|
-
:skip_attribute_test => false,
|
150
|
-
:context => nil,
|
151
|
-
:bang => true
|
152
|
-
}.merge(options)
|
153
|
-
|
154
|
-
# Make sure we're handling all privileges as symbols.
|
155
|
-
privilege = privilege.is_a?( Array ) ?
|
156
|
-
privilege.flatten.collect { |priv| priv.to_sym } :
|
157
|
-
privilege.to_sym
|
158
|
-
|
159
|
-
#
|
160
|
-
# If the object responds to :proxy_reflection, we're probably working with
|
161
|
-
# an association proxy. Use 'new' to leverage ActiveRecord's builder
|
162
|
-
# functionality to obtain an object against which we can check permissions.
|
163
|
-
#
|
164
|
-
# Example: permit!( :edit, :object => user.posts )
|
165
|
-
#
|
166
|
-
if Authorization.is_a_association_proxy?(options[:object]) && options[:object].respond_to?(:new)
|
167
|
-
options[:object] = options[:object].where(nil).new
|
168
|
-
end
|
169
|
-
|
170
|
-
options[:context] ||= options[:object] && (
|
171
|
-
options[:object].class.respond_to?(:decl_auth_context) ?
|
172
|
-
options[:object].class.decl_auth_context :
|
173
|
-
options[:object].class.name.tableize.to_sym
|
174
|
-
) rescue NoMethodError
|
175
|
-
|
176
|
-
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
177
|
-
|
178
|
-
return true if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
|
179
|
-
|
180
|
-
# find a authorization rule that matches for at least one of the roles and
|
181
|
-
# at least one of the given privileges
|
182
|
-
attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
|
183
|
-
rules = matching_auth_rules(roles, privileges, options[:context])
|
184
|
-
|
185
|
-
# Test each rule in turn to see whether any one of them is satisfied.
|
186
|
-
rules.each do |rule|
|
187
|
-
return true if rule.validate?(attr_validator, options[:skip_attribute_test])
|
188
|
-
end
|
189
|
-
|
190
|
-
if options[:bang]
|
191
|
-
if rules.empty?
|
192
|
-
raise NotAuthorized, "No matching rules found for #{privilege} for User with id #{user.try(:id)} " +
|
193
|
-
"(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
|
194
|
-
"context #{options[:context].inspect})."
|
195
|
-
else
|
196
|
-
raise AttributeAuthorizationError, "#{privilege} not allowed for User with id #{user.try(:id)} on #{(options[:object] || options[:context]).inspect}."
|
197
|
-
end
|
198
|
-
else
|
199
|
-
false
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
# Calls permit! but doesn't raise authorization errors. If no exception is
|
204
|
-
# raised, permit? returns true and yields to the optional block.
|
205
|
-
def permit?(privilege, options = {}) # :yields:
|
206
|
-
if permit!(privilege, options.merge(:bang=> false))
|
207
|
-
yield if block_given?
|
208
|
-
true
|
209
|
-
else
|
210
|
-
false
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
# Returns the obligations to be met by the current user for the given
|
215
|
-
# privilege as an array of obligation hashes in form of
|
216
|
-
# [{:object_attribute => obligation_value, ...}, ...]
|
217
|
-
# where +obligation_value+ is either (recursively) another obligation hash
|
218
|
-
# or a value spec, such as
|
219
|
-
# [operator, literal_value]
|
220
|
-
# The obligation hashes in the array should be OR'ed, conditions inside
|
221
|
-
# the hashes AND'ed.
|
222
|
-
#
|
223
|
-
# Example
|
224
|
-
# {:branch => {:company => [:is, 24]}, :active => [:is, true]}
|
225
|
-
#
|
226
|
-
# Options
|
227
|
-
# [:+context+] See permit!
|
228
|
-
# [:+user+] See permit!
|
229
|
-
#
|
230
|
-
def obligations(privilege, options = {})
|
231
|
-
options = {:context => nil}.merge(options)
|
232
|
-
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
233
|
-
|
234
|
-
permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
|
235
|
-
|
236
|
-
return [] if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
|
237
|
-
|
238
|
-
attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
|
239
|
-
matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
|
240
|
-
rule.obligations(attr_validator)
|
241
|
-
end.flatten
|
242
|
-
end
|
243
|
-
|
244
|
-
# Returns the description for the given role. The description may be
|
245
|
-
# specified with the authorization rules. Returns +nil+ if none was
|
246
|
-
# given.
|
247
|
-
def description_for(role)
|
248
|
-
role_descriptions[role]
|
249
|
-
end
|
250
|
-
|
251
|
-
# Returns the title for the given role. The title may be
|
252
|
-
# specified with the authorization rules. Returns +nil+ if none was
|
253
|
-
# given.
|
254
|
-
def title_for(role)
|
255
|
-
role_titles[role]
|
256
|
-
end
|
257
|
-
|
258
|
-
# Returns the role symbols of the given user.
|
259
|
-
def roles_for(user)
|
260
|
-
user ||= Authorization.current_user
|
261
|
-
raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.try(:id)})" \
|
262
|
-
if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
|
263
|
-
|
264
|
-
Rails.logger.info("The use of user.roles is deprecated. Please add a method " +
|
265
|
-
"role_symbols to your User model.") if defined?(Rails) and Rails.respond_to?(:logger) and !user.respond_to?(:role_symbols)
|
266
|
-
|
267
|
-
roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
|
268
|
-
|
269
|
-
raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
|
270
|
-
"doesn't return an Array of Symbols (#{roles.inspect})" \
|
271
|
-
if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
|
272
|
-
|
273
|
-
(roles.empty? ? [Authorization.default_role] : roles)
|
274
|
-
end
|
275
|
-
|
276
|
-
# Returns the role symbols and inherritted role symbols for the given user
|
277
|
-
def roles_with_hierarchy_for(user)
|
278
|
-
flatten_roles(roles_for(user))
|
279
|
-
end
|
280
|
-
|
281
|
-
def self.development_reload?
|
282
|
-
if Rails.env.development?
|
283
|
-
mod_time = AUTH_DSL_FILES.map do |m|
|
284
|
-
begin
|
285
|
-
File.mtime(m)
|
286
|
-
rescue
|
287
|
-
Time.at(0)
|
288
|
-
end
|
289
|
-
end.flatten.max
|
290
|
-
@@auth_dsl_last_modified ||= mod_time
|
291
|
-
if mod_time > @@auth_dsl_last_modified
|
292
|
-
@@auth_dsl_last_modified = mod_time
|
293
|
-
return true
|
294
|
-
end
|
295
|
-
end
|
296
|
-
end
|
297
|
-
|
298
|
-
# Returns an instance of Engine, which is created if there isn't one
|
299
|
-
# yet. If +dsl_file+ is given, it is passed on to Engine.new and
|
300
|
-
# a new instance is always created.
|
301
|
-
def self.instance(dsl_file = nil)
|
302
|
-
if dsl_file or development_reload?
|
303
|
-
@@instance = new(dsl_file)
|
304
|
-
else
|
305
|
-
@@instance ||= new
|
306
|
-
end
|
307
|
-
end
|
308
|
-
|
309
|
-
class AttributeValidator # :nodoc:
|
310
|
-
attr_reader :user, :object, :engine, :context, :privilege
|
311
|
-
def initialize(engine, user, object = nil, privilege = nil, context = nil)
|
312
|
-
@engine = engine
|
313
|
-
@user = user
|
314
|
-
@object = object
|
315
|
-
@privilege = privilege
|
316
|
-
@context = context
|
317
|
-
end
|
318
|
-
|
319
|
-
def evaluate(value_block)
|
320
|
-
# TODO cache?
|
321
|
-
if value_block.is_a? Proc
|
322
|
-
instance_eval(&value_block)
|
323
|
-
else
|
324
|
-
value_block
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
private
|
330
|
-
|
331
|
-
def user_roles_privleges_from_options(privilege, options)
|
332
|
-
options = {
|
333
|
-
:user => nil,
|
334
|
-
:context => nil,
|
335
|
-
:user_roles => nil
|
336
|
-
}.merge(options)
|
337
|
-
user = options[:user] || Authorization.current_user
|
338
|
-
privileges = privilege.is_a?(Array) ? privilege : [privilege]
|
339
|
-
|
340
|
-
raise AuthorizationUsageError, "No user object given for user id (#{user.try(:id)}) or " +
|
341
|
-
"set through Authorization.current_user" unless user
|
342
|
-
|
343
|
-
roles = options[:user_roles] || flatten_roles(roles_for(user))
|
344
|
-
privileges = flatten_privileges privileges, options[:context]
|
345
|
-
[user, roles, privileges]
|
346
|
-
end
|
347
|
-
|
348
|
-
def flatten_roles(roles)
|
349
|
-
# TODO: caching?
|
350
|
-
hierarchy = role_hierarchy
|
351
|
-
flattened_roles = {}
|
352
|
-
roles.each do |role|
|
353
|
-
flattened_roles[role] = true
|
354
|
-
if (hierarchy_for_role = hierarchy[role])
|
355
|
-
hierarchy_for_role.each do |r|
|
356
|
-
flattened_roles[r] = true
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
360
|
-
flattened_roles
|
361
|
-
end
|
362
|
-
|
363
|
-
# Returns the privilege hierarchy flattened for given privileges in context.
|
364
|
-
def flatten_privileges(privileges, context = nil)
|
365
|
-
# TODO: caching?
|
366
|
-
raise AuthorizationUsageError, 'No context given or inferable from object' unless context
|
367
|
-
hierarchy = rev_priv_hierarchy
|
368
|
-
|
369
|
-
flattened_privileges = privileges.clone
|
370
|
-
flattened_privileges.each do |priv|
|
371
|
-
flattened_privileges.concat(hierarchy[[priv, nil]]) if hierarchy[[priv, nil]]
|
372
|
-
flattened_privileges.concat(hierarchy[[priv, context]]) if hierarchy[[priv, context]]
|
373
|
-
end
|
374
|
-
flattened_privileges.uniq
|
375
|
-
end
|
376
|
-
|
377
|
-
def matching_auth_rules(roles, privileges, context)
|
378
|
-
auth_rules.matching(roles, privileges, context)
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
|
383
|
-
class AuthorizationRuleSet
|
384
|
-
include Enumerable
|
385
|
-
extend Forwardable
|
386
|
-
def_delegators :@rules, :each, :length, :[]
|
387
|
-
|
388
|
-
def initialize(rules = [])
|
389
|
-
@rules = rules.clone
|
390
|
-
reset!
|
391
|
-
end
|
392
|
-
|
393
|
-
def initialize_copy(source)
|
394
|
-
@rules = @rules.collect {|rule| rule.clone}
|
395
|
-
reset!
|
396
|
-
end
|
397
|
-
|
398
|
-
def matching(roles, privileges, context)
|
399
|
-
rules = cached_auth_rules[context] || []
|
400
|
-
rules.select do |rule|
|
401
|
-
rule.matches? roles, privileges, context
|
402
|
-
end
|
403
|
-
end
|
404
|
-
|
405
|
-
def delete(rule)
|
406
|
-
@rules.delete rule
|
407
|
-
reset!
|
408
|
-
end
|
409
|
-
|
410
|
-
def <<(rule)
|
411
|
-
@rules << rule
|
412
|
-
reset!
|
413
|
-
end
|
414
|
-
|
415
|
-
def each(&block)
|
416
|
-
@rules.each &block
|
417
|
-
end
|
418
|
-
|
419
|
-
private
|
420
|
-
def reset!
|
421
|
-
@cached_auth_rules =nil
|
422
|
-
end
|
423
|
-
|
424
|
-
def cached_auth_rules
|
425
|
-
return @cached_auth_rules if @cached_auth_rules
|
426
|
-
@cached_auth_rules = {}
|
427
|
-
@rules.each do |rule|
|
428
|
-
rule.contexts.each do |context|
|
429
|
-
@cached_auth_rules[context] ||= []
|
430
|
-
@cached_auth_rules[context] << rule
|
431
|
-
end
|
432
|
-
end
|
433
|
-
@cached_auth_rules
|
434
|
-
end
|
435
|
-
end
|
436
|
-
|
437
|
-
class AuthorizationRule
|
438
|
-
attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
|
439
|
-
:source_file, :source_line
|
440
|
-
|
441
|
-
def initialize(role, privileges = [], contexts = nil, join_operator = :or,
|
442
|
-
options = {})
|
443
|
-
@role = role
|
444
|
-
@privileges = Set.new(privileges)
|
445
|
-
@contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
|
446
|
-
@join_operator = join_operator
|
447
|
-
@attributes = []
|
448
|
-
@source_file = options[:source_file]
|
449
|
-
@source_line = options[:source_line]
|
450
|
-
end
|
451
|
-
|
452
|
-
def initialize_copy(from)
|
453
|
-
@privileges = @privileges.clone
|
454
|
-
@contexts = @contexts.clone
|
455
|
-
@attributes = @attributes.collect {|attribute| attribute.clone }
|
456
|
-
end
|
457
|
-
|
458
|
-
def append_privileges(privs)
|
459
|
-
@privileges.merge(privs)
|
460
|
-
end
|
461
|
-
|
462
|
-
def append_attribute(attribute)
|
463
|
-
@attributes << attribute
|
464
|
-
end
|
465
|
-
|
466
|
-
def matches?(roles, privs, context = nil)
|
467
|
-
roles = Hash[[*roles].map { |r| [r, true] }] unless roles.is_a?(Hash)
|
468
|
-
@contexts.include?(context) && roles.include?(@role) && privs.any? { |priv| @privileges.include?(priv) }
|
469
|
-
end
|
470
|
-
|
471
|
-
def validate?(attr_validator, skip_attribute = false)
|
472
|
-
skip_attribute or @attributes.empty? or
|
473
|
-
@attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
|
474
|
-
begin
|
475
|
-
attr.validate?(attr_validator)
|
476
|
-
rescue NilAttributeValueError => e
|
477
|
-
nil # Bumping up against a nil attribute value flunks the rule.
|
478
|
-
end
|
479
|
-
end
|
480
|
-
end
|
481
|
-
|
482
|
-
def obligations(attr_validator)
|
483
|
-
exceptions = []
|
484
|
-
obligations = @attributes.collect do |attr|
|
485
|
-
begin
|
486
|
-
attr.obligation(attr_validator)
|
487
|
-
rescue NotAuthorized => e
|
488
|
-
exceptions << e
|
489
|
-
nil
|
490
|
-
end
|
491
|
-
end
|
492
|
-
|
493
|
-
if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
|
494
|
-
raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
|
495
|
-
end
|
496
|
-
|
497
|
-
if @join_operator == :and and !obligations.empty?
|
498
|
-
# cross product of OR'ed obligations in arrays
|
499
|
-
arrayed_obligations = obligations.map {|obligation| obligation.is_a?(Hash) ? [obligation] : obligation}
|
500
|
-
merged_obligations = arrayed_obligations.first
|
501
|
-
arrayed_obligations[1..-1].each do |inner_obligations|
|
502
|
-
previous_merged_obligations = merged_obligations
|
503
|
-
merged_obligations = inner_obligations.collect do |inner_obligation|
|
504
|
-
previous_merged_obligations.collect do |merged_obligation|
|
505
|
-
merged_obligation.deep_merge(inner_obligation)
|
506
|
-
end
|
507
|
-
end.flatten
|
508
|
-
end
|
509
|
-
obligations = merged_obligations
|
510
|
-
else
|
511
|
-
obligations = obligations.flatten.compact
|
512
|
-
end
|
513
|
-
obligations.empty? ? [{}] : obligations
|
514
|
-
end
|
515
|
-
|
516
|
-
def to_long_s
|
517
|
-
attributes.collect {|attr| attr.to_long_s } * "; "
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
class Attribute
|
522
|
-
# attr_conditions_hash of form
|
523
|
-
# { :object_attribute => [operator, value_block], ... }
|
524
|
-
# { :object_attribute => { :attr => ... } }
|
525
|
-
def initialize(conditions_hash)
|
526
|
-
@conditions_hash = conditions_hash
|
527
|
-
end
|
528
|
-
|
529
|
-
def initialize_copy(from)
|
530
|
-
@conditions_hash = deep_hash_clone(@conditions_hash)
|
531
|
-
end
|
532
|
-
|
533
|
-
def validate?(attr_validator, object = nil, hash = nil)
|
534
|
-
object ||= attr_validator.object
|
535
|
-
return false unless object
|
536
|
-
|
537
|
-
if Authorization.is_a_association_proxy?(object) && object.respond_to?(:empty?)
|
538
|
-
return false if object.empty?
|
539
|
-
object.each do |member|
|
540
|
-
return true if validate?(attr_validator, member, hash)
|
541
|
-
end
|
542
|
-
return false
|
543
|
-
end
|
544
|
-
|
545
|
-
(hash || @conditions_hash).all? do |attr, value|
|
546
|
-
attr_value = object_attribute_value(object, attr)
|
547
|
-
if value.is_a?(Hash)
|
548
|
-
if attr_value.is_a?(Enumerable)
|
549
|
-
attr_value.any? do |inner_value|
|
550
|
-
validate?(attr_validator, inner_value, value)
|
551
|
-
end
|
552
|
-
elsif attr_value == nil
|
553
|
-
raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
554
|
-
else
|
555
|
-
validate?(attr_validator, attr_value, value)
|
556
|
-
end
|
557
|
-
elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
|
558
|
-
evaluated = if value[1].is_a?(Proc)
|
559
|
-
attr_validator.evaluate(value[1])
|
560
|
-
else
|
561
|
-
value[1]
|
562
|
-
end
|
563
|
-
case value[0]
|
564
|
-
when :is
|
565
|
-
attr_value == evaluated
|
566
|
-
when :is_not
|
567
|
-
attr_value != evaluated
|
568
|
-
when :contains
|
569
|
-
begin
|
570
|
-
attr_value.include?(evaluated)
|
571
|
-
rescue NoMethodError => e
|
572
|
-
raise AuthorizationUsageError, "Operator contains requires a " +
|
573
|
-
"subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
|
574
|
-
"contains #{evaluated.inspect}: #{e}"
|
575
|
-
end
|
576
|
-
when :does_not_contain
|
577
|
-
begin
|
578
|
-
!attr_value.include?(evaluated)
|
579
|
-
rescue NoMethodError => e
|
580
|
-
raise AuthorizationUsageError, "Operator does_not_contain requires a " +
|
581
|
-
"subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
|
582
|
-
"does_not_contain #{evaluated.inspect}: #{e}"
|
583
|
-
end
|
584
|
-
when :intersects_with
|
585
|
-
begin
|
586
|
-
!(evaluated.to_set & attr_value.to_set).empty?
|
587
|
-
rescue NoMethodError => e
|
588
|
-
raise AuthorizationUsageError, "Operator intersects_with requires " +
|
589
|
-
"subclasses of Enumerable, got: #{attr_value.inspect} " +
|
590
|
-
"intersects_with #{evaluated.inspect}: #{e}"
|
591
|
-
end
|
592
|
-
when :is_in
|
593
|
-
begin
|
594
|
-
evaluated.include?(attr_value)
|
595
|
-
rescue NoMethodError => e
|
596
|
-
raise AuthorizationUsageError, "Operator is_in requires a " +
|
597
|
-
"subclass of Enumerable as value, got: #{attr_value.inspect} " +
|
598
|
-
"is_in #{evaluated.inspect}: #{e}"
|
599
|
-
end
|
600
|
-
when :is_not_in
|
601
|
-
begin
|
602
|
-
!evaluated.include?(attr_value)
|
603
|
-
rescue NoMethodError => e
|
604
|
-
raise AuthorizationUsageError, "Operator is_not_in requires a " +
|
605
|
-
"subclass of Enumerable as value, got: #{attr_value.inspect} " +
|
606
|
-
"is_not_in #{evaluated.inspect}: #{e}"
|
607
|
-
end
|
608
|
-
when :lt
|
609
|
-
attr_value && attr_value < evaluated
|
610
|
-
when :lte
|
611
|
-
attr_value && attr_value <= evaluated
|
612
|
-
when :gt
|
613
|
-
attr_value && attr_value > evaluated
|
614
|
-
when :gte
|
615
|
-
attr_value && attr_value >= evaluated
|
616
|
-
when :id_in_scope
|
617
|
-
evaluated.exists?(attr_value)
|
618
|
-
else
|
619
|
-
raise AuthorizationError, "Unknown operator #{value[0]}"
|
620
|
-
end
|
621
|
-
else
|
622
|
-
raise AuthorizationError, "Wrong conditions hash format"
|
623
|
-
end
|
624
|
-
end
|
625
|
-
end
|
626
|
-
|
627
|
-
# resolves all the values in condition_hash
|
628
|
-
def obligation(attr_validator, hash = nil)
|
629
|
-
hash = (hash || @conditions_hash).clone
|
630
|
-
hash.each do |attr, value|
|
631
|
-
if value.is_a?(Hash)
|
632
|
-
hash[attr] = obligation(attr_validator, value)
|
633
|
-
elsif value.is_a?(Array) and value.length == 2
|
634
|
-
hash[attr] = [value[0], attr_validator.evaluate(value[1])]
|
635
|
-
else
|
636
|
-
raise AuthorizationError, "Wrong conditions hash format"
|
637
|
-
end
|
638
|
-
end
|
639
|
-
hash
|
640
|
-
end
|
641
|
-
|
642
|
-
def to_long_s(hash = nil)
|
643
|
-
if hash
|
644
|
-
hash.inject({}) do |memo, key_val|
|
645
|
-
key, val = key_val
|
646
|
-
memo[key] = case val
|
647
|
-
when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
|
648
|
-
when Hash then to_long_s(val)
|
649
|
-
end
|
650
|
-
memo
|
651
|
-
end
|
652
|
-
else
|
653
|
-
"if_attribute #{to_long_s(@conditions_hash).inspect}"
|
654
|
-
end
|
655
|
-
end
|
656
|
-
|
657
|
-
protected
|
658
|
-
def object_attribute_value(object, attr)
|
659
|
-
begin
|
660
|
-
object.send(attr)
|
661
|
-
rescue ArgumentError, NoMethodError => e
|
662
|
-
raise AuthorizationUsageError, "Error occurred while validating attribute ##{attr} on #{object.inspect}: #{e}.\n" +
|
663
|
-
"Please check your authorization rules and ensure the attribute is correctly spelled and \n" +
|
664
|
-
"corresponds to a method on the model you are authorizing for."
|
665
|
-
end
|
666
|
-
end
|
667
|
-
|
668
|
-
def deep_hash_clone(hash)
|
669
|
-
hash.inject({}) do |memo, (key, val)|
|
670
|
-
memo[key] = case val
|
671
|
-
when Hash
|
672
|
-
deep_hash_clone(val)
|
673
|
-
when NilClass, Symbol
|
674
|
-
val
|
675
|
-
else
|
676
|
-
val.clone
|
677
|
-
end
|
678
|
-
memo
|
679
|
-
end
|
680
|
-
end
|
681
|
-
end
|
682
|
-
|
683
|
-
# An attribute condition that uses existing rules to decide validation
|
684
|
-
# and create obligations.
|
685
|
-
class AttributeWithPermission < Attribute
|
686
|
-
# E.g. privilege :read, attr_or_hash either :attribute or
|
687
|
-
# { :attribute => :deeper_attribute }
|
688
|
-
def initialize(privilege, attr_or_hash, context = nil)
|
689
|
-
@privilege = privilege
|
690
|
-
@context = context
|
691
|
-
@attr_hash = attr_or_hash
|
692
|
-
end
|
693
|
-
|
694
|
-
def initialize_copy(from)
|
695
|
-
@attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
|
696
|
-
end
|
697
|
-
|
698
|
-
def validate?(attr_validator, object = nil, hash_or_attr = nil)
|
699
|
-
object ||= attr_validator.object
|
700
|
-
hash_or_attr ||= @attr_hash
|
701
|
-
return false unless object
|
702
|
-
|
703
|
-
case hash_or_attr
|
704
|
-
when Symbol
|
705
|
-
attr_value = object_attribute_value(object, hash_or_attr)
|
706
|
-
case attr_value
|
707
|
-
when nil
|
708
|
-
raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
|
709
|
-
when Enumerable
|
710
|
-
attr_value.any? do |inner_value|
|
711
|
-
attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
|
712
|
-
end
|
713
|
-
else
|
714
|
-
attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
|
715
|
-
end
|
716
|
-
when Hash
|
717
|
-
hash_or_attr.all? do |attr, sub_hash|
|
718
|
-
attr_value = object_attribute_value(object, attr)
|
719
|
-
if attr_value == nil
|
720
|
-
raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
721
|
-
elsif attr_value.is_a?(Enumerable)
|
722
|
-
attr_value.any? do |inner_value|
|
723
|
-
validate?(attr_validator, inner_value, sub_hash)
|
724
|
-
end
|
725
|
-
else
|
726
|
-
validate?(attr_validator, attr_value, sub_hash)
|
727
|
-
end
|
728
|
-
end
|
729
|
-
when NilClass
|
730
|
-
attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
|
731
|
-
else
|
732
|
-
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
733
|
-
end
|
734
|
-
end
|
735
|
-
|
736
|
-
# may return an array of obligations to be OR'ed
|
737
|
-
def obligation(attr_validator, hash_or_attr = nil, path = [])
|
738
|
-
hash_or_attr ||= @attr_hash
|
739
|
-
case hash_or_attr
|
740
|
-
when Symbol
|
741
|
-
@context ||= begin
|
742
|
-
rule_model = attr_validator.context.to_s.classify.constantize
|
743
|
-
context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
|
744
|
-
if context_reflection.klass.respond_to?(:decl_auth_context)
|
745
|
-
context_reflection.klass.decl_auth_context
|
746
|
-
else
|
747
|
-
context_reflection.klass.name.tableize.to_sym
|
748
|
-
end
|
749
|
-
rescue # missing model, reflections
|
750
|
-
hash_or_attr.to_s.pluralize.to_sym
|
751
|
-
end
|
752
|
-
|
753
|
-
obligations = attr_validator.engine.obligations(@privilege,
|
754
|
-
:context => @context,
|
755
|
-
:user => attr_validator.user)
|
756
|
-
|
757
|
-
obligations.collect {|obl| {hash_or_attr => obl} }
|
758
|
-
when Hash
|
759
|
-
obligations_array_attrs = []
|
760
|
-
obligations =
|
761
|
-
hash_or_attr.inject({}) do |all, pair|
|
762
|
-
attr, sub_hash = pair
|
763
|
-
all[attr] = obligation(attr_validator, sub_hash, path + [attr])
|
764
|
-
if all[attr].length > 1
|
765
|
-
obligations_array_attrs << attr
|
766
|
-
else
|
767
|
-
all[attr] = all[attr].first
|
768
|
-
end
|
769
|
-
all
|
770
|
-
end
|
771
|
-
obligations = [obligations]
|
772
|
-
obligations_array_attrs.each do |attr|
|
773
|
-
next_array_size = obligations.first[attr].length
|
774
|
-
obligations = obligations.collect do |obls|
|
775
|
-
(0...next_array_size).collect do |idx|
|
776
|
-
obls_wo_array = obls.clone
|
777
|
-
obls_wo_array[attr] = obls_wo_array[attr][idx]
|
778
|
-
obls_wo_array
|
779
|
-
end
|
780
|
-
end.flatten
|
781
|
-
end
|
782
|
-
obligations
|
783
|
-
when NilClass
|
784
|
-
attr_validator.engine.obligations(@privilege,
|
785
|
-
:context => attr_validator.context,
|
786
|
-
:user => attr_validator.user)
|
787
|
-
else
|
788
|
-
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
789
|
-
end
|
790
|
-
end
|
791
|
-
|
792
|
-
def to_long_s
|
793
|
-
"if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
|
794
|
-
end
|
795
|
-
|
796
|
-
def self.reflection_for_path(parent_model, path)
|
797
|
-
reflection = path.empty? ? parent_model : begin
|
798
|
-
parent = reflection_for_path(parent_model, path[0..-2])
|
799
|
-
if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
|
800
|
-
parent.klass.reflect_on_association(path.last)
|
801
|
-
else
|
802
|
-
parent.reflect_on_association(path.last)
|
803
|
-
end
|
804
|
-
rescue
|
805
|
-
parent.reflect_on_association(path.last)
|
806
|
-
end
|
807
|
-
raise "invalid path #{path.inspect}" if reflection.nil?
|
808
|
-
reflection
|
809
|
-
end
|
810
|
-
end
|
811
|
-
|
812
|
-
# Represents a pseudo-user to facilitate anonymous users in applications
|
813
|
-
class AnonymousUser
|
814
|
-
attr_reader :role_symbols
|
815
|
-
def initialize(roles = [Authorization.default_role])
|
816
|
-
@role_symbols = roles
|
817
|
-
end
|
818
|
-
|
819
|
-
def id; end
|
820
|
-
end
|
821
|
-
end
|
1
|
+
# Authorization
|
2
|
+
require File.dirname(__FILE__) + '/reader.rb'
|
3
|
+
require "set"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module Authorization
|
7
|
+
# An exception raised if anything goes wrong in the Authorization realm
|
8
|
+
class AuthorizationError < StandardError ; end
|
9
|
+
# NotAuthorized is raised if the current user is not allowed to perform
|
10
|
+
# the given operation possibly on a specific object.
|
11
|
+
class NotAuthorized < AuthorizationError ; end
|
12
|
+
# AttributeAuthorizationError is more specific than NotAuthorized, signaling
|
13
|
+
# that the access was denied on the grounds of attribute conditions.
|
14
|
+
class AttributeAuthorizationError < NotAuthorized ; end
|
15
|
+
# AuthorizationUsageError is used whenever a situation is encountered
|
16
|
+
# in which the application misused the plugin. That is, if, e.g.,
|
17
|
+
# authorization rules may not be evaluated.
|
18
|
+
class AuthorizationUsageError < AuthorizationError ; end
|
19
|
+
# NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
|
20
|
+
# The exception is raised to ensure that the entire rule is invalidated.
|
21
|
+
class NilAttributeValueError < AuthorizationError; end
|
22
|
+
|
23
|
+
AUTH_DSL_FILES = [Pathname.new(Rails.root || '').join("config", "authorization_rules.rb").to_s] unless defined? AUTH_DSL_FILES
|
24
|
+
|
25
|
+
# Controller-independent method for retrieving the current user.
|
26
|
+
# Needed for model security where the current controller is not available.
|
27
|
+
def self.current_user
|
28
|
+
Thread.current["current_user"] || guest_user
|
29
|
+
end
|
30
|
+
|
31
|
+
# Controller-independent method for setting the current user.
|
32
|
+
def self.current_user=(user)
|
33
|
+
Thread.current["current_user"] = user
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.guest_user
|
37
|
+
@@guest_user ||= AnonymousUser.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.non_guest_current_user
|
41
|
+
current_user unless current_user.is_a?(AnonymousUser)
|
42
|
+
end
|
43
|
+
|
44
|
+
# For use in test cases only
|
45
|
+
def self.ignore_access_control(state = nil) # :nodoc:
|
46
|
+
Thread.current["ignore_access_control"] = state unless state.nil?
|
47
|
+
Thread.current["ignore_access_control"] || false
|
48
|
+
end
|
49
|
+
|
50
|
+
@@dot_path = "dot"
|
51
|
+
def self.dot_path
|
52
|
+
@@dot_path
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.dot_path=(path)
|
56
|
+
@@dot_path = path
|
57
|
+
end
|
58
|
+
|
59
|
+
@@default_role = :guest
|
60
|
+
def self.default_role
|
61
|
+
@@default_role
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.default_role=(role)
|
65
|
+
@@default_role = role.to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.is_a_association_proxy?(object)
|
69
|
+
object.respond_to?(:proxy_association)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Authorization::Engine implements the reference monitor. It may be used
|
73
|
+
# for querying the permission and retrieving obligations under which
|
74
|
+
# a certain privilege is granted for the current user.
|
75
|
+
#
|
76
|
+
class Engine
|
77
|
+
extend Forwardable
|
78
|
+
attr_reader :reader
|
79
|
+
|
80
|
+
def_delegators :@reader, :auth_rules_reader, :privileges_reader, :load, :load!
|
81
|
+
def_delegators :auth_rules_reader, :auth_rules, :roles, :omnipotent_roles, :role_hierarchy, :role_titles, :role_descriptions
|
82
|
+
def_delegators :privileges_reader, :privileges, :privilege_hierarchy
|
83
|
+
|
84
|
+
# If +reader+ is not given, a new one is created with the default
|
85
|
+
# authorization configuration of +AUTH_DSL_FILES+. If given, may be either
|
86
|
+
# a Reader object or a path to a configuration file.
|
87
|
+
def initialize(reader = nil)
|
88
|
+
#@auth_rules = AuthorizationRuleSet.new reader.auth_rules_reader.auth_rules
|
89
|
+
@reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
|
90
|
+
end
|
91
|
+
|
92
|
+
def initialize_copy(from) # :nodoc:
|
93
|
+
@reader = from.reader.clone
|
94
|
+
end
|
95
|
+
|
96
|
+
# {[priv, ctx] => [priv, ...]}
|
97
|
+
def rev_priv_hierarchy
|
98
|
+
if @rev_priv_hierarchy.nil?
|
99
|
+
@rev_priv_hierarchy = {}
|
100
|
+
privilege_hierarchy.each do |key, value|
|
101
|
+
value.each do |val|
|
102
|
+
@rev_priv_hierarchy[val] ||= []
|
103
|
+
@rev_priv_hierarchy[val] << key
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
@rev_priv_hierarchy
|
108
|
+
end
|
109
|
+
|
110
|
+
# {[priv, ctx] => [priv, ...]}
|
111
|
+
def rev_role_hierarchy
|
112
|
+
if @rev_role_hierarchy.nil?
|
113
|
+
@rev_role_hierarchy = {}
|
114
|
+
role_hierarchy.each do |higher_role, lower_roles|
|
115
|
+
lower_roles.each do |role|
|
116
|
+
(@rev_role_hierarchy[role] ||= []) << higher_role
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
@rev_role_hierarchy
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns true if privilege is met by the current user. Raises
|
124
|
+
# AuthorizationError otherwise. +privilege+ may be given with or
|
125
|
+
# without context. In the latter case, the :+context+ option is
|
126
|
+
# required.
|
127
|
+
#
|
128
|
+
# Options:
|
129
|
+
# [:+context+]
|
130
|
+
# The context part of the privilege.
|
131
|
+
# Defaults either to the tableized +class_name+ of the given :+object+, if given.
|
132
|
+
# That is, :+users+ for :+object+ of type User.
|
133
|
+
# Raises AuthorizationUsageError if context is missing and not to be inferred.
|
134
|
+
# [:+object+] An context object to test attribute checks against.
|
135
|
+
# [:+skip_attribute_test+]
|
136
|
+
# Skips those attribute checks in the
|
137
|
+
# authorization rules. Defaults to false.
|
138
|
+
# [:+user+]
|
139
|
+
# The user to check the authorization for.
|
140
|
+
# Defaults to Authorization#current_user.
|
141
|
+
# [:+bang+]
|
142
|
+
# Should NotAuthorized exceptions be raised
|
143
|
+
# Defaults to true.
|
144
|
+
#
|
145
|
+
def permit!(privilege, options = {})
|
146
|
+
return true if Authorization.ignore_access_control
|
147
|
+
options = {
|
148
|
+
:object => nil,
|
149
|
+
:skip_attribute_test => false,
|
150
|
+
:context => nil,
|
151
|
+
:bang => true
|
152
|
+
}.merge(options)
|
153
|
+
|
154
|
+
# Make sure we're handling all privileges as symbols.
|
155
|
+
privilege = privilege.is_a?( Array ) ?
|
156
|
+
privilege.flatten.collect { |priv| priv.to_sym } :
|
157
|
+
privilege.to_sym
|
158
|
+
|
159
|
+
#
|
160
|
+
# If the object responds to :proxy_reflection, we're probably working with
|
161
|
+
# an association proxy. Use 'new' to leverage ActiveRecord's builder
|
162
|
+
# functionality to obtain an object against which we can check permissions.
|
163
|
+
#
|
164
|
+
# Example: permit!( :edit, :object => user.posts )
|
165
|
+
#
|
166
|
+
if Authorization.is_a_association_proxy?(options[:object]) && options[:object].respond_to?(:new)
|
167
|
+
options[:object] = options[:object].where(nil).new
|
168
|
+
end
|
169
|
+
|
170
|
+
options[:context] ||= options[:object] && (
|
171
|
+
options[:object].class.respond_to?(:decl_auth_context) ?
|
172
|
+
options[:object].class.decl_auth_context :
|
173
|
+
options[:object].class.name.tableize.to_sym
|
174
|
+
) rescue NoMethodError
|
175
|
+
|
176
|
+
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
177
|
+
|
178
|
+
return true if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
|
179
|
+
|
180
|
+
# find a authorization rule that matches for at least one of the roles and
|
181
|
+
# at least one of the given privileges
|
182
|
+
attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
|
183
|
+
rules = matching_auth_rules(roles, privileges, options[:context])
|
184
|
+
|
185
|
+
# Test each rule in turn to see whether any one of them is satisfied.
|
186
|
+
rules.each do |rule|
|
187
|
+
return true if rule.validate?(attr_validator, options[:skip_attribute_test])
|
188
|
+
end
|
189
|
+
|
190
|
+
if options[:bang]
|
191
|
+
if rules.empty?
|
192
|
+
raise NotAuthorized, "No matching rules found for #{privilege} for User with id #{user.try(:id)} " +
|
193
|
+
"(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
|
194
|
+
"context #{options[:context].inspect})."
|
195
|
+
else
|
196
|
+
raise AttributeAuthorizationError, "#{privilege} not allowed for User with id #{user.try(:id)} on #{(options[:object] || options[:context]).inspect}."
|
197
|
+
end
|
198
|
+
else
|
199
|
+
false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Calls permit! but doesn't raise authorization errors. If no exception is
|
204
|
+
# raised, permit? returns true and yields to the optional block.
|
205
|
+
def permit?(privilege, options = {}) # :yields:
|
206
|
+
if permit!(privilege, options.merge(:bang=> false))
|
207
|
+
yield if block_given?
|
208
|
+
true
|
209
|
+
else
|
210
|
+
false
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the obligations to be met by the current user for the given
|
215
|
+
# privilege as an array of obligation hashes in form of
|
216
|
+
# [{:object_attribute => obligation_value, ...}, ...]
|
217
|
+
# where +obligation_value+ is either (recursively) another obligation hash
|
218
|
+
# or a value spec, such as
|
219
|
+
# [operator, literal_value]
|
220
|
+
# The obligation hashes in the array should be OR'ed, conditions inside
|
221
|
+
# the hashes AND'ed.
|
222
|
+
#
|
223
|
+
# Example
|
224
|
+
# {:branch => {:company => [:is, 24]}, :active => [:is, true]}
|
225
|
+
#
|
226
|
+
# Options
|
227
|
+
# [:+context+] See permit!
|
228
|
+
# [:+user+] See permit!
|
229
|
+
#
|
230
|
+
def obligations(privilege, options = {})
|
231
|
+
options = {:context => nil}.merge(options)
|
232
|
+
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
233
|
+
|
234
|
+
permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
|
235
|
+
|
236
|
+
return [] if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
|
237
|
+
|
238
|
+
attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
|
239
|
+
matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
|
240
|
+
rule.obligations(attr_validator)
|
241
|
+
end.flatten
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns the description for the given role. The description may be
|
245
|
+
# specified with the authorization rules. Returns +nil+ if none was
|
246
|
+
# given.
|
247
|
+
def description_for(role)
|
248
|
+
role_descriptions[role]
|
249
|
+
end
|
250
|
+
|
251
|
+
# Returns the title for the given role. The title may be
|
252
|
+
# specified with the authorization rules. Returns +nil+ if none was
|
253
|
+
# given.
|
254
|
+
def title_for(role)
|
255
|
+
role_titles[role]
|
256
|
+
end
|
257
|
+
|
258
|
+
# Returns the role symbols of the given user.
|
259
|
+
def roles_for(user)
|
260
|
+
user ||= Authorization.current_user
|
261
|
+
raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.try(:id)})" \
|
262
|
+
if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
|
263
|
+
|
264
|
+
Rails.logger.info("The use of user.roles is deprecated. Please add a method " +
|
265
|
+
"role_symbols to your User model.") if defined?(Rails) and Rails.respond_to?(:logger) and !user.respond_to?(:role_symbols)
|
266
|
+
|
267
|
+
roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
|
268
|
+
|
269
|
+
raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
|
270
|
+
"doesn't return an Array of Symbols (#{roles.inspect})" \
|
271
|
+
if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
|
272
|
+
|
273
|
+
(roles.empty? ? [Authorization.default_role] : roles)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Returns the role symbols and inherritted role symbols for the given user
|
277
|
+
def roles_with_hierarchy_for(user)
|
278
|
+
flatten_roles(roles_for(user))
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.development_reload?
|
282
|
+
if Rails.env.development?
|
283
|
+
mod_time = AUTH_DSL_FILES.map do |m|
|
284
|
+
begin
|
285
|
+
File.mtime(m)
|
286
|
+
rescue
|
287
|
+
Time.at(0)
|
288
|
+
end
|
289
|
+
end.flatten.max
|
290
|
+
@@auth_dsl_last_modified ||= mod_time
|
291
|
+
if mod_time > @@auth_dsl_last_modified
|
292
|
+
@@auth_dsl_last_modified = mod_time
|
293
|
+
return true
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Returns an instance of Engine, which is created if there isn't one
|
299
|
+
# yet. If +dsl_file+ is given, it is passed on to Engine.new and
|
300
|
+
# a new instance is always created.
|
301
|
+
def self.instance(dsl_file = nil)
|
302
|
+
if dsl_file or development_reload?
|
303
|
+
@@instance = new(dsl_file)
|
304
|
+
else
|
305
|
+
@@instance ||= new
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
class AttributeValidator # :nodoc:
|
310
|
+
attr_reader :user, :object, :engine, :context, :privilege
|
311
|
+
def initialize(engine, user, object = nil, privilege = nil, context = nil)
|
312
|
+
@engine = engine
|
313
|
+
@user = user
|
314
|
+
@object = object
|
315
|
+
@privilege = privilege
|
316
|
+
@context = context
|
317
|
+
end
|
318
|
+
|
319
|
+
def evaluate(value_block)
|
320
|
+
# TODO cache?
|
321
|
+
if value_block.is_a? Proc
|
322
|
+
instance_eval(&value_block)
|
323
|
+
else
|
324
|
+
value_block
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
private
|
330
|
+
|
331
|
+
def user_roles_privleges_from_options(privilege, options)
|
332
|
+
options = {
|
333
|
+
:user => nil,
|
334
|
+
:context => nil,
|
335
|
+
:user_roles => nil
|
336
|
+
}.merge(options)
|
337
|
+
user = options[:user] || Authorization.current_user
|
338
|
+
privileges = privilege.is_a?(Array) ? privilege : [privilege]
|
339
|
+
|
340
|
+
raise AuthorizationUsageError, "No user object given for user id (#{user.try(:id)}) or " +
|
341
|
+
"set through Authorization.current_user" unless user
|
342
|
+
|
343
|
+
roles = options[:user_roles] || flatten_roles(roles_for(user))
|
344
|
+
privileges = flatten_privileges privileges, options[:context]
|
345
|
+
[user, roles, privileges]
|
346
|
+
end
|
347
|
+
|
348
|
+
def flatten_roles(roles)
|
349
|
+
# TODO: caching?
|
350
|
+
hierarchy = role_hierarchy
|
351
|
+
flattened_roles = {}
|
352
|
+
roles.each do |role|
|
353
|
+
flattened_roles[role] = true
|
354
|
+
if (hierarchy_for_role = hierarchy[role])
|
355
|
+
hierarchy_for_role.each do |r|
|
356
|
+
flattened_roles[r] = true
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
flattened_roles
|
361
|
+
end
|
362
|
+
|
363
|
+
# Returns the privilege hierarchy flattened for given privileges in context.
|
364
|
+
def flatten_privileges(privileges, context = nil)
|
365
|
+
# TODO: caching?
|
366
|
+
raise AuthorizationUsageError, 'No context given or inferable from object' unless context
|
367
|
+
hierarchy = rev_priv_hierarchy
|
368
|
+
|
369
|
+
flattened_privileges = privileges.clone
|
370
|
+
flattened_privileges.each do |priv|
|
371
|
+
flattened_privileges.concat(hierarchy[[priv, nil]]) if hierarchy[[priv, nil]]
|
372
|
+
flattened_privileges.concat(hierarchy[[priv, context]]) if hierarchy[[priv, context]]
|
373
|
+
end
|
374
|
+
flattened_privileges.uniq
|
375
|
+
end
|
376
|
+
|
377
|
+
def matching_auth_rules(roles, privileges, context)
|
378
|
+
auth_rules.matching(roles, privileges, context)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
class AuthorizationRuleSet
|
384
|
+
include Enumerable
|
385
|
+
extend Forwardable
|
386
|
+
def_delegators :@rules, :each, :length, :[]
|
387
|
+
|
388
|
+
def initialize(rules = [])
|
389
|
+
@rules = rules.clone
|
390
|
+
reset!
|
391
|
+
end
|
392
|
+
|
393
|
+
def initialize_copy(source)
|
394
|
+
@rules = @rules.collect {|rule| rule.clone}
|
395
|
+
reset!
|
396
|
+
end
|
397
|
+
|
398
|
+
def matching(roles, privileges, context)
|
399
|
+
rules = cached_auth_rules[context] || []
|
400
|
+
rules.select do |rule|
|
401
|
+
rule.matches? roles, privileges, context
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def delete(rule)
|
406
|
+
@rules.delete rule
|
407
|
+
reset!
|
408
|
+
end
|
409
|
+
|
410
|
+
def <<(rule)
|
411
|
+
@rules << rule
|
412
|
+
reset!
|
413
|
+
end
|
414
|
+
|
415
|
+
def each(&block)
|
416
|
+
@rules.each &block
|
417
|
+
end
|
418
|
+
|
419
|
+
private
|
420
|
+
def reset!
|
421
|
+
@cached_auth_rules =nil
|
422
|
+
end
|
423
|
+
|
424
|
+
def cached_auth_rules
|
425
|
+
return @cached_auth_rules if @cached_auth_rules
|
426
|
+
@cached_auth_rules = {}
|
427
|
+
@rules.each do |rule|
|
428
|
+
rule.contexts.each do |context|
|
429
|
+
@cached_auth_rules[context] ||= []
|
430
|
+
@cached_auth_rules[context] << rule
|
431
|
+
end
|
432
|
+
end
|
433
|
+
@cached_auth_rules
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
class AuthorizationRule
|
438
|
+
attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
|
439
|
+
:source_file, :source_line
|
440
|
+
|
441
|
+
def initialize(role, privileges = [], contexts = nil, join_operator = :or,
|
442
|
+
options = {})
|
443
|
+
@role = role
|
444
|
+
@privileges = Set.new(privileges)
|
445
|
+
@contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
|
446
|
+
@join_operator = join_operator
|
447
|
+
@attributes = []
|
448
|
+
@source_file = options[:source_file]
|
449
|
+
@source_line = options[:source_line]
|
450
|
+
end
|
451
|
+
|
452
|
+
def initialize_copy(from)
|
453
|
+
@privileges = @privileges.clone
|
454
|
+
@contexts = @contexts.clone
|
455
|
+
@attributes = @attributes.collect {|attribute| attribute.clone }
|
456
|
+
end
|
457
|
+
|
458
|
+
def append_privileges(privs)
|
459
|
+
@privileges.merge(privs)
|
460
|
+
end
|
461
|
+
|
462
|
+
def append_attribute(attribute)
|
463
|
+
@attributes << attribute
|
464
|
+
end
|
465
|
+
|
466
|
+
def matches?(roles, privs, context = nil)
|
467
|
+
roles = Hash[[*roles].map { |r| [r, true] }] unless roles.is_a?(Hash)
|
468
|
+
@contexts.include?(context) && roles.include?(@role) && privs.any? { |priv| @privileges.include?(priv) }
|
469
|
+
end
|
470
|
+
|
471
|
+
def validate?(attr_validator, skip_attribute = false)
|
472
|
+
skip_attribute or @attributes.empty? or
|
473
|
+
@attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
|
474
|
+
begin
|
475
|
+
attr.validate?(attr_validator)
|
476
|
+
rescue NilAttributeValueError => e
|
477
|
+
nil # Bumping up against a nil attribute value flunks the rule.
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
def obligations(attr_validator)
|
483
|
+
exceptions = []
|
484
|
+
obligations = @attributes.collect do |attr|
|
485
|
+
begin
|
486
|
+
attr.obligation(attr_validator)
|
487
|
+
rescue NotAuthorized => e
|
488
|
+
exceptions << e
|
489
|
+
nil
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
|
494
|
+
raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
|
495
|
+
end
|
496
|
+
|
497
|
+
if @join_operator == :and and !obligations.empty?
|
498
|
+
# cross product of OR'ed obligations in arrays
|
499
|
+
arrayed_obligations = obligations.map {|obligation| obligation.is_a?(Hash) ? [obligation] : obligation}
|
500
|
+
merged_obligations = arrayed_obligations.first
|
501
|
+
arrayed_obligations[1..-1].each do |inner_obligations|
|
502
|
+
previous_merged_obligations = merged_obligations
|
503
|
+
merged_obligations = inner_obligations.collect do |inner_obligation|
|
504
|
+
previous_merged_obligations.collect do |merged_obligation|
|
505
|
+
merged_obligation.deep_merge(inner_obligation)
|
506
|
+
end
|
507
|
+
end.flatten
|
508
|
+
end
|
509
|
+
obligations = merged_obligations
|
510
|
+
else
|
511
|
+
obligations = obligations.flatten.compact
|
512
|
+
end
|
513
|
+
obligations.empty? ? [{}] : obligations
|
514
|
+
end
|
515
|
+
|
516
|
+
def to_long_s
|
517
|
+
attributes.collect {|attr| attr.to_long_s } * "; "
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
class Attribute
|
522
|
+
# attr_conditions_hash of form
|
523
|
+
# { :object_attribute => [operator, value_block], ... }
|
524
|
+
# { :object_attribute => { :attr => ... } }
|
525
|
+
def initialize(conditions_hash)
|
526
|
+
@conditions_hash = conditions_hash
|
527
|
+
end
|
528
|
+
|
529
|
+
def initialize_copy(from)
|
530
|
+
@conditions_hash = deep_hash_clone(@conditions_hash)
|
531
|
+
end
|
532
|
+
|
533
|
+
def validate?(attr_validator, object = nil, hash = nil)
|
534
|
+
object ||= attr_validator.object
|
535
|
+
return false unless object
|
536
|
+
|
537
|
+
if Authorization.is_a_association_proxy?(object) && object.respond_to?(:empty?)
|
538
|
+
return false if object.empty?
|
539
|
+
object.each do |member|
|
540
|
+
return true if validate?(attr_validator, member, hash)
|
541
|
+
end
|
542
|
+
return false
|
543
|
+
end
|
544
|
+
|
545
|
+
(hash || @conditions_hash).all? do |attr, value|
|
546
|
+
attr_value = object_attribute_value(object, attr)
|
547
|
+
if value.is_a?(Hash)
|
548
|
+
if attr_value.is_a?(Enumerable)
|
549
|
+
attr_value.any? do |inner_value|
|
550
|
+
validate?(attr_validator, inner_value, value)
|
551
|
+
end
|
552
|
+
elsif attr_value == nil
|
553
|
+
raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
554
|
+
else
|
555
|
+
validate?(attr_validator, attr_value, value)
|
556
|
+
end
|
557
|
+
elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
|
558
|
+
evaluated = if value[1].is_a?(Proc)
|
559
|
+
attr_validator.evaluate(value[1])
|
560
|
+
else
|
561
|
+
value[1]
|
562
|
+
end
|
563
|
+
case value[0]
|
564
|
+
when :is
|
565
|
+
attr_value == evaluated
|
566
|
+
when :is_not
|
567
|
+
attr_value != evaluated
|
568
|
+
when :contains
|
569
|
+
begin
|
570
|
+
attr_value.include?(evaluated)
|
571
|
+
rescue NoMethodError => e
|
572
|
+
raise AuthorizationUsageError, "Operator contains requires a " +
|
573
|
+
"subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
|
574
|
+
"contains #{evaluated.inspect}: #{e}"
|
575
|
+
end
|
576
|
+
when :does_not_contain
|
577
|
+
begin
|
578
|
+
!attr_value.include?(evaluated)
|
579
|
+
rescue NoMethodError => e
|
580
|
+
raise AuthorizationUsageError, "Operator does_not_contain requires a " +
|
581
|
+
"subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
|
582
|
+
"does_not_contain #{evaluated.inspect}: #{e}"
|
583
|
+
end
|
584
|
+
when :intersects_with
|
585
|
+
begin
|
586
|
+
!(evaluated.to_set & attr_value.to_set).empty?
|
587
|
+
rescue NoMethodError => e
|
588
|
+
raise AuthorizationUsageError, "Operator intersects_with requires " +
|
589
|
+
"subclasses of Enumerable, got: #{attr_value.inspect} " +
|
590
|
+
"intersects_with #{evaluated.inspect}: #{e}"
|
591
|
+
end
|
592
|
+
when :is_in
|
593
|
+
begin
|
594
|
+
evaluated.include?(attr_value)
|
595
|
+
rescue NoMethodError => e
|
596
|
+
raise AuthorizationUsageError, "Operator is_in requires a " +
|
597
|
+
"subclass of Enumerable as value, got: #{attr_value.inspect} " +
|
598
|
+
"is_in #{evaluated.inspect}: #{e}"
|
599
|
+
end
|
600
|
+
when :is_not_in
|
601
|
+
begin
|
602
|
+
!evaluated.include?(attr_value)
|
603
|
+
rescue NoMethodError => e
|
604
|
+
raise AuthorizationUsageError, "Operator is_not_in requires a " +
|
605
|
+
"subclass of Enumerable as value, got: #{attr_value.inspect} " +
|
606
|
+
"is_not_in #{evaluated.inspect}: #{e}"
|
607
|
+
end
|
608
|
+
when :lt
|
609
|
+
attr_value && attr_value < evaluated
|
610
|
+
when :lte
|
611
|
+
attr_value && attr_value <= evaluated
|
612
|
+
when :gt
|
613
|
+
attr_value && attr_value > evaluated
|
614
|
+
when :gte
|
615
|
+
attr_value && attr_value >= evaluated
|
616
|
+
when :id_in_scope
|
617
|
+
evaluated.exists?(attr_value)
|
618
|
+
else
|
619
|
+
raise AuthorizationError, "Unknown operator #{value[0]}"
|
620
|
+
end
|
621
|
+
else
|
622
|
+
raise AuthorizationError, "Wrong conditions hash format"
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
# resolves all the values in condition_hash
|
628
|
+
def obligation(attr_validator, hash = nil)
|
629
|
+
hash = (hash || @conditions_hash).clone
|
630
|
+
hash.each do |attr, value|
|
631
|
+
if value.is_a?(Hash)
|
632
|
+
hash[attr] = obligation(attr_validator, value)
|
633
|
+
elsif value.is_a?(Array) and value.length == 2
|
634
|
+
hash[attr] = [value[0], attr_validator.evaluate(value[1])]
|
635
|
+
else
|
636
|
+
raise AuthorizationError, "Wrong conditions hash format"
|
637
|
+
end
|
638
|
+
end
|
639
|
+
hash
|
640
|
+
end
|
641
|
+
|
642
|
+
def to_long_s(hash = nil)
|
643
|
+
if hash
|
644
|
+
hash.inject({}) do |memo, key_val|
|
645
|
+
key, val = key_val
|
646
|
+
memo[key] = case val
|
647
|
+
when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
|
648
|
+
when Hash then to_long_s(val)
|
649
|
+
end
|
650
|
+
memo
|
651
|
+
end
|
652
|
+
else
|
653
|
+
"if_attribute #{to_long_s(@conditions_hash).inspect}"
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
protected
|
658
|
+
def object_attribute_value(object, attr)
|
659
|
+
begin
|
660
|
+
object.send(attr)
|
661
|
+
rescue ArgumentError, NoMethodError => e
|
662
|
+
raise AuthorizationUsageError, "Error occurred while validating attribute ##{attr} on #{object.inspect}: #{e}.\n" +
|
663
|
+
"Please check your authorization rules and ensure the attribute is correctly spelled and \n" +
|
664
|
+
"corresponds to a method on the model you are authorizing for."
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
def deep_hash_clone(hash)
|
669
|
+
hash.inject({}) do |memo, (key, val)|
|
670
|
+
memo[key] = case val
|
671
|
+
when Hash
|
672
|
+
deep_hash_clone(val)
|
673
|
+
when NilClass, Symbol
|
674
|
+
val
|
675
|
+
else
|
676
|
+
val.clone
|
677
|
+
end
|
678
|
+
memo
|
679
|
+
end
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
# An attribute condition that uses existing rules to decide validation
|
684
|
+
# and create obligations.
|
685
|
+
class AttributeWithPermission < Attribute
|
686
|
+
# E.g. privilege :read, attr_or_hash either :attribute or
|
687
|
+
# { :attribute => :deeper_attribute }
|
688
|
+
def initialize(privilege, attr_or_hash, context = nil)
|
689
|
+
@privilege = privilege
|
690
|
+
@context = context
|
691
|
+
@attr_hash = attr_or_hash
|
692
|
+
end
|
693
|
+
|
694
|
+
def initialize_copy(from)
|
695
|
+
@attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
|
696
|
+
end
|
697
|
+
|
698
|
+
def validate?(attr_validator, object = nil, hash_or_attr = nil)
|
699
|
+
object ||= attr_validator.object
|
700
|
+
hash_or_attr ||= @attr_hash
|
701
|
+
return false unless object
|
702
|
+
|
703
|
+
case hash_or_attr
|
704
|
+
when Symbol
|
705
|
+
attr_value = object_attribute_value(object, hash_or_attr)
|
706
|
+
case attr_value
|
707
|
+
when nil
|
708
|
+
raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
|
709
|
+
when Enumerable
|
710
|
+
attr_value.any? do |inner_value|
|
711
|
+
attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
|
712
|
+
end
|
713
|
+
else
|
714
|
+
attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
|
715
|
+
end
|
716
|
+
when Hash
|
717
|
+
hash_or_attr.all? do |attr, sub_hash|
|
718
|
+
attr_value = object_attribute_value(object, attr)
|
719
|
+
if attr_value == nil
|
720
|
+
raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
721
|
+
elsif attr_value.is_a?(Enumerable)
|
722
|
+
attr_value.any? do |inner_value|
|
723
|
+
validate?(attr_validator, inner_value, sub_hash)
|
724
|
+
end
|
725
|
+
else
|
726
|
+
validate?(attr_validator, attr_value, sub_hash)
|
727
|
+
end
|
728
|
+
end
|
729
|
+
when NilClass
|
730
|
+
attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
|
731
|
+
else
|
732
|
+
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
733
|
+
end
|
734
|
+
end
|
735
|
+
|
736
|
+
# may return an array of obligations to be OR'ed
|
737
|
+
def obligation(attr_validator, hash_or_attr = nil, path = [])
|
738
|
+
hash_or_attr ||= @attr_hash
|
739
|
+
case hash_or_attr
|
740
|
+
when Symbol
|
741
|
+
@context ||= begin
|
742
|
+
rule_model = attr_validator.context.to_s.classify.constantize
|
743
|
+
context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
|
744
|
+
if context_reflection.klass.respond_to?(:decl_auth_context)
|
745
|
+
context_reflection.klass.decl_auth_context
|
746
|
+
else
|
747
|
+
context_reflection.klass.name.tableize.to_sym
|
748
|
+
end
|
749
|
+
rescue # missing model, reflections
|
750
|
+
hash_or_attr.to_s.pluralize.to_sym
|
751
|
+
end
|
752
|
+
|
753
|
+
obligations = attr_validator.engine.obligations(@privilege,
|
754
|
+
:context => @context,
|
755
|
+
:user => attr_validator.user)
|
756
|
+
|
757
|
+
obligations.collect {|obl| {hash_or_attr => obl} }
|
758
|
+
when Hash
|
759
|
+
obligations_array_attrs = []
|
760
|
+
obligations =
|
761
|
+
hash_or_attr.inject({}) do |all, pair|
|
762
|
+
attr, sub_hash = pair
|
763
|
+
all[attr] = obligation(attr_validator, sub_hash, path + [attr])
|
764
|
+
if all[attr].length > 1
|
765
|
+
obligations_array_attrs << attr
|
766
|
+
else
|
767
|
+
all[attr] = all[attr].first
|
768
|
+
end
|
769
|
+
all
|
770
|
+
end
|
771
|
+
obligations = [obligations]
|
772
|
+
obligations_array_attrs.each do |attr|
|
773
|
+
next_array_size = obligations.first[attr].length
|
774
|
+
obligations = obligations.collect do |obls|
|
775
|
+
(0...next_array_size).collect do |idx|
|
776
|
+
obls_wo_array = obls.clone
|
777
|
+
obls_wo_array[attr] = obls_wo_array[attr][idx]
|
778
|
+
obls_wo_array
|
779
|
+
end
|
780
|
+
end.flatten
|
781
|
+
end
|
782
|
+
obligations
|
783
|
+
when NilClass
|
784
|
+
attr_validator.engine.obligations(@privilege,
|
785
|
+
:context => attr_validator.context,
|
786
|
+
:user => attr_validator.user)
|
787
|
+
else
|
788
|
+
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
def to_long_s
|
793
|
+
"if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
|
794
|
+
end
|
795
|
+
|
796
|
+
def self.reflection_for_path(parent_model, path)
|
797
|
+
reflection = path.empty? ? parent_model : begin
|
798
|
+
parent = reflection_for_path(parent_model, path[0..-2])
|
799
|
+
if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
|
800
|
+
parent.klass.reflect_on_association(path.last)
|
801
|
+
else
|
802
|
+
parent.reflect_on_association(path.last)
|
803
|
+
end
|
804
|
+
rescue
|
805
|
+
parent.reflect_on_association(path.last)
|
806
|
+
end
|
807
|
+
raise "invalid path #{path.inspect}" if reflection.nil?
|
808
|
+
reflection
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
812
|
+
# Represents a pseudo-user to facilitate anonymous users in applications
|
813
|
+
class AnonymousUser
|
814
|
+
attr_reader :role_symbols
|
815
|
+
def initialize(roles = [Authorization.default_role])
|
816
|
+
@role_symbols = roles
|
817
|
+
end
|
818
|
+
|
819
|
+
def id; end
|
820
|
+
end
|
821
|
+
end
|