annotation_security 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. data/CHANGELOG.md +14 -0
  2. data/HOW-TO.md +275 -0
  3. data/{MIT-LICENSE → LICENSE} +1 -1
  4. data/README.md +39 -0
  5. data/Rakefile +62 -55
  6. data/assets/app/helpers/annotation_security_helper.rb +8 -8
  7. data/assets/config/initializers/annotation_security.rb +11 -11
  8. data/assets/config/security/relations.rb +20 -20
  9. data/assets/vendor/plugins/annotation_security/init.rb +13 -13
  10. data/bin/annotation_security +7 -7
  11. data/lib/annotation_security/exceptions.rb +124 -124
  12. data/lib/annotation_security/exec.rb +188 -188
  13. data/lib/annotation_security/filters.rb +37 -37
  14. data/lib/annotation_security/includes/action_controller.rb +144 -143
  15. data/lib/annotation_security/includes/active_record.rb +27 -27
  16. data/lib/annotation_security/includes/helper.rb +215 -215
  17. data/lib/annotation_security/includes/resource.rb +84 -84
  18. data/lib/annotation_security/includes/role.rb +30 -30
  19. data/lib/annotation_security/includes/user.rb +26 -26
  20. data/lib/annotation_security/manager/policy_factory.rb +29 -29
  21. data/lib/annotation_security/manager/policy_manager.rb +79 -79
  22. data/lib/annotation_security/manager/relation_loader.rb +272 -272
  23. data/lib/annotation_security/manager/resource_manager.rb +36 -36
  24. data/lib/annotation_security/manager/right_loader.rb +87 -87
  25. data/lib/annotation_security/model_observer.rb +61 -61
  26. data/lib/annotation_security/policy/abstract_policy.rb +344 -344
  27. data/lib/annotation_security/policy/abstract_static_policy.rb +75 -75
  28. data/lib/annotation_security/policy/all_resources_policy.rb +20 -20
  29. data/lib/annotation_security/policy/rule.rb +340 -340
  30. data/lib/annotation_security/policy/rule_set.rb +138 -138
  31. data/lib/annotation_security/rails.rb +38 -38
  32. data/lib/annotation_security/user_wrapper.rb +73 -73
  33. data/lib/annotation_security/utils.rb +141 -141
  34. data/lib/annotation_security/version.rb +10 -0
  35. data/lib/annotation_security.rb +102 -97
  36. data/lib/extensions/action_controller.rb +32 -32
  37. data/lib/extensions/active_record.rb +34 -34
  38. data/lib/extensions/filter.rb +133 -133
  39. data/lib/extensions/object.rb +10 -10
  40. data/lib/security_context.rb +589 -551
  41. data/spec/annotation_security/exceptions_spec.rb +16 -16
  42. data/spec/annotation_security/includes/helper_spec.rb +82 -82
  43. data/spec/annotation_security/manager/policy_manager_spec.rb +15 -15
  44. data/spec/annotation_security/manager/resource_manager_spec.rb +17 -17
  45. data/spec/annotation_security/manager/right_loader_spec.rb +17 -17
  46. data/spec/annotation_security/policy/abstract_policy_spec.rb +16 -16
  47. data/spec/annotation_security/policy/all_resources_policy_spec.rb +24 -24
  48. data/spec/annotation_security/policy/rule_set_spec.rb +112 -112
  49. data/spec/annotation_security/policy/rule_spec.rb +77 -77
  50. data/spec/annotation_security/policy/test_policy_spec.rb +80 -80
  51. data/spec/annotation_security/security_context_spec.rb +78 -78
  52. data/spec/annotation_security/utils_spec.rb +73 -73
  53. data/spec/helper/test_controller.rb +65 -65
  54. data/spec/helper/test_helper.rb +5 -5
  55. data/spec/helper/test_relations.rb +6 -6
  56. data/spec/helper/test_resource.rb +38 -38
  57. data/spec/helper/test_role.rb +21 -21
  58. data/spec/helper/test_user.rb +31 -31
  59. data/spec/rails_stub.rb +37 -37
  60. metadata +94 -72
  61. data/CHANGELOG +0 -2
  62. data/HOW-TO +0 -261
  63. data/README +0 -39
