ghart-declarative_authorization 0.3.2.4

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 (42) hide show
  1. data/CHANGELOG +83 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +510 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +259 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +187 -0
  8. data/app/views/authorization_rules/_change.erb +58 -0
  9. data/app/views/authorization_rules/_show_graph.erb +37 -0
  10. data/app/views/authorization_rules/_suggestions.erb +48 -0
  11. data/app/views/authorization_rules/change.html.erb +152 -0
  12. data/app/views/authorization_rules/graph.dot.erb +68 -0
  13. data/app/views/authorization_rules/graph.html.erb +40 -0
  14. data/app/views/authorization_rules/index.html.erb +17 -0
  15. data/app/views/authorization_usages/index.html.erb +36 -0
  16. data/authorization_rules.dist.rb +20 -0
  17. data/config/routes.rb +7 -0
  18. data/garlic_example.rb +20 -0
  19. data/init.rb +5 -0
  20. data/lib/declarative_authorization.rb +15 -0
  21. data/lib/declarative_authorization/authorization.rb +634 -0
  22. data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
  23. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  24. data/lib/declarative_authorization/development_support/change_supporter.rb +620 -0
  25. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  26. data/lib/declarative_authorization/helper.rb +60 -0
  27. data/lib/declarative_authorization/in_controller.rb +597 -0
  28. data/lib/declarative_authorization/in_model.rb +159 -0
  29. data/lib/declarative_authorization/maintenance.rb +182 -0
  30. data/lib/declarative_authorization/obligation_scope.rb +308 -0
  31. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  32. data/lib/declarative_authorization/reader.rb +441 -0
  33. data/test/authorization_test.rb +827 -0
  34. data/test/controller_filter_resource_access_test.rb +394 -0
  35. data/test/controller_test.rb +386 -0
  36. data/test/dsl_reader_test.rb +157 -0
  37. data/test/helper_test.rb +171 -0
  38. data/test/maintenance_test.rb +46 -0
  39. data/test/model_test.rb +1308 -0
  40. data/test/schema.sql +54 -0
  41. data/test/test_helper.rb +118 -0
  42. metadata +106 -0
