uhees-declarative_authorization 0.3.1

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 +77 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +490 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +235 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +183 -0
  8. data/app/views/authorization_rules/_change.erb +49 -0
  9. data/app/views/authorization_rules/_show_graph.erb +37 -0
  10. data/app/views/authorization_rules/_suggestion.erb +9 -0
  11. data/app/views/authorization_rules/_suggestions.erb +24 -0
  12. data/app/views/authorization_rules/change.html.erb +124 -0
  13. data/app/views/authorization_rules/graph.dot.erb +68 -0
  14. data/app/views/authorization_rules/graph.html.erb +40 -0
  15. data/app/views/authorization_rules/index.html.erb +17 -0
  16. data/app/views/authorization_usages/index.html.erb +36 -0
  17. data/authorization_rules.dist.rb +20 -0
  18. data/config/routes.rb +7 -0
  19. data/garlic_example.rb +20 -0
  20. data/init.rb +5 -0
  21. data/lib/declarative_authorization.rb +15 -0
  22. data/lib/declarative_authorization/authorization.rb +630 -0
  23. data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
  24. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  25. data/lib/declarative_authorization/development_support/change_supporter.rb +578 -0
  26. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  27. data/lib/declarative_authorization/helper.rb +60 -0
  28. data/lib/declarative_authorization/in_controller.rb +367 -0
  29. data/lib/declarative_authorization/in_model.rb +150 -0
  30. data/lib/declarative_authorization/maintenance.rb +188 -0
  31. data/lib/declarative_authorization/obligation_scope.rb +297 -0
  32. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  33. data/lib/declarative_authorization/reader.rb +438 -0
  34. data/test/authorization_test.rb +823 -0
  35. data/test/controller_test.rb +418 -0
  36. data/test/dsl_reader_test.rb +157 -0
  37. data/test/helper_test.rb +154 -0
  38. data/test/maintenance_test.rb +41 -0
  39. data/test/model_test.rb +1171 -0
  40. data/test/schema.sql +53 -0
  41. data/test/test_helper.rb +103 -0
  42. metadata +104 -0
@@ -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