declarative_authorization 0.3.2.3

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