stffn-declarative_authorization 0.3.0 → 0.3.1

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