rubycs-declarative_authorization 0.3.0

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 (37) hide show
  1. data/CHANGELOG +70 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +9 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +114 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +100 -0
  8. data/app/views/authorization_rules/graph.dot.erb +49 -0
  9. data/app/views/authorization_rules/graph.html.erb +39 -0
  10. data/app/views/authorization_rules/index.html.erb +16 -0
  11. data/app/views/authorization_usages/index.html.erb +45 -0
  12. data/authorization_rules.dist.rb +20 -0
  13. data/config/locales/en.declarative_authorization.yml +35 -0
  14. data/config/locales/ro.declarative_authorization.yml +35 -0
  15. data/config/routes.rb +6 -0
  16. data/garlic_example.rb +20 -0
  17. data/init.rb +5 -0
  18. data/lib/declarative_authorization.rb +15 -0
  19. data/lib/declarative_authorization/authorization.rb +578 -0
  20. data/lib/declarative_authorization/authorization_rules_analyzer.rb +138 -0
  21. data/lib/declarative_authorization/helper.rb +56 -0
  22. data/lib/declarative_authorization/in_controller.rb +343 -0
  23. data/lib/declarative_authorization/in_model.rb +125 -0
  24. data/lib/declarative_authorization/maintenance.rb +174 -0
  25. data/lib/declarative_authorization/obligation_scope.rb +292 -0
  26. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  27. data/lib/declarative_authorization/reader.rb +430 -0
  28. data/test/authorization_rules_analyzer_test.rb +123 -0
  29. data/test/authorization_test.rb +779 -0
  30. data/test/controller_test.rb +361 -0
  31. data/test/dsl_reader_test.rb +157 -0
  32. data/test/helper_test.rb +133 -0
  33. data/test/maintenance_test.rb +15 -0
  34. data/test/model_test.rb +1143 -0
  35. data/test/schema.sql +53 -0
  36. data/test/test_helper.rb +99 -0
  37. metadata +97 -0
