annotation_security 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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