tarsolya-declarative_authorization 0.4.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 +139 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +503 -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 +218 -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 +169 -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 +681 -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 +623 -0
  28. data/lib/declarative_authorization/in_model.rb +162 -0
  29. data/lib/declarative_authorization/maintenance.rb +198 -0
  30. data/lib/declarative_authorization/obligation_scope.rb +345 -0
  31. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  32. data/lib/declarative_authorization/reader.rb +496 -0
  33. data/test/authorization_test.rb +971 -0
  34. data/test/controller_filter_resource_access_test.rb +511 -0
  35. data/test/controller_test.rb +465 -0
  36. data/test/dsl_reader_test.rb +173 -0
  37. data/test/helper_test.rb +171 -0
  38. data/test/maintenance_test.rb +46 -0
  39. data/test/model_test.rb +1694 -0
  40. data/test/schema.sql +54 -0
  41. data/test/test_helper.rb +137 -0
  42. metadata +118 -0
@@ -0,0 +1,252 @@
1
+ require File.join(File.dirname(__FILE__), %w{development_support})
2
+
3
+ begin
4
+ require "ruby_parser"
5
+ #require "parse_tree"
6
+ #require "parse_tree_extensions"
7
+ require "sexp_processor"
8
+ rescue LoadError
9
+ raise "Authorization::DevelopmentSupport::Analyzer requires ruby_parser gem"
10
+ end
11
+
12
+ module Authorization
13
+
14
+ module DevelopmentSupport
15
+
16
+ # Ideas for improvement
17
+ # * moving rules up in the role hierarchy
18
+ # * merging roles
19
+ # * role hierarchy
20
+ #
21
+ # Mergeable Rules: respect if_permitted_to hash
22
+ #
23
+ class Analyzer < AbstractAnalyzer
24
+ def analyze (rules)
25
+ sexp_array = RubyParser.new.parse(rules)
26
+ #sexp_array = ParseTree.translate(rules)
27
+ @reports = []
28
+ [MergeableRulesProcessor].each do |parser|
29
+ parser.new(self).analyze(sexp_array)
30
+ end
31
+ [
32
+ RoleExplosionAnalyzer, InheritingPrivilegesAnalyzer,
33
+ ProposedPrivilegeHierarchyAnalyzer
34
+ ].each do |parser|
35
+ parser.new(self).analyze
36
+ end
37
+ end
38
+
39
+ def reports
40
+ @reports or raise "No rules analyzed!"
41
+ end
42
+
43
+ class GeneralRulesAnalyzer
44
+ def initialize(analyzer)
45
+ @analyzer = analyzer
46
+ end
47
+
48
+ def analyze
49
+ mark(:policy, nil) if analyze_policy
50
+ roles.select {|role| analyze_role(role) }.
51
+ each { |role| mark(:role, role) }
52
+ rules.select {|rule| analyze_rule(rule) }.
53
+ each { |rule| mark(:rule, rule) }
54
+ privileges.select {|privilege| !!analyze_privilege(privilege) }.
55
+ each { |privilege| mark(:privilege, privilege) }
56
+ end
57
+
58
+ protected
59
+ def roles
60
+ @analyzer.roles
61
+ end
62
+
63
+ def rules
64
+ @analyzer.rules
65
+ end
66
+
67
+ def privileges
68
+ @privileges ||= rules.collect {|rule| rule.privileges.to_a}.flatten.uniq
69
+ end
70
+
71
+ # to be implemented by specific processor
72
+ def analyze_policy; end
73
+ def analyze_role (a_role); end
74
+ def analyze_rule (a_rule); end
75
+ def analyze_privilege (a_privilege); end
76
+ def message (object); end
77
+
78
+ private
79
+ def source_line (object)
80
+ object.source_line if object.respond_to?(:source_line)
81
+ end
82
+
83
+ def source_file (object)
84
+ object.source_file if object.respond_to?(:source_file)
85
+ end
86
+
87
+ def mark (type, object)
88
+ @analyzer.reports << Report.new(report_type,
89
+ source_file(object), source_line(object), message(object))
90
+ end
91
+
92
+ # analyzer class name stripped of last word
93
+ def report_type
94
+ (self.class.name.demodulize.underscore.split('_')[0...-1] * '_').to_sym
95
+ end
96
+ end
97
+
98
+ class RoleExplosionAnalyzer < GeneralRulesAnalyzer
99
+ SMALL_ROLE_RULES_COUNT = 3
100
+ SMALL_ROLES_RATIO = 0.2
101
+
102
+ def analyze_policy
103
+ small_roles.length > 1 and small_roles.length.to_f / roles.length.to_f > SMALL_ROLES_RATIO
104
+ end
105
+
106
+ def message (object)
107
+ "The ratio of small roles is quite high (> %.0f%%). Consider refactoring." % (SMALL_ROLES_RATIO * 100)
108
+ end
109
+
110
+ private
111
+ def small_roles
112
+ roles.select {|role| role.rules.length < SMALL_ROLE_RULES_COUNT }
113
+ end
114
+ end
115
+
116
+ class InheritingPrivilegesAnalyzer < GeneralRulesAnalyzer
117
+ def analyze_rule (rule)
118
+ rule.privileges.any? {|privilege| rule.privileges.intersects?(privilege.ancestors) }
119
+ end
120
+
121
+ def message (object)
122
+ "At least one privilege inherits from another in this rule."
123
+ end
124
+ end
125
+
126
+ class ProposedPrivilegeHierarchyAnalyzer < GeneralRulesAnalyzer
127
+ # TODO respect, consider contexts
128
+ def analyze_privilege (privilege)
129
+ privileges.find do |other_privilege|
130
+ other_privilege != privilege and
131
+ other_privilege.rules.all? {|rule| rule.privileges.include?(privilege)}
132
+ end
133
+ end
134
+
135
+ def message (privilege)
136
+ other_privilege = analyze_privilege(privilege)
137
+ "Privilege #{other_privilege.to_sym} is always used together with #{privilege.to_sym}. " +
138
+ "Consider to include #{other_privilege.to_sym} in #{privilege.to_sym}."
139
+ end
140
+ end
141
+
142
+ class GeneralAuthorizationProcessor < SexpProcessor
143
+ def initialize(analyzer)
144
+ super()
145
+ self.auto_shift_type = true
146
+ self.require_empty = false
147
+ self.strict = false
148
+ @analyzer = analyzer
149
+ end
150
+
151
+ def analyze (sexp_array)
152
+ process(sexp_array)
153
+ analyze_rules
154
+ end
155
+
156
+ def analyze_rules
157
+ # to be implemented by specific processor
158
+ end
159
+
160
+ def process_iter (exp)
161
+ s(:iter, process(exp.shift), process(exp.shift), process(exp.shift))
162
+ end
163
+
164
+ def process_arglist (exp)
165
+ s(exp.collect {|inner_exp| process(inner_exp).shift})
166
+ end
167
+
168
+ def process_hash (exp)
169
+ s(Hash[*exp.collect {|inner_exp| process(inner_exp).shift}])
170
+ end
171
+
172
+ def process_lit (exp)
173
+ s(exp.shift)
174
+ end
175
+ end
176
+
177
+ class MergeableRulesProcessor < GeneralAuthorizationProcessor
178
+ def analyze_rules
179
+ if @has_permission
180
+ #p @has_permission
181
+ permissions_by_context_and_rules = @has_permission.inject({}) do |memo, permission|
182
+ key = [permission[:context], permission[:rules]]
183
+ memo[key] ||= []
184
+ memo[key] << permission
185
+ memo
186
+ end
187
+
188
+ permissions_by_context_and_rules.each do |key, rules|
189
+ if rules.length > 1
190
+ rule_lines = rules.collect {|rule| rule[:line] }
191
+ rules.each do |rule|
192
+ @analyzer.reports << Report.new(:mergeable_rules, "", rule[:line],
193
+ "Similar rules already in line(s) " +
194
+ rule_lines.reject {|l| l == rule[:line] } * ", ")
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ def process_call (exp)
202
+ klass = exp.shift
203
+ name = exp.shift
204
+ case name
205
+ when :role
206
+ analyze_rules
207
+ @has_permission = []
208
+ s(:call, klass, name)
209
+ when :has_permission_on
210
+ arglist_line = exp[0].line
211
+ arglist = process(exp.shift).shift
212
+ context = arglist.shift
213
+ args_hash = arglist.shift
214
+ @has_permission << {
215
+ :context => context,
216
+ :rules => [],
217
+ :privilege => args_hash && args_hash[:to],
218
+ # a hack: call exp line seems to be wrong
219
+ :line => arglist_line
220
+ }
221
+ s(:call, klass, name)
222
+ when :to
223
+ @has_permission.last[:privilege] = process(exp.shift).shift if @has_permission
224
+ s(:call, klass, name)
225
+ when :if_attribute
226
+ rules = process(exp.shift).shift
227
+ rules.unshift :if_attribute
228
+ @has_permission.last[:rules] << rules if @has_permission
229
+ s(:call, klass, name)
230
+ when :if_permitted_to
231
+ rules = process(exp.shift).shift
232
+ rules.unshift :if_permitted_to
233
+ @has_permission.last[:rules] << rules if @has_permission
234
+ s(:call, klass, name)
235
+ else
236
+ s(:call, klass, name, process(exp.shift))
237
+ end
238
+ end
239
+ end
240
+
241
+ class Report
242
+ attr_reader :type, :filename, :line, :message
243
+ def initialize (type, filename, line, msg)
244
+ @type = type
245
+ @filename = filename
246
+ @line = line
247
+ @message = msg
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,253 @@
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
+ # * Plan by tackling each condition separately
9
+ # * e.g. two users have a permission through the same role,
10
+ # one should lose that
11
+ # * Consider privilege hierarchy
12
+ # * Consider merging, splitting roles, role hierarchies
13
+ # * Add privilege to existing rules
14
+ # * Features
15
+ # * Show consequences from changes: which users are affected,
16
+ # show users in graph
17
+ # * restructure GUI layout: more room for analyzing suggestions
18
+ # * AI: planning: ADL-like, actions with preconditions and effects
19
+ # * Removing need of intention
20
+ # * Evaluation of approaches with Analyzer algorithms
21
+ # * Consider constraints
22
+ #
23
+ # NOTE:
24
+ # * user.clone needs to clone role_symbols
25
+ # * user.role_symbols needs to respond to <<
26
+ # * user.login is needed
27
+ #
28
+ class ChangeAnalyzer < AbstractAnalyzer
29
+
30
+ def find_approaches_for (change_action, type, options, &tests)
31
+ raise ArgumentError, "Missing options" if !options[:on] or !options[:to]
32
+
33
+ # * strategy for removing: [remove privilege, add privilege to different role]
34
+ @seen_states = Set.new
35
+ # * heurisic: change of failed tests; small number of policy items
36
+ strategy = case [change_action, type]
37
+ when [:remove, :permission]
38
+ [:remove_role_from_user, :remove_privilege, :add_privilege,
39
+ :add_role, :assign_role_to_user]
40
+ when [:add, :permission]
41
+ [:add_role, :add_privilege, :assign_role_to_user]
42
+ else
43
+ raise ArgumentError, "Unknown change action/type: #{[change_action, type].inspect}"
44
+ end
45
+
46
+ candidates = []
47
+ viable_approaches = []
48
+ approach_checker = ApproachChecker.new(self, tests)
49
+
50
+ starting_candidate = Approach.new(@engine, options[:users], [])
51
+ if starting_candidate.check(approach_checker)
52
+ viable_approaches << starting_candidate
53
+ else
54
+ candidates << starting_candidate
55
+ end
56
+
57
+ step_count = 0
58
+ while !candidates.empty? and step_count < 100
59
+ next_step(viable_approaches, candidates, approach_checker, options[:to],
60
+ options[:on], strategy)
61
+ step_count += 1
62
+ end
63
+
64
+ # remove subsets
65
+
66
+ viable_approaches.sort!
67
+ end
68
+
69
+ class ApproachChecker
70
+ attr_reader :failed_test_count, :users
71
+
72
+ def initialize (analyzer, tests)
73
+ @analyzer, @tests = analyzer, tests
74
+ end
75
+
76
+ def check (engine, users)
77
+ @current_engine = engine
78
+ @failed_test_count = 0
79
+ @users = users
80
+ @ok = true
81
+ instance_eval(&@tests)
82
+ @ok
83
+ end
84
+
85
+ def assert (ok)
86
+ @failed_test_count += 1 unless ok
87
+ @ok &&= ok
88
+ end
89
+
90
+ def permit? (*args)
91
+ @current_engine.permit?(*args)
92
+ end
93
+ end
94
+
95
+ class Approach
96
+ attr_reader :steps, :engine, :users
97
+ def initialize (engine, users, steps)
98
+ @engine, @users, @steps = engine, users, steps
99
+ end
100
+
101
+ def check (approach_checker)
102
+ res = approach_checker.check(@engine, @users)
103
+ @failed_test_count = approach_checker.failed_test_count
104
+ #puts "CHECKING #{inspect} (#{res}, #{sort_value})"
105
+ res
106
+ end
107
+
108
+ def clone_for_step (*step_params)
109
+ self.class.new(@engine.clone, @users.clone, @steps + [Step.new(step_params)])
110
+ end
111
+
112
+ def changes
113
+ @steps.select {|step| step.length > 1}
114
+ end
115
+
116
+ def subset? (other_approach)
117
+ other_approach.changes.length >= changes.length &&
118
+ changes.all? {|step| other_approach.changes.any? {|step_2| step_2.eql?(step)} }
119
+ end
120
+
121
+ def state_hash
122
+ @engine.auth_rules.inject(0) do |memo, rule|
123
+ memo + rule.privileges.hash + rule.contexts.hash +
124
+ rule.attributes.hash + rule.role.hash
125
+ end +
126
+ @users.inject(0) {|memo, user| memo + user.role_symbols.hash } +
127
+ @engine.privileges.hash + @engine.privilege_hierarchy.hash +
128
+ @engine.roles.hash + @engine.role_hierarchy.hash
129
+ end
130
+
131
+ def sort_value
132
+ (changes.length + 1) + steps.length / 2 + (@failed_test_count.to_i + 1)
133
+ end
134
+
135
+ def inspect
136
+ "Approach (#{state_hash}): Steps: #{changes.map(&:inspect) * ', '}"# +
137
+ # "\n Roles: #{AnalyzerEngine.roles(@engine).map(&:to_sym).inspect}; " +
138
+ # "\n Users: #{@users.map(&:role_symbols).inspect}"
139
+ end
140
+
141
+ def <=> (other)
142
+ sort_value <=> other.sort_value
143
+ end
144
+ end
145
+
146
+ class Step < Array
147
+ def eql? (other)
148
+ # TODO use approach.users.index(self[idx]) ==
149
+ # other.approach.users.index(other[idx])
150
+ # instead of user.login
151
+ other.is_a?(Array) && other.length == length &&
152
+ (0...length).all? {|idx| self[idx].class == other[idx].class &&
153
+ ((self[idx].respond_to?(:to_sym) && self[idx].to_sym == other[idx].to_sym) ||
154
+ (self[idx].respond_to?(:login) && self[idx].login == other[idx].login) ||
155
+ self[idx] == other[idx] ) }
156
+ end
157
+
158
+ def inspect
159
+ collect {|info| info.respond_to?(:to_sym) ? info.to_sym : (info.respond_to?(:login) ? info.login : info.class.name)}.inspect
160
+ end
161
+ end
162
+
163
+ protected
164
+ def next_step (viable_approaches, candidates, approach_checker,
165
+ privilege, context, strategy)
166
+ candidate = candidates.shift
167
+ next_in_strategy = strategy[candidate.steps.length % strategy.length]
168
+
169
+ #if @seen_states.include?([candidate.state_hash, next_in_strategy])
170
+ # puts "SKIPPING #{next_in_strategy}; #{candidate.inspect}"
171
+ #end
172
+ return if @seen_states.include?([candidate.state_hash, next_in_strategy])
173
+ @seen_states << [candidate.state_hash, next_in_strategy]
174
+ candidate.steps << [next_in_strategy]
175
+ candidates << candidate
176
+
177
+ new_approaches = []
178
+
179
+ #puts "#{next_in_strategy} on #{candidate.inspect}"
180
+ case next_in_strategy
181
+ when :add_role
182
+ # ensure non-existent name
183
+ approach = candidate.clone_for_step(:add_role, :new_role_for_change_analyzer)
184
+ if AnalyzerEngine.apply_change(approach.engine, approach.changes.last)
185
+ #AnalyzerEngine.apply_change(approach.engine, [:add_privilege, privilege, context, :new_role_for_change_analyzer])
186
+ new_approaches << approach
187
+ end
188
+ when :assign_role_to_user
189
+ candidate.users.each do |user|
190
+ relevant_roles(candidate).each do |role|
191
+ next if user.role_symbols.include?(role.to_sym)
192
+ approach = candidate.clone_for_step(:assign_role_to_user, role, user)
193
+ # beware of shallow copies!
194
+ cloned_user = user.clone
195
+ approach.users[approach.users.index(user)] = cloned_user
196
+ # possible on real user objects?
197
+ cloned_user.role_symbols << role.to_sym
198
+ new_approaches << approach
199
+ end
200
+ end
201
+ when :remove_role_from_user
202
+ candidate.users.each do |user|
203
+ user.role_symbols.each do |role_sym|
204
+ approach = candidate.clone_for_step(:remove_role_from_user, role_sym, user)
205
+ # beware of shallow copies!
206
+ cloned_user = user.clone
207
+ approach.users[approach.users.index(user)] = cloned_user
208
+ # possible on real user objects?
209
+ cloned_user.role_symbols.delete(role_sym)
210
+ new_approaches << approach
211
+ end
212
+ end
213
+ when :add_privilege
214
+ relevant_roles(candidate).each do |role|
215
+ approach = candidate.clone_for_step(:add_privilege, privilege, context, role)
216
+ AnalyzerEngine.apply_change(approach.engine, approach.changes.last)
217
+ new_approaches << approach
218
+ end
219
+ when :remove_privilege
220
+ relevant_roles(candidate).each do |role|
221
+ approach = candidate.clone_for_step(:remove_privilege, privilege, context, role)
222
+ if AnalyzerEngine.apply_change(approach.engine, approach.changes.last)
223
+ new_approaches << approach
224
+ end
225
+ end
226
+ else
227
+ raise "Unknown next strategy step #{next_in_strategy}"
228
+ end
229
+
230
+ new_approaches.each do |new_approach|
231
+ if new_approach.check(approach_checker)
232
+ unless viable_approaches.any? {|viable_approach| viable_approach.subset?(new_approach) }
233
+ #puts "New: #{new_approach.changes.inspect}\n #{viable_approaches.map(&:changes).inspect}"
234
+ viable_approaches.delete_if {|viable_approach| new_approach.subset?(viable_approach)}
235
+ viable_approaches << new_approach unless viable_approaches.find {|v_a| v_a.state_hash == new_approach.state_hash}
236
+ end
237
+ else
238
+ candidates << new_approach
239
+ end
240
+ end
241
+
242
+ candidates.sort!
243
+ end
244
+
245
+ def relevant_roles (approach)
246
+ #return AnalyzerEngine.roles(approach.engine)
247
+ (AnalyzerEngine.relevant_roles(approach.engine, approach.users) +
248
+ (approach.engine.roles.include?(:new_role_for_change_analyzer) ?
249
+ [AnalyzerEngine::Role.for_sym(:new_role_for_change_analyzer, approach.engine)] : [])).uniq
250
+ end
251
+ end
252
+ end
253
+ end