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,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