@@ -1,551 +1,589 @@
1
- #
2
- # = lib/security_context.rb
3
- #
4
- # Contains the SecurityContext
5
- require 'active_support'
6
-
7
- # = SecurityContext
8
- #
9
- # The SecurityContext provides methods for all security concerns of
10
- # the current request.
11
- #
12
- # For every request, it has to be initialized using #current_user=. It is
13
- # recommended to do this in a security filter, which can be used to catch
14
- # AnnotationSecurityExceptions as well.
15
- #
16
- # The SecurityContext is implemented as a singleton for the current thread.
17
- # Thus, all instance methods can be send to the class as well.
18
- #
19
- class SecurityContext
20
-
21
- # Returns current security context
22
- #
23
- def self.current
24
- Thread.current[:security_context]
25
- end
26
-
27
- # At the begin of a request, the security context will be initialized for the
28
- # current controller.
29
- #
30
- def self.initialize(controller) # :nodoc:
31
- load(new(controller))
32
- end
33
-
34
- # As the security context is a singleton bound to the current thread,
35
- # it will not be available in other threads. The following example shows
36
- # how to use the security context inside of a spawn block
37
- #
38
- # copy = SecurityContext.copy
39
- # spawn do
40
- # SecurityContext.load(copy)
41
- # begin
42
- # # ...
43
- # rescue SecurityViolationError
44
- # puts 'Security was violated'
45
- # end
46
- # end
47
- #
48
- def self.load(sec_context)
49
- Thread.current[:security_context] = sec_context
50
- end
51
-
52
- if RAILS_ENV == 'development'
53
- # Disables all security checkings.
54
- # Is only available in development mode.
55
- def self.ignore_security!
56
- security_methods.each do |method|
57
- class_eval "def self.#{method}(*args); true; end"
58
- end
59
- end
60
- end
61
-
62
- ## ===========================================================================
63
- ## Instance
64
-
65
- # Initialize context for the given controller
66
- #
67
- def initialize(controller) # :nodoc:
68
- super()
69
-
70
- @controller = controller
71
-
72
- # initialize rule
73
-
74
- # rules that are not bound to any source,
75
- # will be triggered by model observer
76
- @context_rules = new_rules_hash
77
- @valid_objects = new_valid_objects_hash
78
-
79
- # rules bound to request param
80
- @param_rules = new_bound_rules_hash
81
- @param_valid_objects = new_bound_valid_objects_hash
82
-
83
- # rules bound to variable
84
- @var_rules = new_bound_rules_hash
85
- @var_valid_objects = new_bound_valid_objects_hash
86
-
87
- # Hash with all required policies
88
- @policies = new_policy_hash
89
- end
90
-
91
- # Sets the current user. This has to be done in a before or around filter,
92
- # *before* entering the action. Elsewise, the user will be interpreted as
93
- # not being logged in. Once set, the current user cannot be changed.
94
- #
95
- def credential=(user)
96
- if @cred_set
97
- raise AnnotationSecurity::AnnotationSecurityError,
98
- "Credential already set for this request"
99
- end
100
- @cred_set = true
101
- @credential = user
102
- end
103
-
104
- # Get the current credential
105
- def credential
106
- @credential
107
- end
108
-
109
- alias current_credential= credential=
110
- alias current_credential credential
111
-
112
- # Creates a copy of the current security context.
113
- # See #load for more information.
114
- def copy
115
- returning self.class.new(@controller) { |sc| sc.credential = credential }
116
- end
117
-
118
- # Will be set if an security exception was catched by the security filter
119
- def security_exception=(ex) # :nodoc:
120
- @security_exception = ex
121
- @controller.security_exception = ex
122
- end
123
-
124
- # If the action was aborted due to a security exception, this returns the
125
- # exception that was raised. Returns nil if no exception occurred.
126
- #
127
- def security_exception
128
- @security_exception
129
- end
130
-
131
- # See eval_with_security.
132
- def send_with_security(rules, obj, msg, *args, &proc)
133
- eval_with_security(rules) { obj.send(msg, *args, &proc) }
134
- end
135
-
136
- # Evaluates the given block, additionally using the given rules.
137
- # rules == [ { :action => action, :resource => res_type, :source => binding }, ...]
138
- # action and res_type should be symbols, binding is optional
139
- #
140
- def eval_with_security(rules)
141
- install_rules(rules)
142
-
143
- apply_rules_before_action
144
-
145
- result = yield
146
-
147
- apply_rules_after_action
148
-
149
- result
150
- rescue AnnotationSecurity::SecurityError
151
- SecurityContext.security_exception = $!
152
- raise $!
153
- ensure
154
- uninstall_rules(rules)
155
- result
156
- end
157
-
158
- def apply_rules_before_action # :nodoc:
159
- # apply static rules before entering the action
160
- apply_static_rules
161
- # bindings may apply to parameters, try to check them too
162
- apply_param_rules
163
- apply_var_rules
164
- end
165
-
166
- def apply_rules_after_action # :nodoc:
167
- # check again, bindings may have been changed
168
- apply_var_rules
169
- end
170
-
171
- # Returns true iif the operation defined by +policy_args+ is allowed.
172
- #
173
- # The following calls to \#allowed? are allowed:
174
- #
175
- # allowed? :show, :resource, @resource
176
- # # => true if the current user has the right to show @resource,
177
- # # which belongs to the :resource resource-class
178
- #
179
- # In case of model objects or other classes which implement a #resource_type
180
- # method the the second argument may be ommited
181
- #
182
- # allowed? :show, @resource
183
- # # equivalent to the above call if @resource.resource_type == :resource
184
- #
185
- # A policy description used as a controller annotation may also be to check
186
- # a right
187
- #
188
- # allowed? "show resource", @resource
189
- # # => true if the current user has the right "show resource" for @resource
190
- #
191
- # A policy may also be applied without an object representing the context:
192
- #
193
- # allowed? :show, :resource
194
- # # => true if the current may show resources.
195
- #
196
- # This will only check system and pretest rules. The result +true+ does not
197
- # mean that the user may show all resources. However, a +false+ indicates
198
- # that the user is not allowed to show any resources.
199
- #
200
- # If the resource type is omitted as well, only rules defined for all
201
- # resources can be tested. See RelationLoader#all_resources for details.
202
- #
203
- # allowed? :administrate
204
- # # => true if the user is allowed to administrate all resources.
205
- #
206
- def allowed?(*policy_args)
207
- policy_args = AnnotationSecurity::Utils.parse_policy_arguments(policy_args)
208
- __allowed?(*policy_args)
209
- end
210
-
211
- # Equivalent to allowed?; is? is provided for better readability.
212
- #
213
- # SecurityContext.allowed? :logged_in
214
- # vs
215
- # SecurityContext.is? :logged_in
216
- #
217
- alias is? allowed?
218
-
219
- # Raises a SecurityViolationError if the rule defined by +policy_args+ is not
220
- # allowed. See allowed? for details.
221
- #
222
- def apply_rule(*args)
223
- self.class.raise_access_denied(*args) unless allowed?(*args)
224
- end
225
-
226
- # Checks the rules of an other action. Note that rules that are bound to a
227
- # variable can not be checked.
228
- #
229
- # ==== Parameters
230
- # * +controller+ Symbol representing the controller, like :resource
231
- # * +action+ The called action, like :update
232
- # * +objects+ (optional) List of objects that will be relevant for that action.
233
- # * +params+ (optional) Hash of the passed parameters, like :id => 1.
234
- #
235
- # ==== Examples
236
- #
237
- # Checks static and pretest rules.
238
- # allow_action? :resource, :create
239
- # # => true if the current user may execute ResourcesController#create
240
- #
241
- # Checks static, pretest and context rules
242
- # allow_action? :resource, :edit, [@resource]
243
- # # => true if the current user may execute ResourcesController#edit,
244
- # # assuming that @resource will be used in that action
245
- #
246
- # Checks static, pretest and context rules and all rules that are bound
247
- # to :id.
248
- # allow_action? :resource, :edit, [@resource], {:id => 4}
249
- # # => true if the current user may execute ResourcesController#edit,
250
- # # assuming that @resource will be used in that action
251
- #
252
- def allow_action?(*args) # :nodoc:
253
-
254
- controller, action, objects, params =
255
- AnnotationSecurity::Utils.parse_action_args(args)
256
-
257
- # var rules are ignored here
258
- context_rules, param_rules, _ = get_rule_set(controller, action)
259
-
260
- # check static rules
261
- evaluate_statically(context_rules)
262
-
263
- # check context rules for all objects
264
- objects.each do |o|
265
- res_type = o.resource_type
266
- evaluate_context_rules(context_rules, res_type, o)
267
- end
268
-
269
- evaluate_bound_rules_for_params(param_rules, params)
270
-
271
- true
272
- rescue SecurityViolationError
273
- return false
274
- end
275
-
276
- # Applies all system and pretest rules of the current action.
277
- # Raises a SecurityViolationError if a rule is violated.
278
- #
279
- def apply_static_rules # :nodoc:
280
- evaluate_statically(@context_rules)
281
- end
282
-
283
- def apply_param_rules # :nodoc:
284
- evaluate_bound_rules(@param_rules, @param_valid_objects)
285
- end
286
-
287
- def apply_var_rules # :nodoc:
288
- evaluate_bound_rules(@var_rules, @var_valid_objects)
289
- end
290
-
291
- # Applies all rules of the current action to the resource defined by
292
- # +resource_args+. Raises a SecurityViolationError if a rule is
293
- # violated.
294
- #
295
- def apply_context_rules(*res_args) # :nodoc:
296
- restype, res = AnnotationSecurity::Utils.parse_resource_arguments(res_args)
297
- evaluate_context_rules(@context_rules, restype, res)
298
- end
299
-
300
- alias apply_rules apply_context_rules # :nodoc:
301
-
302
- # Call if a resource object was touched during an action. Will be called
303
- # automatically for model objects.
304
- #
305
- # Applies all rules that are currently active to the resource defined by
306
- # +resource_args+. Raises a SecurityViolationError if a rule is
307
- # violated.
308
- #
309
- # ==== Usage
310
- # observe :resource, @resource
311
- # where <tt>:resource</tt> is the resource type @resource belongs to, or
312
- # observe @resource
313
- # which is equivalent if <tt>@resource.resource_name == :resource</tt>
314
- #
315
- def observe(*resource_args)
316
- apply_context_rules(*resource_args)
317
- end
318
-
319
- # Raise a SecurityViolationError.
320
- # See allowed? for details on +policy_args+.
321
- #
322
- def self.raise_access_denied(*policy_args)
323
- log_access_denied(policy_args)
324
- raise SecurityViolationError.access_denied(credential,*policy_args)
325
- end
326
-
327
- # Activates access logging for the current request.
328
- #
329
- def log!(&proc)
330
- @enable_logging = true
331
- @log = proc || Proc.new do |result, action, res_type, resource|
332
- result = result ? 'ALLOWED' : 'REFUSED' unless result.is_a? String
333
- msg = "%-8s %-10s %-16s %s" % [result, action, res_type, resource]
334
- puts msg
335
- end
336
- end
337
-
338
- def log_access_denied(policy_args) # :nodoc:
339
- @log.call('DENIED!', *policy_args) if @enable_logging
340
- end
341
-
342
- protected
343
-
344
- def log_access_check(*policy_args)
345
- @log.call(*policy_args) if @enable_logging
346
- end
347
-
348
- private
349
-
350
- # data =========================================================================
351
-
352
- # { binding => { :res_type1 => [:action1, ...], ... }, ... }
353
- def new_bound_rules_hash() # :nodoc:
354
- Hash.new { |h,k| h[k] = new_rules_hash }
355
- end
356
-
357
- # { :res_type1 => [:action1, ...], ... }
358
- def new_rules_hash() # :nodoc:
359
- Hash.new { |h,k| h[k] = [] }
360
- end
361
-
362
- # { binding => [object1, ...], ...}
363
- def new_bound_valid_objects_hash() # :nodoc:
364
- Hash.new { |h,k| h[k] = [] }
365
- end
366
-
367
- # { :res_type1 => { :action1 => [object1, ...], ...}, ...}
368
- def new_valid_objects_hash() # :nodoc:
369
- Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
370
- end
371
-
372
- # {:res_type1 => policy1, ...}
373
- def new_policy_hash() # :nodoc:
374
- Hash.new { |h,k| h[k] = new_policy(k) }
375
- end
376
-
377
- def new_policy(resource_type) # :nodoc:
378
- AnnotationSecurity::PolicyManager.create_policy(resource_type, credential)
379
- end
380
-
381
- # Get the policy for a resource type from the cache
382
- def policy(res_type) # :nodoc:
383
- @policies[res_type]
384
- end
385
-
386
- # rules management =============================================================
387
-
388
- def install_rules(rules, rule_set=nil, controller=@controller.class)
389
- rules.each { |rule| install_rule rule, rule_set, controller }
390
- end
391
-
392
- def install_rule(rule, rule_set, controller)
393
- rule_list(rule, rule_set, controller) << rule[:action]
394
- end
395
-
396
- def uninstall_rules(rules, rule_set=nil, controller=@controller.class)
397
- rules.each { |rule| uninstall_rule rule, rule_set, controller }
398
- end
399
-
400
- def uninstall_rule(rule, rule_set, controller)
401
- list = rule_list(rule, rule_set, controller)
402
- i = list.index(rule[:action])
403
- list.delete_at(i) if i
404
- end
405
-
406
- def rule_list(rule, rule_set, controller)
407
- rule_set ||= [@context_rules, @param_rules, @var_rules]
408
- resource = rule[:resource] || controller.default_resource
409
- source = rule[:source]
410
- if source.nil?
411
- list = rule_set.first[resource]
412
- elsif source.is_a? Symbol
413
- list = rule_set.second[source][resource]
414
- else
415
- list = rule_set.third[source][resource]
416
- end
417
- list
418
- end
419
-
420
- # returns rule set for other controller actions
421
- def get_rule_set(controller, action) # :nodoc:
422
- @rule_sets ||= Hash.new { |h,k| h[k] = {} }
423
- rule_set = @rule_sets[controller][action]
424
- unless rule_set
425
- rule_set = [new_rules_hash, new_bound_rules_hash, new_bound_rules_hash]
426
- rules = controller.descriptions_of action
427
- install_rules rules, rule_set, controller
428
- @rule_sets[controller][action] = rule_set
429
- end
430
- rule_set
431
- end
432
-
433
- # rule evaluation ==============================================================
434
-
435
- # Evaluate the rules statically, skips all rules that are static only.
436
- # * +rules+ a Hash like {:resource_type => [:right1, :right2]}
437
- def evaluate_statically(rules) # :nodoc:
438
- # rules == { :resource1 => [:right1, ...], ... }
439
- rules.each_pair do |resource_type,rights|
440
- policy(resource_type).evaluate_statically(rights)
441
- end
442
- end
443
-
444
- # Checks bound rules. Evaluates the bindings, on success adds objects
445
- # to valid objects.
446
- # rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
447
- # valid_objects == { binding1 => [object1, ...], ... }
448
- def evaluate_bound_rules(rules, valid_objects) # :nodoc:
449
- evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
450
- @controller.values_of_source(binding)
451
- end
452
- end
453
-
454
- # Checks bound rules using the values from params
455
- # rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
456
- # params == { binding1 => object1, ... }
457
- def evaluate_bound_rules_for_params(rules, params) # :nodoc:
458
- valid_objects = new_bound_valid_objects_hash
459
- evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
460
- values = params[binding]
461
- values.is_a?(Array) ? values : [values]
462
- end
463
- end
464
-
465
- def evaluate_bound_rules_with_binding(rules, valid_objects, &proc) # :nodoc:
466
- rules.each_key do |binding|
467
- value_ids = proc.call(binding)
468
- rules[binding].each_key do |res_type|
469
- values = value_ids.collect do |id|
470
- AnnotationSecurity::ResourceManager.get_resource res_type, id
471
- end
472
- values.compact!
473
- values_of_res_type = values - valid_objects[binding]
474
- values_of_res_type.each do |resource|
475
- evaluate_rules(rules[binding][res_type],
476
- res_type,
477
- resource)
478
- valid_objects[binding] << resource
479
- end
480
- end
481
- end
482
- end
483
-
484
- # Checks context rules for given resource
485
- # rules == { :res_type1 => [:action1, ...], ... }
486
- def evaluate_context_rules(rules, res_type, res) # :nodoc:
487
- evaluate_rules(rules[res_type], res_type, res)
488
- end
489
-
490
- # Checks if actions on resource are allowed. If true, adds to valid objects.
491
- # Returns true
492
- # actions == [:action1, ...]
493
- # valid_objects == { :action1 => [object1, ...], ... }
494
- def evaluate_rules(actions, res_type, resource) # :nodoc:
495
- valid_objects = @valid_objects[res_type]
496
- actions.each do |action|
497
- unless valid_objects[action].index(resource)
498
- __apply_rule(action, res_type, resource)
499
- valid_objects[action] << resource
500
- end
501
- end
502
- true
503
- end
504
-
505
- # Usage:
506
- # __allowed? :show, :assignment, an_assignment
507
- def __allowed?(action, res_type, resource=nil) # :nodoc:
508
-
509
- block = lambda do
510
- if resource
511
- policy(res_type).allowed?(action, resource)
512
- else
513
- policy(res_type).static_policy.allowed?(action, nil)
514
- end
515
- end
516
-
517
- returning block.call do |r|
518
- log_access_check r, action, res_type, resource
519
- end
520
- end
521
-
522
- # Raises a SecurityViolationError if the rule defined by +policy_args+ is not
523
- # allowed. See __allowed? for details.
524
- #
525
- def __apply_rule(*args) # :nodoc:
526
- self.class.raise_access_denied(*args) unless __allowed?(*args)
527
- end
528
-
529
- #=============================================================================
530
- # Singleton
531
-
532
- def self.security_methods
533
- instance_methods(false)
534
- end
535
-
536
- # create singleton methods
537
- security_methods.each do |method|
538
- if method.to_s.ends_with? '='
539
- # setters need a different handling
540
- class_eval %{
541
- def self.#{method}(value)
542
- current.#{method} value
543
- end }
544
- else
545
- class_eval %{
546
- def self.#{method}(*args,&proc)
547
- current.#{method}(*args,&proc)
548
- end }
549
- end
550
- end
551
- end
1
+ #
2
+ # = lib/security_context.rb
3
+ #
4
+ # Contains the SecurityContext
5
+ require 'active_support'
6
+
7
+ # = SecurityContext
8
+ #
9
+ # The SecurityContext provides methods for all security concerns of
10
+ # the current request.
11
+ #
12
+ # For every request, it has to be initialized using #current_user=. It is
13
+ # recommended to do this in a security filter, which can be used to catch
14
+ # AnnotationSecurityExceptions as well.
15
+ #
16
+ # The SecurityContext is implemented as a singleton for the current thread.
17
+ # Thus, all instance methods can be send to the class as well.
18
+ #
19
+ class SecurityContext
20
+
21
+ # Returns current security context
22
+ #
23
+ def self.current
24
+ Thread.current[:security_context]
25
+ end
26
+
27
+ # At the begin of a request, the security context will be initialized for the
28
+ # current controller.
29
+ #
30
+ def self.initialize(controller) # :nodoc:
31
+ load(new(controller))
32
+ end
33
+
34
+ # As the security context is a singleton bound to the current thread,
35
+ # it will not be available in other threads. The following example shows
36
+ # how to use the security context inside of a spawn block
37
+ #
38
+ # copy = SecurityContext.copy
39
+ # spawn do
40
+ # SecurityContext.load(copy)
41
+ # begin
42
+ # # ...
43
+ # rescue SecurityViolationError
44
+ # puts 'Security was violated'
45
+ # end
46
+ # end
47
+ #
48
+ def self.load(sec_context)
49
+ Thread.current[:security_context] = sec_context
50
+ end
51
+
52
+ if Rails.env == 'development'
53
+ # Disables all security checkings.
54
+ # Is only available in development mode.
55
+ def self.ignore_security!
56
+ security_methods.each do |method|
57
+ class_eval "def self.#{method}(*args); true; end"
58
+ end
59
+ end
60
+ end
61
+
62
+ ## ===========================================================================
63
+ ## Instance
64
+
65
+ # Initialize context for the given controller
66
+ #
67
+ def initialize(controller) # :nodoc:
68
+ super()
69
+
70
+ @controller = controller
71
+
72
+ # initialize rule
73
+
74
+ # rules that are not bound to any source,
75
+ # will be triggered by model observer
76
+ @context_rules = new_rules_hash
77
+ @valid_objects = new_valid_objects_hash
78
+
79
+ # rules bound to request param
80
+ @param_rules = new_bound_rules_hash
81
+ @param_valid_objects = new_bound_valid_objects_hash
82
+
83
+ # rules bound to variable
84
+ @var_rules = new_bound_rules_hash
85
+ @var_valid_objects = new_bound_valid_objects_hash
86
+
87
+ # Hash with all required policies
88
+ @policies = new_policy_hash
89
+ end
90
+
91
+ # Sets the current user. This has to be done in a before or around filter,
92
+ # *before* entering the action. Elsewise, the user will be interpreted as
93
+ # not being logged in. Once set, the current user cannot be changed.
94
+ #
95
+ def credential=(user)
96
+ if @cred_set
97
+ raise AnnotationSecurity::AnnotationSecurityError,
98
+ "Credential already set for this request"
99
+ end
100
+ @cred_set = true
101
+ @credential = user
102
+ end
103
+
104
+ # Get the current credential
105
+ def credential
106
+ @credential
107
+ end
108
+
109
+ alias current_credential= credential=
110
+ alias current_credential credential
111
+
112
+ # Creates a copy of the current security context.
113
+ # See #load for more information.
114
+ def copy
115
+ self.class.new(@controller).tap { |sc| sc.credential = credential }
116
+ end
117
+
118
+ # Will be set if an security exception was catched by the security filter
119
+ def security_exception=(ex) # :nodoc:
120
+ @security_exception = ex
121
+ @controller.security_exception = ex
122
+ end
123
+
124
+ # If the action was aborted due to a security exception, this returns the
125
+ # exception that was raised. Returns nil if no exception occurred.
126
+ #
127
+ def security_exception
128
+ @security_exception
129
+ end
130
+
131
+ # See eval_with_security.
132
+ def send_with_security(rules, obj, msg, *args, &proc)
133
+ eval_with_security(rules) { obj.send(msg, *args, &proc) }
134
+ end
135
+
136
+ # Evaluates the given block, additionally using the given rules.
137
+ # rules == [ { :action => action, :resource => res_type, :source => binding }, ...]
138
+ # action and res_type should be symbols, binding is optional
139
+ #
140
+ def eval_with_security(rules)
141
+ install_rules(rules)
142
+
143
+ apply_rules_before_action
144
+
145
+ result = yield
146
+
147
+ apply_rules_after_action
148
+
149
+ result
150
+ rescue AnnotationSecurity::SecurityError
151
+ SecurityContext.security_exception = $!
152
+ raise $!
153
+ ensure
154
+ uninstall_rules(rules)
155
+ result
156
+ end
157
+
158
+ def apply_rules_before_action # :nodoc:
159
+ # apply static rules before entering the action
160
+ apply_static_rules
161
+ # bindings may apply to parameters, try to check them too
162
+ apply_param_rules
163
+ apply_var_rules
164
+ end
165
+
166
+ def apply_rules_after_action # :nodoc:
167
+ # check again, bindings may have been changed
168
+ apply_var_rules
169
+ end
170
+
171
+ # Returns true iif the operation defined by +policy_args+ is allowed.
172
+ #
173
+ # The following calls to \#allowed? are allowed:
174
+ #
175
+ # allowed? :show, :resource, @resource
176
+ # # => true if the current user has the right to show @resource,
177
+ # # which belongs to the :resource resource-class
178
+ #
179
+ # In case of model objects or other classes which implement a #resource_type
180
+ # method the the second argument may be ommited
181
+ #
182
+ # allowed? :show, @resource
183
+ # # equivalent to the above call if @resource.resource_type == :resource
184
+ #
185
+ # A policy description used as a controller annotation may also be to check
186
+ # a right
187
+ #
188
+ # allowed? "show resource", @resource
189
+ # # => true if the current user has the right "show resource" for @resource
190
+ #
191
+ # A policy may also be applied without an object representing the context:
192
+ #
193
+ # allowed? :show, :resource
194
+ # # => true if the current may show resources.
195
+ #
196
+ # This will only check system and pretest rules. The result +true+ does not
197
+ # mean that the user may show all resources. However, a +false+ indicates
198
+ # that the user is not allowed to show any resources.
199
+ #
200
+ # If the resource type is omitted as well, only rules defined for all
201
+ # resources can be tested. See RelationLoader#all_resources for details.
202
+ #
203
+ # allowed? :administrate
204
+ # # => true if the user is allowed to administrate all resources.
205
+ #
206
+ def allowed?(*policy_args)
207
+ policy_args = AnnotationSecurity::Utils.parse_policy_arguments(policy_args)
208
+ __allowed?(*policy_args)
209
+ end
210
+
211
+ # Equivalent to allowed?; is? is provided for better readability.
212
+ #
213
+ # SecurityContext.allowed? :logged_in
214
+ # vs
215
+ # SecurityContext.is? :logged_in
216
+ #
217
+ alias is? allowed?
218
+
219
+ # Raises a SecurityViolationError if the rule defined by +policy_args+ is not
220
+ # allowed. See allowed? for details.
221
+ #
222
+ def apply_rule(*args)
223
+ self.class.raise_access_denied(*args) unless allowed?(*args)
224
+ end
225
+
226
+ # Checks the rules of an other action. Note that rules that are bound to a
227
+ # variable can not be checked.
228
+ #
229
+ # ==== Parameters
230
+ # * +controller+ Symbol representing the controller, like :resource
231
+ # * +action+ The called action, like :update
232
+ # * +objects+ (optional) List of objects that will be relevant for that action.
233
+ # * +params+ (optional) Hash of the passed parameters, like :id => 1.
234
+ #
235
+ # ==== Examples
236
+ #
237
+ # Checks static and pretest rules.
238
+ # allow_action? :resource, :create
239
+ # # => true if the current user may execute ResourcesController#create
240
+ #
241
+ # Checks static, pretest and context rules
242
+ # allow_action? :resource, :edit, [@resource]
243
+ # # => true if the current user may execute ResourcesController#edit,
244
+ # # assuming that @resource will be used in that action
245
+ #
246
+ # Checks static, pretest and context rules and all rules that are bound
247
+ # to :id.
248
+ # allow_action? :resource, :edit, [@resource], {:id => 4}
249
+ # # => true if the current user may execute ResourcesController#edit,
250
+ # # assuming that @resource will be used in that action
251
+ #
252
+ def allow_action?(*args) # :nodoc:
253
+
254
+ controller, action, objects, params =
255
+ AnnotationSecurity::Utils.parse_action_args(args)
256
+
257
+ # var rules are ignored here
258
+ context_rules, param_rules, _ = get_rule_set(controller, action)
259
+
260
+ # check static rules
261
+ evaluate_statically(context_rules)
262
+
263
+ # check context rules for all objects
264
+ objects.each do |o|
265
+ res_type = o.resource_type
266
+ evaluate_context_rules(context_rules, res_type, o)
267
+ end
268
+
269
+ evaluate_bound_rules_for_params(param_rules, params)
270
+
271
+ true
272
+ rescue SecurityViolationError
273
+ return false
274
+ end
275
+
276
+ # Applies all system and pretest rules of the current action.
277
+ # Raises a SecurityViolationError if a rule is violated.
278
+ #
279
+ def apply_static_rules # :nodoc:
280
+ evaluate_statically(@context_rules)
281
+ end
282
+
283
+ def apply_param_rules # :nodoc:
284
+ evaluate_bound_rules(@param_rules, @param_valid_objects)
285
+ end
286
+
287
+ def apply_var_rules # :nodoc:
288
+ evaluate_bound_rules(@var_rules, @var_valid_objects)
289
+ end
290
+
291
+ # Applies all rules of the current action to the resource defined by
292
+ # +resource_args+. Raises a SecurityViolationError if a rule is
293
+ # violated.
294
+ #
295
+ def apply_context_rules(*res_args) # :nodoc:
296
+ restype, res = AnnotationSecurity::Utils.parse_resource_arguments(res_args)
297
+ evaluate_context_rules(@context_rules, restype, res)
298
+ end
299
+
300
+ alias apply_rules apply_context_rules # :nodoc:
301
+
302
+ # Call if a resource object was touched during an action. Will be called
303
+ # automatically for model objects.
304
+ #
305
+ # Applies all rules that are currently active to the resource defined by
306
+ # +resource_args+. Raises a SecurityViolationError if a rule is
307
+ # violated.
308
+ #
309
+ # ==== Usage
310
+ # observe :resource, @resource
311
+ # where <tt>:resource</tt> is the resource type @resource belongs to, or
312
+ # observe @resource
313
+ # which is equivalent if <tt>@resource.resource_name == :resource</tt>
314
+ #
315
+ def observe(*resource_args)
316
+ apply_context_rules(*resource_args)
317
+ end
318
+
319
+ # Raise a SecurityViolationError.
320
+ # See allowed? for details on +policy_args+.
321
+ #
322
+ def self.raise_access_denied(*policy_args)
323
+ log_access_denied(policy_args)
324
+ raise SecurityViolationError.access_denied(credential,*policy_args)
325
+ end
326
+
327
+ # Activates access logging for the current request.
328
+ #
329
+ def log!(&proc)
330
+ @enable_logging = true
331
+ @log = proc || Proc.new do |result, action, res_type, resource|
332
+ result = result ? 'ALLOWED' : 'REFUSED' unless result.is_a? String
333
+ msg = "%-8s %-10s %-16s %s" % [result, action, res_type, resource]
334
+ puts msg
335
+ end
336
+ end
337
+
338
+ def log_access_denied(policy_args) # :nodoc:
339
+ @log.call('DENIED!', *policy_args) if @enable_logging
340
+ end
341
+
342
+ protected
343
+
344
+ def log_access_check(*policy_args)
345
+ @log.call(*policy_args) if @enable_logging
346
+ end
347
+
348
+ private
349
+
350
+ # data =========================================================================
351
+
352
+ # { binding => { :res_type1 => [:action1, ...], ... }, ... }
353
+ def new_bound_rules_hash() # :nodoc:
354
+ Hash.new { |h,k| h[k] = new_rules_hash }
355
+ end
356
+
357
+ # { :res_type1 => [:action1, ...], ... }
358
+ def new_rules_hash() # :nodoc:
359
+ Hash.new { |h,k| h[k] = [] }
360
+ end
361
+
362
+ # { binding => [object1, ...], ...}
363
+ def new_bound_valid_objects_hash() # :nodoc:
364
+ Hash.new { |h,k| h[k] = [] }
365
+ end
366
+
367
+ # { :res_type1 => { :action1 => [object1, ...], ...}, ...}
368
+ def new_valid_objects_hash() # :nodoc:
369
+ Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
370
+ end
371
+
372
+ # {:res_type1 => policy1, ...}
373
+ def new_policy_hash() # :nodoc:
374
+ Hash.new { |h,k| h[k] = new_policy(k) }
375
+ end
376
+
377
+ def new_policy(resource_type) # :nodoc:
378
+ AnnotationSecurity::PolicyManager.create_policy(resource_type, credential)
379
+ end
380
+
381
+ # Get the policy for a resource type from the cache
382
+ def policy(res_type) # :nodoc:
383
+ @policies[res_type]
384
+ end
385
+
386
+ # rules management =============================================================
387
+
388
+ def install_rules(rules, rule_set=nil, controller=@controller.class)
389
+ rules.each { |rule| install_rule rule, rule_set, controller }
390
+ end
391
+
392
+ def install_rule(rule, rule_set, controller)
393
+ rule_list(rule, rule_set, controller) << rule[:action]
394
+ end
395
+
396
+ def uninstall_rules(rules, rule_set=nil, controller=@controller.class)
397
+ rules.each { |rule| uninstall_rule rule, rule_set, controller }
398
+ end
399
+
400
+ def uninstall_rule(rule, rule_set, controller)
401
+ list = rule_list(rule, rule_set, controller)
402
+ i = list.index(rule[:action])
403
+ list.delete_at(i) if i
404
+ end
405
+
406
+ def rule_list(rule, rule_set, controller)
407
+ rule_set ||= [@context_rules, @param_rules, @var_rules]
408
+ resource = rule[:resource] || controller.default_resource
409
+ source = rule[:source]
410
+ if source.nil?
411
+ list = rule_set.first[resource]
412
+ elsif source.is_a? Symbol
413
+ list = rule_set.second[source][resource]
414
+ else
415
+ list = rule_set.third[source][resource]
416
+ end
417
+ list
418
+ end
419
+
420
+ # returns rule set for other controller actions
421
+ def get_rule_set(controller, action) # :nodoc:
422
+ @rule_sets ||= Hash.new { |h,k| h[k] = {} }
423
+ rule_set = @rule_sets[controller][action]
424
+ unless rule_set
425
+ rule_set = [new_rules_hash, new_bound_rules_hash, new_bound_rules_hash]
426
+ rules = controller.descriptions_of action
427
+ install_rules rules, rule_set, controller
428
+ @rule_sets[controller][action] = rule_set
429
+ end
430
+ rule_set
431
+ end
432
+
433
+ # rule evaluation ==============================================================
434
+
435
+ # Evaluate the rules statically, skips all rules that are static only.
436
+ # * +rules+ a Hash like {:resource_type => [:right1, :right2]}
437
+ def evaluate_statically(rules) # :nodoc:
438
+ # rules == { :resource1 => [:right1, ...], ... }
439
+ rules.each_pair do |resource_type,rights|
440
+ policy(resource_type).evaluate_statically(rights)
441
+ end
442
+ end
443
+
444
+ # Checks bound rules. Evaluates the bindings, on success adds objects
445
+ # to valid objects.
446
+ # rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
447
+ # valid_objects == { binding1 => [object1, ...], ... }
448
+ def evaluate_bound_rules(rules, valid_objects) # :nodoc:
449
+ evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
450
+ @controller.values_of_source(binding)
451
+ end
452
+ end
453
+
454
+ # Checks bound rules using the values from params
455
+ # rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
456
+ # params == { binding1 => object1, ... }
457
+ def evaluate_bound_rules_for_params(rules, params) # :nodoc:
458
+ valid_objects = new_bound_valid_objects_hash
459
+ evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
460
+ values = params[binding]
461
+ values.is_a?(Array) ? values : [values]
462
+ end
463
+ end
464
+
465
+ def evaluate_bound_rules_with_binding(rules, valid_objects, &proc) # :nodoc:
466
+ rules.each_key do |binding|
467
+ value_ids = proc.call(binding)
468
+ rules[binding].each_key do |res_type|
469
+ values = value_ids.collect do |id|
470
+ AnnotationSecurity::ResourceManager.get_resource res_type, id
471
+ end
472
+ values.compact!
473
+ values_of_res_type = values - valid_objects[binding]
474
+ values_of_res_type.each do |resource|
475
+ evaluate_rules(rules[binding][res_type],
476
+ res_type,
477
+ resource)
478
+ valid_objects[binding] << resource
479
+ end
480
+ end
481
+ end
482
+ end
483
+
484
+ # Checks context rules for given resource
485
+ # rules == { :res_type1 => [:action1, ...], ... }
486
+ def evaluate_context_rules(rules, res_type, res) # :nodoc:
487
+ evaluate_rules(rules[res_type], res_type, res)
488
+ end
489
+
490
+ # Checks if actions on resource are allowed. If true, adds to valid objects.
491
+ # Returns true
492
+ # actions == [:action1, ...]
493
+ # valid_objects == { :action1 => [object1, ...], ... }
494
+ def evaluate_rules(actions, res_type, resource) # :nodoc:
495
+ valid_objects = @valid_objects[res_type]
496
+ actions.each do |action|
497
+ unless valid_objects[action].index(resource)
498
+ __apply_rule(action, res_type, resource)
499
+ valid_objects[action] << resource
500
+ end
501
+ end
502
+ true
503
+ end
504
+
505
+ # Usage:
506
+ # __allowed? :show, :assignment, an_assignment
507
+ def __allowed?(action, res_type, resource=nil) # :nodoc:
508
+
509
+ block = lambda do
510
+ if resource
511
+ policy(res_type).allowed?(action, resource)
512
+ else
513
+ policy(res_type).static_policy.allowed?(action, nil)
514
+ end
515
+ end
516
+
517
+ block.call.tap do |r|
518
+ log_access_check r, action, res_type, resource
519
+ end
520
+ end
521
+
522
+ # Raises a SecurityViolationError if the rule defined by +policy_args+ is not
523
+ # allowed. See __allowed? for details.
524
+ #
525
+ def __apply_rule(*args) # :nodoc:
526
+ self.class.raise_access_denied(*args) unless __allowed?(*args)
527
+ end
528
+
529
+ #=============================================================================
530
+ # Singleton
531
+
532
+ def self.security_methods
533
+ instance_methods(false).delete_if { |m| [:enabled?].member? m.to_sym }
534
+ end
535
+
536
+ #=============================================================================
537
+ # Without security block implementation
538
+
539
+ class SecurityContextDummy
540
+ attr_accessor :credential
541
+
542
+ def initialize(credential)
543
+ self.credential = credential
544
+ end
545
+
546
+ def method_missing(symbol, *args)
547
+ # puts "#{self.class}##{symbol}(#{args})"
548
+ end
549
+
550
+ def enabled?
551
+ false
552
+ end
553
+ end
554
+
555
+ public
556
+
557
+ def enabled?
558
+ true
559
+ end
560
+
561
+ # Runs a given block with security disabled. Inside the block, the context
562
+ # will be disabled for the current thread.
563
+ #
564
+ def self.without_security!(&block)
565
+ old_current = current
566
+
567
+ credential = old_current.credential if old_current
568
+ load SecurityContextDummy.new(credential)
569
+ return_value = yield
570
+ load old_current
571
+ return_value
572
+ end
573
+
574
+ # create singleton methods
575
+ security_methods.each do |method|
576
+ if method.to_s.end_with? '='
577
+ # setters need a different handling
578
+ class_eval %{
579
+ def self.#{method}(value)
580
+ current.#{method}(value) if current
581
+ end }
582
+ else
583
+ class_eval %{
584
+ def self.#{method}(*args,&proc)
585
+ current.#{method}(*args,&proc) if current
586
+ end }
587
+ end
588
+ end
589
+ end