@@ -0,0 +1,138 @@
1
+ begin
2
+ require "ruby_parser"
3
+ #require "parse_tree"
4
+ #require "parse_tree_extensions"
5
+ require "sexp_processor"
6
+ rescue LoadError
7
+ raise "Authorization::Analyzer requires ruby_parser gem"
8
+ end
9
+
10
+ module Authorization
11
+
12
+ class Analyzer
13
+ attr_reader :engine
14
+
15
+ def initialize (engine)
16
+ @engine = engine
17
+ end
18
+
19
+ def analyze (rules)
20
+ sexp_array = RubyParser.new.parse(rules)
21
+ #sexp_array = ParseTree.translate(rules)
22
+ @reports = []
23
+ [MergeableRulesProcessor].each do |parser|
24
+ parser.new(self).analyze(sexp_array)
25
+ end
26
+ #p @reports
27
+ end
28
+
29
+ def reports
30
+ @reports or raise "No rules analyzed!"
31
+ end
32
+
33
+ class GeneralAuthorizationProcessor < SexpProcessor
34
+ def initialize(analyzer)
35
+ super()
36
+ self.auto_shift_type = true
37
+ self.require_empty = false
38
+ self.strict = false
39
+ @analyzer = analyzer
40
+ end
41
+
42
+ def analyze (sexp_array)
43
+ process(sexp_array)
44
+ analyze_rules
45
+ end
46
+
47
+ def analyze_rules
48
+ # to be implemented by specific processor
49
+ end
50
+
51
+ def process_iter (exp)
52
+ s(:iter, process(exp.shift), process(exp.shift), process(exp.shift))
53
+ end
54
+
55
+ def process_arglist (exp)
56
+ s(exp.collect {|inner_exp| process(inner_exp).shift})
57
+ end
58
+
59
+ def process_hash (exp)
60
+ s(Hash[*exp.collect {|inner_exp| process(inner_exp).shift}])
61
+ end
62
+
63
+ def process_lit (exp)
64
+ s(exp.shift)
65
+ end
66
+ end
67
+
68
+ class MergeableRulesProcessor < GeneralAuthorizationProcessor
69
+ def analyze_rules
70
+ if @has_permission
71
+ #p @has_permission
72
+ permissions_by_context_and_rules = @has_permission.inject({}) do |memo, permission|
73
+ key = [permission[:context], permission[:rules]]
74
+ memo[key] ||= []
75
+ memo[key] << permission
76
+ memo
77
+ end
78
+
79
+ permissions_by_context_and_rules.each do |key, rules|
80
+ if rules.length > 1
81
+ rule_lines = rules.collect {|rule| rule[:line] }
82
+ rules.each do |rule|
83
+ @analyzer.reports << Report.new(:mergeable_rules, "", rule[:line],
84
+ "Similar rules already in line(s) " +
85
+ rule_lines.reject {|l| l == rule[:line] } * ", ")
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def process_call (exp)
93
+ klass = exp.shift
94
+ name = exp.shift
95
+ case name
96
+ when :role
97
+ analyze_rules
98
+ @has_permission = []
99
+ s(:call, klass, name)
100
+ when :has_permission_on
101
+ arglist_line = exp[0].line
102
+ arglist = process(exp.shift).shift
103
+ context = arglist.shift
104
+ args_hash = arglist.shift
105
+ @has_permission << {
106
+ :context => context,
107
+ :rules => [],
108
+ :privilege => args_hash && args_hash[:to],
109
+ # a hack: call exp line seems to be wrong
110
+ :line => arglist_line
111
+ }
112
+ s(:call, klass, name)
113
+ when :to
114
+ @has_permission.last[:privilege] = process(exp.shift).shift if @has_permission
115
+ s(:call, klass, name)
116
+ when :if_attribute
117
+ @in_if_attribute = true
118
+ rules = process(exp.shift).shift
119
+ @has_permission.last[:rules] << rules if @has_permission
120
+ @in_if_attribute = false
121
+ s(:call, klass, name)
122
+ else
123
+ s(:call, klass, name, process(exp.shift))
124
+ end
125
+ end
126
+ end
127
+
128
+ class Report
129
+ attr_reader :type, :filename, :line, :message
130
+ def initialize (type, filename, line, msg)
131
+ @type = type
132
+ @filename = filename
133
+ @line = line
134
+ @message = msg
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,56 @@
1
+ # Authorization::AuthorizationHelper
2
+ require File.dirname(__FILE__) + '/authorization.rb'
3
+
4
+ module Authorization
5
+ module AuthorizationHelper
6
+
7
+ # If the current user meets the given privilege, permitted_to? returns true
8
+ # and yields to the optional block. The attribute checks that are defined
9
+ # in the authorization rules are only evaluated if an object is given
10
+ # for context.
11
+ #
12
+ # Examples:
13
+ # <% permitted_to? :create, :users do %>
14
+ # <%= link_to 'New', new_user_path %>
15
+ # <% end %>
16
+ # ...
17
+ # <% if permitted_to? :create, :users %>
18
+ # <%= link_to 'New', new_user_path %>
19
+ # <% else %>
20
+ # You are not allowed to create new users!
21
+ # <% end %>
22
+ # ...
23
+ # <% for user in @users %>
24
+ # <%= link_to 'Edit', edit_user_path(user) if permitted_to? :update, user %>
25
+ # <% end %>
26
+ #
27
+ def permitted_to? (privilege, object_or_sym = nil, &block)
28
+ controller.permitted_to?(privilege, object_or_sym, &block)
29
+ end
30
+
31
+ # While permitted_to? is used for authorization in views, in some cases
32
+ # content should only be shown to some users without being concerned
33
+ # with authorization. E.g. to only show the most relevant menu options
34
+ # to a certain group of users. That is what has_role? should be used for.
35
+ #
36
+ # Examples:
37
+ # <% has_role?(:sales) do %>
38
+ # <%= link_to 'All contacts', contacts_path %>
39
+ # <% end %>
40
+ # ...
41
+ # <% if has_role?(:sales) %>
42
+ # <%= link_to 'Customer contacts', contacts_path %>
43
+ # <% else %>
44
+ # ...
45
+ # <% end %>
46
+ #
47
+ def has_role? (*roles, &block)
48
+ controller.has_role?(*roles, &block)
49
+ end
50
+
51
+ # As has_role? except checks all roles included in the role hierarchy
52
+ def has_role_with_hierarchy?(*roles, &block)
53
+ controller.has_role_with_hierarchy?(*roles, &block)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,343 @@
1
+ # Authorization::AuthorizationInController
2
+ require File.dirname(__FILE__) + '/authorization.rb'
3
+
4
+ module Authorization
5
+ module AuthorizationInController
6
+
7
+ def self.included(base) # :nodoc:
8
+ base.extend(ClassMethods)
9
+ base.hide_action :authorization_engine, :permitted_to?,
10
+ :permitted_to!
11
+ end
12
+
13
+ DEFAULT_DENY = false
14
+
15
+ # Returns the Authorization::Engine for the current controller.
16
+ def authorization_engine
17
+ @authorization_engine ||= Authorization::Engine.instance
18
+ end
19
+
20
+ # If the current user meets the given privilege, permitted_to? returns true
21
+ # and yields to the optional block. The attribute checks that are defined
22
+ # in the authorization rules are only evaluated if an object is given
23
+ # for context.
24
+ #
25
+ # See examples for Authorization::AuthorizationHelper #permitted_to?
26
+ #
27
+ def permitted_to? (privilege, object_or_sym = nil, &block)
28
+ context = object = nil
29
+ if object_or_sym.is_a?(Symbol)
30
+ context = object_or_sym
31
+ else
32
+ object = object_or_sym
33
+ end
34
+ # TODO infer context also from self.class.name
35
+ authorization_engine.permit?(privilege,
36
+ {:user => current_user,
37
+ :object => object,
38
+ :context => context,
39
+ :skip_attribute_test => object.nil?},
40
+ &block)
41
+ end
42
+
43
+ # Works similar to the permitted_to? method, but doesn't accept a block
44
+ # and throws the authorization exceptions, just like Engine#permit!
45
+ def permitted_to! (privilege, object_or_sym = nil)
46
+ context = object = nil
47
+ if object_or_sym.is_a?(Symbol)
48
+ context = object_or_sym
49
+ else
50
+ object = object_or_sym
51
+ end
52
+ authorization_engine.permit!(privilege,
53
+ {:user => current_user,
54
+ :object => object,
55
+ :context => context,
56
+ :skip_attribute_test => object.nil?})
57
+ end
58
+
59
+ # While permitted_to? is used for authorization, in some cases
60
+ # content should only be shown to some users without being concerned
61
+ # with authorization. E.g. to only show the most relevant menu options
62
+ # to a certain group of users. That is what has_role? should be used for.
63
+ def has_role? (*roles, &block)
64
+ user_roles = authorization_engine.roles_for(current_user)
65
+ result = roles.all? do |role|
66
+ user_roles.include?(role)
67
+ end
68
+ yield if result and block_given?
69
+ result
70
+ end
71
+
72
+ # As has_role? except checks all roles included in the role hierarchy
73
+ def has_role_with_hierarchy?(*roles, &block)
74
+ user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
75
+ result = roles.all? do |role|
76
+ user_roles.include?(role)
77
+ end
78
+ yield if result and block_given?
79
+ result
80
+ end
81
+
82
+
83
+ protected
84
+ def filter_access_filter # :nodoc:
85
+ permissions = self.class.all_filter_access_permissions
86
+ all_permissions = permissions.select {|p| p.actions.include?(:all)}
87
+ matching_permissions = permissions.select {|p| p.matches?(action_name)}
88
+ allowed = false
89
+ auth_exception = nil
90
+ begin
91
+ allowed = if !matching_permissions.empty?
92
+ matching_permissions.all? {|perm| perm.permit!(self)}
93
+ elsif !all_permissions.empty?
94
+ all_permissions.all? {|perm| perm.permit!(self)}
95
+ else
96
+ !DEFAULT_DENY
97
+ end
98
+ rescue AuthorizationError => e
99
+ auth_exception = e
100
+ end
101
+
102
+ unless allowed
103
+ if all_permissions.empty? and matching_permissions.empty?
104
+ logger.warn "Permission denied: No matching filter access " +
105
+ "rule found for #{self.class.controller_name}.#{action_name}"
106
+ elsif auth_exception
107
+ logger.info "Permission denied: #{auth_exception}"
108
+ end
109
+ if respond_to?(:permission_denied)
110
+ # permission_denied needs to render or redirect
111
+ send(:permission_denied)
112
+ else
113
+ send(:render, :text => I18n.t(:you_are_not_allowed_to_access_this_action, :scope => [:declarative_authorization]),
114
+ :status => :forbidden)
115
+ end
116
+ end
117
+ end
118
+
119
+ module ClassMethods
120
+ #
121
+ # Defines a filter to be applied according to the authorization of the
122
+ # current user. Requires at least one symbol corresponding to an
123
+ # action as parameter. The special symbol :+all+ refers to all action.
124
+ # The all :+all+ statement is only employed if no specific statement is
125
+ # present.
126
+ # class UserController < ApplicationController
127
+ # filter_access_to :index
128
+ # filter_access_to :new, :edit
129
+ # filter_access_to :all
130
+ # ...
131
+ # end
132
+ #
133
+ # The default is to allow access unconditionally if no rule matches.
134
+ # Thus, including the +filter_access_to+ :+all+ statement is a good
135
+ # idea, implementing a default-deny policy.
136
+ #
137
+ # When the access is denied, the method +permission_denied+ is called
138
+ # on the current controller, if defined. Else, a simple "you are not
139
+ # allowed" string is output. Log.info is given more information on the
140
+ # reasons of denial.
141
+ #
142
+ # def permission_denied
143
+ # flash[:error] = 'Sorry, you are not allowed to the requested page.'
144
+ # respond_to do |format|
145
+ # format.html { redirect_to(:back) rescue redirect_to('/') }
146
+ # format.xml { head :unauthorized }
147
+ # format.js { head :unauthorized }
148
+ # end
149
+ # end
150
+ #
151
+ # By default, required privileges are infered from the action name and
152
+ # the controller name. Thus, in UserController :+edit+ requires
153
+ # :+edit+ +users+. To specify required privilege, use the option :+require+
154
+ # filter_access_to :new, :create, :require => :create, :context => :users
155
+ #
156
+ # Without the :+attribute_check+ option, no constraints from the
157
+ # authorization rules are enforced because for some actions (collections,
158
+ # +new+, +create+), there is no object to evaluate conditions against. To
159
+ # allow attribute checks on all actions, it is a common pattern to provide
160
+ # custom objects through +before_filters+:
161
+ # class BranchesController < ApplicationController
162
+ # before_filter :load_company
163
+ # before_filter :new_branch_from_company_and_params,
164
+ # :only => [:index, :new, :create]
165
+ # filter_access_to :all, :attribute_check => true
166
+ #
167
+ # protected
168
+ # def new_branch_from_company_and_params
169
+ # @branch = @company.branches.new(params[:branch])
170
+ # end
171
+ # end
172
+ # NOTE: +before_filters+ need to be defined before the first
173
+ # +filter_access_to+ call.
174
+ #
175
+ # For further customization, a custom filter expression may be formulated
176
+ # in a block, which is then evaluated in the context of the controller
177
+ # on a matching request. That is, for checking two objects, use the
178
+ # following:
179
+ # filter_access_to :merge do
180
+ # permitted_to!(:update, User.find(params[:original_id])) and
181
+ # permitted_to!(:delete, User.find(params[:id]))
182
+ # end
183
+ # The block should raise a Authorization::AuthorizationError or return
184
+ # false if the access is to be denied.
185
+ #
186
+ # Later calls to filter_access_to with overlapping actions overwrite
187
+ # previous ones for that action.
188
+ #
189
+ # All options:
190
+ # [:+require+]
191
+ # Privilege required; defaults to action_name
192
+ # [:+context+]
193
+ # The privilege's context, defaults to controller_name, pluralized.
194
+ # [:+attribute_check+]
195
+ # Enables the check of attributes defined in the authorization rules.
196
+ # Defaults to false. If enabled, filter_access_to will use a context
197
+ # object from one of the following sources (in that order):
198
+ # * the method from the :+load_method+ option,
199
+ # * an instance variable named after the singular of the context
200
+ # (by default from the controller name, e.g. @post for PostsController),
201
+ # * a find on the context model, using +params+[:id] as id value.
202
+ # Any of these methods will only be employed if :+attribute_check+
203
+ # is enabled.
204
+ # [:+model+]
205
+ # The data model to load a context object from. Defaults to the
206
+ # context, singularized.
207
+ # [:+load_method+]
208
+ # Specify a method by symbol or a Proc object which should be used
209
+ # to load the object. Both should return the loaded object.
210
+ # If a Proc object is given, e.g. by way of
211
+ # +lambda+, it is called in the instance of the controller.
212
+ # Example demonstrating the default behaviour:
213
+ # filter_access_to :show, :attribute_check => true,
214
+ # :load_method => lambda { User.find(params[:id]) }
215
+ #
216
+
217
+ def filter_access_to (*args, &filter_block)
218
+ options = args.last.is_a?(Hash) ? args.pop : {}
219
+ options = {
220
+ :require => nil,
221
+ :context => nil,
222
+ :attribute_check => false,
223
+ :model => nil,
224
+ :load_method => nil
225
+ }.merge!(options)
226
+ privilege = options[:require]
227
+ context = options[:context]
228
+ actions = args.flatten
229
+
230
+ # collect permits in controller array for use in one before_filter
231
+ unless filter_chain.any? {|filter| filter.method == :filter_access_filter}
232
+ before_filter :filter_access_filter
233
+ end
234
+
235
+ filter_access_permissions.each do |perm|
236
+ perm.remove_actions(actions)
237
+ end
238
+ filter_access_permissions <<
239
+ ControllerPermission.new(actions, privilege, context,
240
+ options[:attribute_check],
241
+ options[:model],
242
+ options[:load_method],
243
+ filter_block)
244
+ end
245
+
246
+ # Collecting all the ControllerPermission objects from the controller
247
+ # hierarchy. Permissions for actions are overwritten by calls to
248
+ # filter_access_to in child controllers with the same action.
249
+ def all_filter_access_permissions # :nodoc:
250
+ ancestors.inject([]) do |perms, mod|
251
+ if mod.respond_to?(:filter_access_permissions)
252
+ perms +
253
+ mod.filter_access_permissions.collect do |p1|
254
+ p1.clone.remove_actions(perms.inject(Set.new) {|actions, p2| actions + p2.actions})
255
+ end
256
+ else
257
+ perms
258
+ end
259
+ end
260
+ end
261
+
262
+ protected
263
+ def filter_access_permissions # :nodoc:
264
+ unless filter_access_permissions?
265
+ ancestors[1..-1].reverse.each do |mod|
266
+ mod.filter_access_permissions if mod.respond_to?(:filter_access_permissions)
267
+ end
268
+ end
269
+ class_variable_set(:@@declarative_authorization_permissions, {}) unless filter_access_permissions?
270
+ class_variable_get(:@@declarative_authorization_permissions)[self.name] ||= []
271
+ end
272
+
273
+ def filter_access_permissions? # :nodoc:
274
+ class_variable_defined?(:@@declarative_authorization_permissions)
275
+ end
276
+ end
277
+ end
278
+
279
+ class ControllerPermission # :nodoc:
280
+ attr_reader :actions, :privilege, :context, :attribute_check
281
+ def initialize (actions, privilege, context, attribute_check = false,
282
+ load_object_model = nil, load_object_method = nil,
283
+ filter_block = nil)
284
+ @actions = actions.to_set
285
+ @privilege = privilege
286
+ @context = context
287
+ @load_object_model = load_object_model
288
+ @load_object_method = load_object_method
289
+ @filter_block = filter_block
290
+ @attribute_check = attribute_check
291
+ end
292
+
293
+ def matches? (action_name)
294
+ @actions.include?(action_name.to_sym)
295
+ end
296
+
297
+ def permit! (contr)
298
+ if @filter_block
299
+ return contr.instance_eval(&@filter_block)
300
+ end
301
+ context = @context || contr.class.controller_name.to_sym
302
+ object = @attribute_check ? load_object(contr, context) : nil
303
+ privilege = @privilege || :"#{contr.action_name}"
304
+
305
+ #puts "Trying permit?(#{privilege.inspect}, "
306
+ #puts " :user => #{contr.send(:current_user).inspect}, "
307
+ #puts " :object => #{object.inspect},"
308
+ #puts " :skip_attribute_test => #{!@attribute_check},"
309
+ #puts " :context => #{contr.class.controller_name.pluralize.to_sym})"
310
+ res = contr.authorization_engine.permit!(privilege,
311
+ :user => contr.send(:current_user),
312
+ :object => object,
313
+ :skip_attribute_test => !@attribute_check,
314
+ :context => context)
315
+ #puts "permit? result: #{res.inspect}"
316
+ res
317
+ end
318
+
319
+ def remove_actions (actions)
320
+ @actions -= actions
321
+ self
322
+ end
323
+
324
+ private
325
+ def load_object(contr, context)
326
+ if @load_object_method and @load_object_method.is_a?(Symbol)
327
+ contr.send(@load_object_method)
328
+ elsif @load_object_method and @load_object_method.is_a?(Proc)
329
+ contr.instance_eval(&@load_object_method)
330
+ else
331
+ load_object_model = @load_object_model || context.to_s.classify.constantize
332
+ instance_var = :"@#{load_object_model.name.underscore}"
333
+ object = contr.instance_variable_get(instance_var)
334
+ unless object
335
+ # catch ActiveRecord::RecordNotFound?
336
+ object = load_object_model.find(contr.params[:id])
337
+ contr.instance_variable_set(instance_var, object)
338
+ end
339
+ object
340
+ end
341
+ end
342
+ end
343
+ end