@@ -0,0 +1,620 @@
1
+ require File.join(File.dirname(__FILE__), %w{development_support})
2
+
3
+ module Authorization
4
+
5
+ module DevelopmentSupport
6
+ # Ideas for improvement
7
+ # * Algorithm
8
+ # * Objective function:
9
+ # * affected user count,
10
+ # * as specific as possible (roles, privileges)
11
+ # * as little changes as necessary
12
+ # * Modify role, privilege hierarchy
13
+ # * Merge, split roles
14
+ # * Add privilege to existing rules
15
+ # * Features
16
+ # * Improve review facts: impact, affected users count
17
+ # * group similar candidates: only show abstract methods?
18
+ # * restructure GUI layout: more room for analyzing suggestions
19
+ # * changelog, previous tests, etc.
20
+ # * multiple permissions in tests
21
+ # * Evaluation of approaches with Analyzer algorithms
22
+ # * Authorization constraints
23
+ #
24
+ # Algorithm
25
+ # * for each candidate
26
+ # * abstract actions: solving first failing test (remove privilege from role)
27
+ # * for each abstract action
28
+ # * specific actions: concrete steps (remove privilege from specific role)
29
+ # * for each specific action
30
+ # * next if reversal action of previous step
31
+ # * apply specific action on candidate
32
+ # * save as solution if no failing tests on changed_candidate
33
+ # * else: queue as candidate
34
+ # * equivalent states
35
+ #
36
+ # NOTE:
37
+ # * user.clone needs to clone role_symbols
38
+ # * user.role_symbols needs to respond to <<
39
+ # * user.login is needed
40
+ #
41
+ class ChangeSupporter < AbstractAnalyzer
42
+
43
+ # Returns a list of possible approaches for changes to the current
44
+ # authorization rules that achieve a given goal. The goal is given as
45
+ # permission tests in the block. The instance method +users+ is available
46
+ # when the block is executed to refer to the then-current users, whose
47
+ # roles might have changed as one suggestion.
48
+ def find_approaches_for (options, &tests)
49
+ @prohibited_actions = (options[:prohibited_actions] || []).to_set
50
+
51
+ @approaches_by_actions = {}
52
+
53
+ candidates = []
54
+ suggestions = []
55
+ approach_checker = ApproachChecker.new(self, tests)
56
+
57
+ starting_candidate = Approach.new(@engine, options[:users], [])
58
+ if starting_candidate.check(approach_checker)
59
+ suggestions << starting_candidate
60
+ else
61
+ candidates << starting_candidate
62
+ end
63
+
64
+ checked_candidates = 0
65
+ while !candidates.empty? and checked_candidates < 200
66
+ checked_candidates += next_step(suggestions, candidates, approach_checker)
67
+ end
68
+
69
+ # remove subsets
70
+ suggestions.sort!
71
+ end
72
+
73
+ # Returns an array of GroupedApproaches for the given array of approaches.
74
+ # Only groups directly adjacent approaches
75
+ def group_approaches (approaches)
76
+ approaches.each_with_object([]) do |approach, grouped|
77
+ if grouped.last and grouped.last.approach.similar_to(approach)
78
+ grouped.last.similar_approaches << approach
79
+ else
80
+ grouped << GroupedApproach.new(approach)
81
+ end
82
+ end
83
+ end
84
+
85
+ class GroupedApproach
86
+ attr_accessor :approach, :similar_approaches
87
+ def initialize (approach)
88
+ @approach = approach
89
+ @similar_approaches = []
90
+ end
91
+ end
92
+
93
+ class ApproachChecker
94
+ attr_reader :users, :failed_tests
95
+
96
+ def initialize (analyzer, tests)
97
+ @analyzer, @tests = analyzer, tests
98
+ end
99
+
100
+ def check (engine, users)
101
+ @current_engine = engine
102
+ @failed_tests = []
103
+ @current_test_args = nil
104
+ @current_permit_result = nil
105
+ @users = users
106
+ @ok = true
107
+ instance_eval(&@tests)
108
+ @ok
109
+ end
110
+
111
+ def assert (ok)
112
+ @failed_tests << Test.new(*([!@current_permit_result] + @current_test_args)) unless ok
113
+ @ok &&= ok
114
+ end
115
+
116
+ def permit? (*args)
117
+ @current_test_args = args
118
+ @current_permit_result = @current_engine.permit?(
119
+ *(args[0...-1] + [args.last.merge(:skip_attribute_test => true)]))
120
+ end
121
+ end
122
+
123
+ class Test
124
+ attr_reader :positive, :privilege, :context, :user
125
+ def initialize (positive, privilege, options = {})
126
+ @positive, @privilege = positive, privilege
127
+ @context = options[:context]
128
+ @user = options[:user]
129
+ end
130
+ end
131
+
132
+ class Approach
133
+ attr_reader :steps, :engine, :users, :failed_tests
134
+ def initialize (engine, users, steps)
135
+ @engine, @users, @steps = engine, users, steps
136
+ end
137
+
138
+ def check (approach_checker)
139
+ res = approach_checker.check(@engine, @users)
140
+ @failed_tests = approach_checker.failed_tests
141
+ #puts "CHECKING #{inspect} (#{res}, #{sort_value})"
142
+ res
143
+ end
144
+
145
+ def affected_users (original_engine, original_users, privilege, context)
146
+ (0...@users.length).select do |i|
147
+ original_engine.permit?(privilege, :context => context,
148
+ :skip_attribute_test => true, :user => original_users[i]) !=
149
+ @engine.permit?(privilege, :context => context,
150
+ :skip_attribute_test => true, :user => @users[i])
151
+ end.collect {|i| original_users[i]}
152
+ end
153
+
154
+ def initialize_copy (other)
155
+ @engine = @engine.clone
156
+ @users = @users.clone
157
+ @steps = @steps.clone
158
+ end
159
+
160
+ def changes
161
+ @steps
162
+ end
163
+
164
+ def abstract_actions
165
+ if failed_tests.first.positive
166
+ [
167
+ AssignPrivilegeToRoleAction,
168
+ AssignRoleToUserAction,
169
+ CreateAndAssignRoleToUserAction,
170
+ AddPrivilegeAndAssignRoleToUserAction
171
+ ]
172
+ else
173
+ [
174
+ RemovePrivilegeFromRoleAction,
175
+ RemoveRoleFromUserAction
176
+ ]
177
+ end
178
+ end
179
+
180
+ def reverse_of_previous? (specific_action)
181
+ changes.any? {|step| step.reverse?(specific_action)}
182
+ end
183
+
184
+ def apply (action)
185
+ ok = action.apply(self)
186
+ @steps << action if ok
187
+ ok
188
+ end
189
+
190
+ def subset? (other_approach)
191
+ other_approach.changes.length >= changes.length &&
192
+ changes.all? {|step| other_approach.changes.any? {|step_2| step_2.eql?(step)} }
193
+ end
194
+
195
+ def state_hash
196
+ @state_hash ||= @engine.auth_rules.inject(0) do |memo, rule|
197
+ memo + rule.privileges.hash + rule.contexts.hash +
198
+ rule.attributes.hash + rule.role.hash
199
+ end +
200
+ @users.inject(0) {|memo, user| memo + user.role_symbols.hash } +
201
+ @engine.privileges.hash + @engine.privilege_hierarchy.hash +
202
+ @engine.roles.hash + @engine.role_hierarchy.hash
203
+ end
204
+
205
+ def sort_value
206
+ weight + @failed_tests.length
207
+ end
208
+
209
+ def weight
210
+ changes.sum(&:weight)
211
+ end
212
+
213
+ def similar_to (other)
214
+ other.weight == weight and
215
+ other.changes.map {|change| change.class.name}.sort ==
216
+ changes.map {|change| change.class.name}.sort
217
+ end
218
+
219
+ def inspect
220
+ "Approach: Steps: #{changes.map(&:inspect) * ', '}"# +
221
+ # "\n Roles: #{AnalyzerEngine.roles(@engine).map(&:to_sym).inspect}; " +
222
+ # "\n Users: #{@users.map(&:role_symbols).inspect}"
223
+ end
224
+
225
+ def <=> (other)
226
+ sort_value <=> other.sort_value
227
+ end
228
+ end
229
+
230
+ class AbstractAction
231
+ def weight
232
+ 1
233
+ end
234
+
235
+ # returns a list of instances of the action that may be applied
236
+ def self.specific_actions (candidate)
237
+ raise NotImplementedError, "Not yet?"
238
+ end
239
+
240
+ # applies the specific action on the given candidate
241
+ def apply (candidate)
242
+ raise NotImplementedError, "Not yet?"
243
+ end
244
+
245
+ def eql? (other)
246
+ other.class == self.class and hash == other.hash
247
+ end
248
+
249
+ def hash
250
+ @hash ||= to_a.hash
251
+ end
252
+
253
+ def reverse? (other)
254
+ false
255
+ end
256
+
257
+ def inspect
258
+ "#{self.class.name.demodulize} #{hash} #{to_a.hash} (#{to_a[1..-1].collect {|info| self.class.readable_info(info)} * ','})"
259
+ end
260
+
261
+ def to_a
262
+ [:abstract]
263
+ end
264
+
265
+ def resembles? (spec)
266
+ min_length = [spec.length, to_a.length].min
267
+ to_a[0,min_length] == spec[0,min_length]
268
+ end
269
+
270
+ def resembles_any? (specs)
271
+ specs.any? {|spec| resembles?(spec) }
272
+ end
273
+
274
+ def self.readable_info (info)
275
+ if info.respond_to?(:to_sym)
276
+ info.to_sym.inspect
277
+ else
278
+ info.inspect
279
+ end
280
+ end
281
+ end
282
+
283
+ class AbstractCompoundAction < AbstractAction
284
+ def weight
285
+ @actions.sum(&:weight) + 1
286
+ end
287
+
288
+ def apply (candidate)
289
+ @actions.all? {|action| action.apply(candidate)}
290
+ end
291
+
292
+ def reverse? (other)
293
+ @actions.any? {|action| action.reverse?(other)}
294
+ end
295
+
296
+ def to_a
297
+ @actions.inject([]) {|memo, action| memo += action.to_a.first.is_a?(Enumerable) ? action.to_a : [action.to_a]; memo }
298
+ end
299
+
300
+ def hash
301
+ @hash ||= @actions.inject(0) {|memo, action| memo += action.hash }
302
+ end
303
+
304
+ def resembles? (spec)
305
+ @actions.any? {|action| action.resembles?(spec) } or
306
+ to_a.any? do |array|
307
+ min_length = [spec.length, array.length].min
308
+ array[0,min_length] == spec[0,min_length]
309
+ end
310
+ end
311
+ end
312
+
313
+ class AssignPrivilegeToRoleAction < AbstractAction
314
+ def self.specific_actions (candidate)
315
+ privilege = AnalyzerEngine::Privilege.for_sym(
316
+ candidate.failed_tests.first.privilege, candidate.engine)
317
+ context = candidate.failed_tests.first.context
318
+ user = candidate.failed_tests.first.user
319
+ ([privilege] + privilege.ancestors).collect do |ancestor_privilege|
320
+ user.role_symbols.collect {|role_sym| AnalyzerEngine::Role.for_sym(role_sym, candidate.engine) }.
321
+ collect {|role| [role] + role.ancestors}.flatten.uniq.collect do |role|
322
+ # apply checks later if privilege is already present in that role
323
+ new(ancestor_privilege.to_sym, context, role.to_sym)
324
+ end
325
+ end.flatten
326
+ end
327
+
328
+ attr_reader :privilege, :context, :role
329
+ def initialize (privilege_sym, context, role_sym)
330
+ @privilege, @context, @role = privilege_sym, context, role_sym
331
+ end
332
+
333
+ def apply (candidate)
334
+ AnalyzerEngine.apply_change(candidate.engine, to_a)
335
+ end
336
+
337
+ def reverse? (other)
338
+ other.is_a?(RemovePrivilegeFromRoleAction) and
339
+ other.privilege == @privilege and
340
+ other.context == @context and
341
+ other.role == @role
342
+ end
343
+
344
+ def to_a
345
+ [:add_privilege, @privilege, @context, @role]
346
+ end
347
+ end
348
+
349
+ class AssignRoleToUserAction < AbstractAction
350
+ def self.specific_actions (candidate)
351
+ privilege = candidate.failed_tests.first.privilege
352
+ context = candidate.failed_tests.first.context
353
+ user = candidate.failed_tests.first.user
354
+ AnalyzerEngine::Role.all_for_privilege(privilege, context, candidate.engine).collect do |role|
355
+ new(user, role.to_sym)
356
+ end
357
+ end
358
+
359
+ attr_reader :user, :role
360
+ def initialize (user, role_sym)
361
+ @user, @role = user, role_sym
362
+ end
363
+
364
+ def apply (candidate)
365
+ if candidate.engine.roles_with_hierarchy_for(@user).include?(@role)
366
+ false
367
+ else
368
+ # beware of shallow copies!
369
+ cloned_user = @user.clone
370
+ user_index = candidate.users.index(@user)
371
+ raise "Cannot find #{@user.inspect} in users array" unless user_index
372
+ candidate.users[user_index] = cloned_user
373
+ # possible on real user objects?
374
+ cloned_user.role_symbols << @role
375
+ raise "User#role_symbols immutable or user only shallowly cloned!" if cloned_user.role_symbols == @user.role_symbols
376
+ true
377
+ end
378
+ end
379
+
380
+ def hash
381
+ to_a[0,2].hash + @user.login.hash
382
+ end
383
+
384
+ def reverse? (other)
385
+ other.is_a?(RemoveRoleFromUserAction) and
386
+ other.user.login == @user.login and
387
+ other.role == @role
388
+ end
389
+
390
+ def resembles? (spec)
391
+ super(spec[0,2]) and (spec.length == 2 or spec[2] == @user.login)
392
+ end
393
+
394
+ def to_a
395
+ [:assign_role_to_user, @role, @user]
396
+ end
397
+ end
398
+
399
+ class CreateAndAssignRoleToUserAction < AbstractCompoundAction
400
+ def self.specific_actions (candidate)
401
+ privilege = AnalyzerEngine::Privilege.for_sym(
402
+ candidate.failed_tests.first.privilege, candidate.engine)
403
+ context = candidate.failed_tests.first.context
404
+ user = candidate.failed_tests.first.user
405
+ role = AnalyzerEngine::Role.for_sym(:change_supporter_new_role, candidate.engine)
406
+ ([privilege] + privilege.ancestors).collect do |ancestor_privilege|
407
+ new(user, ancestor_privilege.to_sym, context, role.to_sym)
408
+ end
409
+ end
410
+
411
+ attr_reader :user, :privilege, :context, :role
412
+ def initialize (user, privilege_sym, context_sym, role_sym)
413
+ @user, @privilege, @context, @role = user, privilege_sym, context_sym, role_sym
414
+ @actions = [AddPrivilegeAndAssignRoleToUserAction.new(@user, @privilege, @context, role_sym)]
415
+ end
416
+
417
+ def apply (candidate)
418
+ if AnalyzerEngine.apply_change(candidate.engine, [:add_role, @role])
419
+ super(candidate)
420
+ else
421
+ false
422
+ end
423
+ end
424
+
425
+ def hash
426
+ to_a[0].hash + super
427
+ end
428
+
429
+ def to_a
430
+ [[:add_role, @role]] + super
431
+ end
432
+ end
433
+
434
+ class AddPrivilegeAndAssignRoleToUserAction < AbstractCompoundAction
435
+ def self.specific_actions (candidate)
436
+ privilege = AnalyzerEngine::Privilege.for_sym(
437
+ candidate.failed_tests.first.privilege, candidate.engine)
438
+ context = candidate.failed_tests.first.context
439
+ user = candidate.failed_tests.first.user
440
+ ([privilege] + privilege.ancestors).collect do |ancestor_privilege|
441
+ AnalyzerEngine::Role.all(candidate.engine).collect do |role|
442
+ new(user, ancestor_privilege.to_sym, context, role.to_sym)
443
+ end
444
+ end.flatten
445
+ end
446
+
447
+ attr_reader :user, :privilege, :context, :role
448
+ def initialize (user, privilege_sym, context, role_sym)
449
+ @user, @privilege, @context, @role = user, privilege_sym, context, role_sym
450
+ @actions = [
451
+ AssignRoleToUserAction.new(@user, @role),
452
+ AssignPrivilegeToRoleAction.new(@privilege, @context, @role)
453
+ ]
454
+ end
455
+ end
456
+
457
+ class RemovePrivilegeFromRoleAction < AbstractAction
458
+ def self.specific_actions (candidate)
459
+ privilege = AnalyzerEngine::Privilege.for_sym(
460
+ candidate.failed_tests.first.privilege, candidate.engine)
461
+ context = candidate.failed_tests.first.context
462
+ user = candidate.failed_tests.first.user
463
+ ([privilege] + privilege.ancestors).collect do |ancestor_privilege|
464
+ user.role_symbols.collect {|role_sym| AnalyzerEngine::Role.for_sym(role_sym, candidate.engine) }.
465
+ collect {|role| [role] + role.ancestors}.flatten.uniq.collect do |role|
466
+ new(ancestor_privilege.to_sym, context, role.to_sym)
467
+ end
468
+ end.flatten
469
+ end
470
+
471
+ attr_reader :privilege, :context, :role
472
+ def initialize (privilege_sym, context, role_sym)
473
+ @privilege, @context, @role = privilege_sym, context, role_sym
474
+ end
475
+
476
+ def apply (candidate)
477
+ AnalyzerEngine.apply_change(candidate.engine, to_a)
478
+ end
479
+
480
+ def reverse? (other)
481
+ (other.is_a?(AssignPrivilegeToRoleAction) or
482
+ other.is_a?(AbstractCompoundAction)) and
483
+ other.reverse?(self)
484
+ end
485
+
486
+ def to_a
487
+ [:remove_privilege, @privilege, @context, @role]
488
+ end
489
+ end
490
+
491
+ class RemoveRoleFromUserAction < AbstractAction
492
+ def self.specific_actions (candidate)
493
+ privilege = candidate.failed_tests.first.privilege
494
+ context = candidate.failed_tests.first.context
495
+ user = candidate.failed_tests.first.user
496
+ roles_for_privilege = AnalyzerEngine::Role.all_for_privilege(privilege, context, candidate.engine).map(&:to_sym)
497
+ user.role_symbols.collect {|role_sym| AnalyzerEngine::Role.for_sym(role_sym, candidate.engine)}.
498
+ select {|role| roles_for_privilege.include?(role.to_sym)}.
499
+ collect do |role|
500
+ new(user, role.to_sym)
501
+ end
502
+ end
503
+
504
+ attr_reader :user, :role
505
+ def initialize (user, role_sym)
506
+ @user, @role = user, role_sym
507
+ end
508
+
509
+ def apply (candidate)
510
+ # beware of shallow copies!
511
+ cloned_user = @user.clone
512
+ user_index = candidate.users.index(@user)
513
+ raise "Cannot find #{@user.inspect} in users array" unless user_index
514
+ candidate.users[user_index] = cloned_user
515
+ cloned_user.role_symbols.delete(@role)
516
+ raise "User#role_symbols immutable or user only shallowly cloned!" if cloned_user.role_symbols == @user.role_symbols
517
+ true
518
+ end
519
+
520
+ def hash
521
+ to_a[0,2].hash + @user.login.hash
522
+ end
523
+
524
+ def reverse? (other)
525
+ (other.is_a?(AssignRoleToUserAction) or
526
+ other.is_a?(AbstractCompoundAction)) and
527
+ other.reverse?(self)
528
+ end
529
+
530
+ def resembles? (spec)
531
+ super(spec[0,2]) and (spec.length == 2 or spec[2] == @user.login)
532
+ end
533
+
534
+ def to_a
535
+ [:remove_role_from_user, @role, @user]
536
+ end
537
+ end
538
+
539
+ protected
540
+ def next_step (viable_approaches, candidates, approach_checker)
541
+ candidate = candidates.shift
542
+
543
+ child_candidates = generate_child_candidates(candidate)
544
+ check_child_candidates!(approach_checker, viable_approaches, candidates, child_candidates)
545
+
546
+ candidates.sort!
547
+ child_candidates.length
548
+ end
549
+
550
+ def generate_child_candidates (candidate)
551
+ child_candidates = []
552
+ abstract_actions = candidate.abstract_actions
553
+ abstract_actions.each do |abstract_action|
554
+ abstract_action.specific_actions(candidate).each do |specific_action|
555
+ child_candidate = candidate.dup
556
+ if !specific_action.resembles_any?(@prohibited_actions) and
557
+ !child_candidate.reverse_of_previous?(specific_action) and
558
+ child_candidate.apply(specific_action)
559
+ child_candidates << child_candidate
560
+ end
561
+ end
562
+ end
563
+ child_candidates
564
+ end
565
+
566
+ def check_child_candidates! (approach_checker, viable_approaches, candidates, child_candidates)
567
+ child_candidates.each do |child_candidate|
568
+ if child_candidate.check(approach_checker)
569
+ unless superset_of_existing?(child_candidate)
570
+ remove_supersets!(viable_approaches, child_candidate)
571
+ viable_approaches << child_candidate
572
+ add_to_approaches_by_action!(child_candidate)
573
+ end
574
+ else
575
+ candidates << child_candidate
576
+ end
577
+ child_candidate.freeze
578
+ end
579
+ end
580
+
581
+ def superset_of_existing? (candidate)
582
+ candidate.changes.any? do |action|
583
+ (@approaches_by_actions[action] ||= []).any? {|approach| approach.subset?(candidate)}
584
+ end
585
+ end
586
+
587
+ def remove_supersets! (existing, candidate)
588
+ candidate.changes.inject([]) do |memo, action|
589
+ memo += (@approaches_by_actions[action] ||= []).select do |approach|
590
+ candidate.subset?(approach)
591
+ end
592
+ end.uniq.each do |approach|
593
+ existing.delete(approach)
594
+ remove_from_approaches_by_action!(approach)
595
+ end
596
+ end
597
+
598
+ def add_to_approaches_by_action! (candidate)
599
+ candidate.changes.each do |action|
600
+ (@approaches_by_actions[action] ||= []) << candidate
601
+ end
602
+ end
603
+
604
+ def remove_from_approaches_by_action! (candidate)
605
+ candidate.changes.each do |action|
606
+ (@approaches_by_actions[action] ||= []).delete(candidate)
607
+ end
608
+ end
609
+
610
+ def relevant_roles (approach)
611
+ self.class.relevant_roles(approach)
612
+ end
613
+ def self.relevant_roles (approach)
614
+ (AnalyzerEngine.relevant_roles(approach.engine, approach.users) +
615
+ (approach.engine.roles.include?(:new_role_for_change_analyzer) ?
616
+ [AnalyzerEngine::Role.for_sym(:new_role_for_change_analyzer, approach.engine)] : [])).uniq
617
+ end
618
+ end
619
+ end
620
+ end