zeiv-declarative_authorization 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +189 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +632 -0
  5. data/Rakefile +53 -0
  6. data/app/controllers/authorization_rules_controller.rb +258 -0
  7. data/app/controllers/authorization_usages_controller.rb +22 -0
  8. data/app/helpers/authorization_rules_helper.rb +218 -0
  9. data/app/views/authorization_rules/_change.erb +58 -0
  10. data/app/views/authorization_rules/_show_graph.erb +44 -0
  11. data/app/views/authorization_rules/_suggestions.erb +48 -0
  12. data/app/views/authorization_rules/change.html.erb +169 -0
  13. data/app/views/authorization_rules/graph.dot.erb +68 -0
  14. data/app/views/authorization_rules/graph.html.erb +47 -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 +20 -0
  19. data/garlic_example.rb +20 -0
  20. data/init.rb +5 -0
  21. data/lib/declarative_authorization.rb +19 -0
  22. data/lib/declarative_authorization/adapters/active_record.rb +13 -0
  23. data/lib/declarative_authorization/adapters/active_record/base_extensions.rb +0 -0
  24. data/lib/declarative_authorization/adapters/active_record/obligation_scope_builder.rb +0 -0
  25. data/lib/declarative_authorization/authorization.rb +798 -0
  26. data/lib/declarative_authorization/development_support/analyzer.rb +261 -0
  27. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  28. data/lib/declarative_authorization/development_support/change_supporter.rb +620 -0
  29. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  30. data/lib/declarative_authorization/helper.rb +68 -0
  31. data/lib/declarative_authorization/in_controller.rb +703 -0
  32. data/lib/declarative_authorization/in_model.rb +188 -0
  33. data/lib/declarative_authorization/maintenance.rb +210 -0
  34. data/lib/declarative_authorization/obligation_scope.rb +361 -0
  35. data/lib/declarative_authorization/rails_legacy.rb +22 -0
  36. data/lib/declarative_authorization/railsengine.rb +6 -0
  37. data/lib/declarative_authorization/reader.rb +546 -0
  38. data/lib/generators/authorization/install/install_generator.rb +77 -0
  39. data/lib/generators/authorization/rules/rules_generator.rb +14 -0
  40. data/lib/generators/authorization/rules/templates/authorization_rules.rb +27 -0
  41. data/lib/tasks/authorization_tasks.rake +89 -0
  42. data/test/authorization_test.rb +1124 -0
  43. data/test/controller_filter_resource_access_test.rb +575 -0
  44. data/test/controller_test.rb +480 -0
  45. data/test/database.yml +3 -0
  46. data/test/dsl_reader_test.rb +178 -0
  47. data/test/helper_test.rb +247 -0
  48. data/test/maintenance_test.rb +46 -0
  49. data/test/model_test.rb +2008 -0
  50. data/test/schema.sql +56 -0
  51. data/test/test_helper.rb +255 -0
  52. metadata +95 -0
@@ -0,0 +1,243 @@
1
+
2
+ module Authorization
3
+ module DevelopmentSupport
4
+ class AbstractAnalyzer
5
+ attr_reader :engine
6
+
7
+ def initialize (engine)
8
+ @engine = engine
9
+ end
10
+
11
+ def roles
12
+ AnalyzerEngine.roles(engine)
13
+ end
14
+
15
+ def rules
16
+ roles.collect {|role| role.rules }.flatten
17
+ end
18
+ end
19
+
20
+ # Groups utility methods and classes to better work with authorization object
21
+ # model.
22
+ module AnalyzerEngine
23
+
24
+ def self.roles (engine)
25
+ Role.all(engine)
26
+ end
27
+
28
+ def self.relevant_roles (engine, users)
29
+ users.collect {|user| user.role_symbols.map {|role_sym| Role.for_sym(role_sym, engine)}}.
30
+ flatten.uniq.collect {|role| [role] + role.ancestors}.flatten.uniq
31
+ end
32
+
33
+ def self.rule_for_permission (engine, privilege, context, role)
34
+ AnalyzerEngine.roles(engine).
35
+ find {|cloned_role| cloned_role.to_sym == role.to_sym}.rules.find do |rule|
36
+ rule.contexts.include?(context) and rule.privileges.include?(privilege)
37
+ end
38
+ end
39
+
40
+ def self.apply_change (engine, change)
41
+ case change[0]
42
+ when :add_role
43
+ role_symbol = change[1]
44
+ if engine.roles.include?(role_symbol)
45
+ false
46
+ else
47
+ engine.roles << role_symbol
48
+ true
49
+ end
50
+ when :add_privilege
51
+ privilege, context, role = change[1,3]
52
+ role = Role.for_sym(role.to_sym, engine)
53
+ privilege = Privilege.for_sym(privilege.to_sym, engine)
54
+ if ([privilege] + privilege.ancestors).any? {|ancestor_privilege| ([role] + role.ancestors).any? {|ancestor_role| !ancestor_role.rules_for_permission(ancestor_privilege, context).empty?}}
55
+ false
56
+ else
57
+ engine.auth_rules << AuthorizationRule.new(role.to_sym,
58
+ [privilege.to_sym], [context])
59
+ true
60
+ end
61
+ when :remove_privilege
62
+ privilege, context, role = change[1,3]
63
+ role = Role.for_sym(role.to_sym, engine)
64
+ privilege = Privilege.for_sym(privilege.to_sym, engine)
65
+ rules_with_priv = role.rules_for_permission(privilege, context)
66
+ if rules_with_priv.empty?
67
+ false
68
+ else
69
+ rules_with_priv.each do |rule|
70
+ rule.rule.privileges.delete(privilege.to_sym)
71
+ engine.auth_rules.delete(rule.rule) if rule.rule.privileges.empty?
72
+ end
73
+ true
74
+ end
75
+ end
76
+ end
77
+
78
+ class Role
79
+ @@role_objects = {}
80
+ attr_reader :role
81
+ def initialize (role, rules, engine)
82
+ @role = role
83
+ @rules = rules
84
+ @engine = engine
85
+ end
86
+
87
+ def source_line
88
+ @rules.empty? ? nil : @rules.first.source_line
89
+ end
90
+ def source_file
91
+ @rules.empty? ? nil : @rules.first.source_file
92
+ end
93
+
94
+ # ancestors' privileges are included in in the current role
95
+ def ancestors (role_symbol = nil)
96
+ role_symbol ||= @role
97
+ (@engine.role_hierarchy[role_symbol] || []).
98
+ collect {|lower_role| ancestors(lower_role) }.flatten +
99
+ (role_symbol == @role ? [] : [Role.for_sym(role_symbol, @engine)])
100
+ end
101
+ def descendants (role_symbol = nil)
102
+ role_symbol ||= @role
103
+ (@engine.rev_role_hierarchy[role_symbol] || []).
104
+ collect {|higher_role| descendants(higher_role) }.flatten +
105
+ (role_symbol == @role ? [] : [Role.for_sym(role_symbol, @engine)])
106
+ end
107
+
108
+ def rules
109
+ @rules ||= @engine.auth_rules.select {|rule| rule.role == @role}.
110
+ collect {|rule| Rule.new(rule, @engine)}
111
+ end
112
+ def rules_for_permission (privilege, context)
113
+ rules.select do |rule|
114
+ rule.matches?([@role], [privilege.to_sym], context)
115
+ end
116
+ end
117
+
118
+ def to_sym
119
+ @role
120
+ end
121
+ def self.for_sym (role_sym, engine)
122
+ @@role_objects[[role_sym, engine]] ||= new(role_sym, nil, engine)
123
+ end
124
+
125
+ def self.all (engine)
126
+ rules_by_role = engine.auth_rules.inject({}) do |memo, rule|
127
+ memo[rule.role] ||= []
128
+ memo[rule.role] << rule
129
+ memo
130
+ end
131
+ engine.roles.collect do |role|
132
+ new(role, (rules_by_role[role] || []).
133
+ collect {|rule| Rule.new(rule, engine)}, engine)
134
+ end
135
+ end
136
+ def self.all_for_privilege (privilege, context, engine)
137
+ privilege = privilege.is_a?(Symbol) ? Privilege.for_sym(privilege, engine) : privilege
138
+ privilege_symbols = ([privilege] + privilege.ancestors).map(&:to_sym)
139
+ all(engine).select {|role| role.rules.any? {|rule| rule.matches?([role.to_sym], privilege_symbols, context)}}.
140
+ collect {|role| [role] + role.descendants}.flatten.uniq
141
+ end
142
+ end
143
+
144
+ class Rule
145
+ @@rule_objects = {}
146
+ delegate :source_line, :source_file, :contexts, :matches?, :to => :@rule
147
+ attr_reader :rule
148
+ def initialize (rule, engine)
149
+ @rule = rule
150
+ @engine = engine
151
+ end
152
+ def privileges
153
+ PrivilegesSet.new(self, @engine, @rule.privileges.collect {|privilege| Privilege.for_sym(privilege, @engine) })
154
+ end
155
+ def self.for_rule (rule, engine)
156
+ @@rule_objects[[rule, engine]] ||= new(rule, engine)
157
+ end
158
+ end
159
+
160
+ class Privilege
161
+ @@privilege_objects = {}
162
+ def initialize (privilege, engine)
163
+ @privilege = privilege
164
+ @engine = engine
165
+ end
166
+
167
+ # Ancestor privileges are higher in the hierarchy.
168
+ # Doesn't take context into account.
169
+ def ancestors (priv_symbol = nil)
170
+ priv_symbol ||= @privilege
171
+ # context-specific?
172
+ (@engine.rev_priv_hierarchy[[priv_symbol, nil]] || []).
173
+ collect {|higher_priv| ancestors(higher_priv) }.flatten +
174
+ (priv_symbol == @privilege ? [] : [Privilege.for_sym(priv_symbol, @engine)])
175
+ end
176
+ def descendants (priv_symbol = nil)
177
+ priv_symbol ||= @privilege
178
+ # context-specific?
179
+ (@engine.privilege_hierarchy[priv_symbol] || []).
180
+ collect {|lower_priv, context| descendants(lower_priv) }.flatten +
181
+ (priv_symbol == @privilege ? [] : [Privilege.for_sym(priv_symbol, @engine)])
182
+ end
183
+
184
+ def rules
185
+ @rules ||= find_rules_for_privilege
186
+ end
187
+ def source_line
188
+ rules.empty? ? nil : rules.first.source_line
189
+ end
190
+ def source_file
191
+ rules.empty? ? nil : rules.first.source_file
192
+ end
193
+
194
+ def to_sym
195
+ @privilege
196
+ end
197
+ def self.for_sym (privilege_sym, engine)
198
+ @@privilege_objects[[privilege_sym, engine]] ||= new(privilege_sym, engine)
199
+ end
200
+
201
+ private
202
+ def find_rules_for_privilege
203
+ @engine.auth_rules.select {|rule| rule.privileges.include?(@privilege)}.
204
+ collect {|rule| Rule.for_rule(rule, @engine)}
205
+ end
206
+ end
207
+
208
+ class PrivilegesSet < Set
209
+ def initialize (*args)
210
+ if args.length > 2
211
+ @rule = args.shift
212
+ @engine = args.shift
213
+ end
214
+ super(*args)
215
+ end
216
+ def include? (privilege)
217
+ if privilege.is_a?(Symbol)
218
+ super(privilege_from_symbol(privilege))
219
+ else
220
+ super
221
+ end
222
+ end
223
+ def delete (privilege)
224
+ @rule.rule.privileges.delete(privilege.to_sym)
225
+ if privilege.is_a?(Symbol)
226
+ super(privilege_from_symbol(privilege))
227
+ else
228
+ super
229
+ end
230
+ end
231
+
232
+ def intersects? (privileges)
233
+ intersection(privileges).length > 0
234
+ end
235
+
236
+ private
237
+ def privilege_from_symbol (privilege_sym)
238
+ Privilege.for_sym(privilege_sym, @engine)
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,68 @@
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
+ # To pass in an object and override the context, you can use the optional
28
+ # options:
29
+ # permitted_to? :update, user, :context => :account
30
+ #
31
+ def permitted_to? (privilege, object_or_sym = nil, options = {}, &block)
32
+ controller.permitted_to?(privilege, object_or_sym, options, &block)
33
+ end
34
+
35
+ # While permitted_to? is used for authorization in views, in some cases
36
+ # content should only be shown to some users without being concerned
37
+ # with authorization. E.g. to only show the most relevant menu options
38
+ # to a certain group of users. That is what has_role? should be used for.
39
+ #
40
+ # Examples:
41
+ # <% has_role?(:sales) do %>
42
+ # <%= link_to 'All contacts', contacts_path %>
43
+ # <% end %>
44
+ # ...
45
+ # <% if has_role?(:sales) %>
46
+ # <%= link_to 'Customer contacts', contacts_path %>
47
+ # <% else %>
48
+ # ...
49
+ # <% end %>
50
+ #
51
+ def has_role? (*roles, &block)
52
+ controller.has_role?(*roles, &block)
53
+ end
54
+
55
+ # As has_role? except checks all roles included in the role hierarchy
56
+ def has_role_with_hierarchy?(*roles, &block)
57
+ controller.has_role_with_hierarchy?(*roles, &block)
58
+ end
59
+
60
+ def has_any_role?(*roles,&block)
61
+ controller.has_any_role?(*roles,&block)
62
+ end
63
+
64
+ def has_any_role_with_hierarchy?(*roles, &block)
65
+ controller.has_any_role_with_hierarchy?(*roles, &block)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,703 @@
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
+ # If attribute_check is set for filter_access_to, decl_auth_context will try to
16
+ # load the appropriate object from the current controller's model with
17
+ # the id from params[:id]. If that fails, a 404 Not Found is often the
18
+ # right way to handle the error. If you have additional measures in place
19
+ # that restricts the find scope, handling this error as a permission denied
20
+ # might be a better way. Set failed_auto_loading_is_not_found to false
21
+ # for the latter behavior.
22
+ @@failed_auto_loading_is_not_found = true
23
+ def self.failed_auto_loading_is_not_found?
24
+ @@failed_auto_loading_is_not_found
25
+ end
26
+ def self.failed_auto_loading_is_not_found= (new_value)
27
+ @@failed_auto_loading_is_not_found = new_value
28
+ end
29
+
30
+ # Returns the Authorization::Engine for the current controller.
31
+ def authorization_engine
32
+ @authorization_engine ||= Authorization::Engine.instance
33
+ end
34
+
35
+ # If the current user meets the given privilege, permitted_to? returns true
36
+ # and yields to the optional block. The attribute checks that are defined
37
+ # in the authorization rules are only evaluated if an object is given
38
+ # for context.
39
+ #
40
+ # See examples for Authorization::AuthorizationHelper #permitted_to?
41
+ #
42
+ # If no object or context is specified, the controller_name is used as
43
+ # context.
44
+ #
45
+ def permitted_to? (privilege, object_or_sym = nil, options = {})
46
+ if authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, false))
47
+ yield if block_given?
48
+ true
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ # Works similar to the permitted_to? method, but
55
+ # throws the authorization exceptions, just like Engine#permit!
56
+ def permitted_to! (privilege, object_or_sym = nil, options = {})
57
+ authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, true))
58
+ end
59
+
60
+ # While permitted_to? is used for authorization, in some cases
61
+ # content should only be shown to some users without being concerned
62
+ # with authorization. E.g. to only show the most relevant menu options
63
+ # to a certain group of users. That is what has_role? should be used for.
64
+ def has_role? (*roles, &block)
65
+ user_roles = authorization_engine.roles_for(current_user)
66
+ result = roles.all? do |role|
67
+ user_roles.include?(role)
68
+ end
69
+ yield if result and block_given?
70
+ result
71
+ end
72
+
73
+ # Intended to be used where you want to allow users with any single listed role to view
74
+ # the content in question
75
+ def has_any_role?(*roles,&block)
76
+ user_roles = authorization_engine.roles_for(current_user)
77
+ result = roles.any? do |role|
78
+ user_roles.include?(role)
79
+ end
80
+ yield if result and block_given?
81
+ result
82
+ end
83
+
84
+ # As has_role? except checks all roles included in the role hierarchy
85
+ def has_role_with_hierarchy?(*roles, &block)
86
+ user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
87
+ result = roles.all? do |role|
88
+ user_roles.include?(role)
89
+ end
90
+ yield if result and block_given?
91
+ result
92
+ end
93
+
94
+ # As has_any_role? except checks all roles included in the role hierarchy
95
+ def has_any_role_with_hierarchy?(*roles, &block)
96
+ user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
97
+ result = roles.any? do |role|
98
+ user_roles.include?(role)
99
+ end
100
+ yield if result and block_given?
101
+ result
102
+ end
103
+
104
+ protected
105
+ def filter_access_filter # :nodoc:
106
+ permissions = self.class.all_filter_access_permissions
107
+ all_permissions = permissions.select {|p| p.actions.include?(:all)}
108
+ matching_permissions = permissions.select {|p| p.matches?(action_name)}
109
+ allowed = false
110
+ auth_exception = nil
111
+ begin
112
+ allowed = if !matching_permissions.empty?
113
+ matching_permissions.all? {|perm| perm.permit!(self)}
114
+ elsif !all_permissions.empty?
115
+ all_permissions.all? {|perm| perm.permit!(self)}
116
+ else
117
+ !DEFAULT_DENY
118
+ end
119
+ rescue NotAuthorized => e
120
+ auth_exception = e
121
+ end
122
+
123
+ unless allowed
124
+ if all_permissions.empty? and matching_permissions.empty?
125
+ logger.warn "Permission denied: No matching filter access " +
126
+ "rule found for #{self.class.controller_name}.#{action_name}"
127
+ elsif auth_exception
128
+ logger.info "Permission denied: #{auth_exception}"
129
+ end
130
+ if respond_to?(:permission_denied, true)
131
+ # permission_denied needs to render or redirect
132
+ send(:permission_denied)
133
+ else
134
+ send(:render, :text => "You are not allowed to access this action.",
135
+ :status => :forbidden)
136
+ end
137
+ end
138
+ end
139
+
140
+ def load_controller_object (context_without_namespace = nil, model = nil) # :nodoc:
141
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
142
+ model = model ? model.classify.constantize : context_without_namespace.to_s.classify.constantize
143
+ instance_variable_set(instance_var, model.find(params[:id]))
144
+ end
145
+
146
+ def load_parent_controller_object (parent_context_without_namespace) # :nodoc:
147
+ instance_var = :"@#{parent_context_without_namespace.to_s.singularize}"
148
+ model = parent_context_without_namespace.to_s.classify.constantize
149
+ instance_variable_set(instance_var, model.find(params[:"#{parent_context_without_namespace.to_s.singularize}_id"]))
150
+ end
151
+
152
+ def new_controller_object_from_params (context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc:
153
+ model_or_proxy = parent_context_without_namespace ?
154
+ instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
155
+ context_without_namespace.to_s.classify.constantize
156
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
157
+ instance_variable_set(instance_var,
158
+ model_or_proxy.new(params[context_without_namespace.to_s.singularize]))
159
+ end
160
+
161
+ def new_blank_controller_object (context_without_namespace, parent_context_without_namespace, strong_params, model) # :nodoc:
162
+ if model
163
+ model_or_proxy = model.to_s.classify.constantize
164
+ else
165
+ model_or_proxy = parent_context_without_namespace ?
166
+ instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
167
+ context_without_namespace.to_s.classify.constantize
168
+ end
169
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
170
+ instance_variable_set(instance_var,
171
+ model_or_proxy.new())
172
+ end
173
+
174
+ def new_controller_object_for_collection (context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc:
175
+ model_or_proxy = parent_context_without_namespace ?
176
+ instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
177
+ context_without_namespace.to_s.classify.constantize
178
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
179
+ instance_variable_set(instance_var, model_or_proxy.new)
180
+ end
181
+
182
+ def options_for_permit (object_or_sym = nil, options = {}, bang = true)
183
+ context = object = nil
184
+ if object_or_sym.nil?
185
+ context = self.class.decl_auth_context
186
+ elsif !Authorization.is_a_association_proxy?(object_or_sym) and object_or_sym.is_a?(Symbol)
187
+ context = object_or_sym
188
+ else
189
+ object = object_or_sym
190
+ end
191
+
192
+ result = {:object => object,
193
+ :context => context,
194
+ :skip_attribute_test => object.nil?,
195
+ :bang => bang}.merge(options)
196
+ result[:user] = current_user unless result.key?(:user)
197
+ result
198
+ end
199
+
200
+ module ClassMethods
201
+ #
202
+ # Defines a filter to be applied according to the authorization of the
203
+ # current user. Requires at least one symbol corresponding to an
204
+ # action as parameter. The special symbol :+all+ refers to all action.
205
+ # The all :+all+ statement is only employed if no specific statement is
206
+ # present.
207
+ # class UserController < ApplicationController
208
+ # filter_access_to :index
209
+ # filter_access_to :new, :edit
210
+ # filter_access_to :all
211
+ # ...
212
+ # end
213
+ #
214
+ # The default is to allow access unconditionally if no rule matches.
215
+ # Thus, including the +filter_access_to+ :+all+ statement is a good
216
+ # idea, implementing a default-deny policy.
217
+ #
218
+ # When the access is denied, the method +permission_denied+ is called
219
+ # on the current controller, if defined. Else, a simple "you are not
220
+ # allowed" string is output. Log.info is given more information on the
221
+ # reasons of denial.
222
+ #
223
+ # def permission_denied
224
+ # flash[:error] = 'Sorry, you are not allowed to the requested page.'
225
+ # respond_to do |format|
226
+ # format.html { redirect_to(:back) rescue redirect_to('/') }
227
+ # format.xml { head :unauthorized }
228
+ # format.js { head :unauthorized }
229
+ # end
230
+ # end
231
+ #
232
+ # By default, required privileges are inferred from the action name and
233
+ # the controller name. Thus, in UserController :+edit+ requires
234
+ # :+edit+ +users+. To specify required privilege, use the option :+require+
235
+ # filter_access_to :new, :create, :require => :create, :context => :users
236
+ #
237
+ # Without the :+attribute_check+ option, no constraints from the
238
+ # authorization rules are enforced because for some actions (collections,
239
+ # +new+, +create+), there is no object to evaluate conditions against. To
240
+ # allow attribute checks on all actions, it is a common pattern to provide
241
+ # custom objects through +before_filters+:
242
+ # class BranchesController < ApplicationController
243
+ # before_filter :load_company
244
+ # before_filter :new_branch_from_company_and_params,
245
+ # :only => [:index, :new, :create]
246
+ # filter_access_to :all, :attribute_check => true
247
+ #
248
+ # protected
249
+ # def new_branch_from_company_and_params
250
+ # @branch = @company.branches.new(params[:branch])
251
+ # end
252
+ # end
253
+ # NOTE: +before_filters+ need to be defined before the first
254
+ # +filter_access_to+ call.
255
+ #
256
+ # For further customization, a custom filter expression may be formulated
257
+ # in a block, which is then evaluated in the context of the controller
258
+ # on a matching request. That is, for checking two objects, use the
259
+ # following:
260
+ # filter_access_to :merge do
261
+ # permitted_to!(:update, User.find(params[:original_id])) and
262
+ # permitted_to!(:delete, User.find(params[:id]))
263
+ # end
264
+ # The block should raise a Authorization::AuthorizationError or return
265
+ # false if the access is to be denied.
266
+ #
267
+ # Later calls to filter_access_to with overlapping actions overwrite
268
+ # previous ones for that action.
269
+ #
270
+ # All options:
271
+ # [:+require+]
272
+ # Privilege required; defaults to action_name
273
+ # [:+context+]
274
+ # The privilege's context, defaults to decl_auth_context, which consists
275
+ # of controller_name, prepended by any namespaces
276
+ # [:+attribute_check+]
277
+ # Enables the check of attributes defined in the authorization rules.
278
+ # Defaults to false. If enabled, filter_access_to will use a context
279
+ # object from one of the following sources (in that order):
280
+ # * the method from the :+load_method+ option,
281
+ # * an instance variable named after the singular of the context
282
+ # (by default from the controller name, e.g. @post for PostsController),
283
+ # * a find on the context model, using +params+[:id] as id value.
284
+ # Any of these methods will only be employed if :+attribute_check+
285
+ # is enabled.
286
+ # [:+model+]
287
+ # The data model to load a context object from. Defaults to the
288
+ # context, singularized.
289
+ # [:+load_method+]
290
+ # Specify a method by symbol or a Proc object which should be used
291
+ # to load the object. Both should return the loaded object.
292
+ # If a Proc object is given, e.g. by way of
293
+ # +lambda+, it is called in the instance of the controller.
294
+ # Example demonstrating the default behavior:
295
+ # filter_access_to :show, :attribute_check => true,
296
+ # :load_method => lambda { User.find(params[:id]) }
297
+ #
298
+
299
+ def filter_access_to (*args, &filter_block)
300
+ options = args.last.is_a?(Hash) ? args.pop : {}
301
+ options = {
302
+ :require => nil,
303
+ :context => nil,
304
+ :attribute_check => false,
305
+ :model => nil,
306
+ :load_method => nil,
307
+ :strong_parameters => nil
308
+ }.merge!(options)
309
+ privilege = options[:require]
310
+ context = options[:context]
311
+ actions = args.flatten
312
+
313
+ # prevent setting filter_access_filter multiple times
314
+ skip_before_filter :filter_access_filter
315
+ before_filter :filter_access_filter
316
+
317
+ filter_access_permissions.each do |perm|
318
+ perm.remove_actions(actions)
319
+ end
320
+ filter_access_permissions <<
321
+ ControllerPermission.new(actions, privilege, context,
322
+ options[:strong_parameters],
323
+ options[:attribute_check],
324
+ options[:model],
325
+ options[:load_method],
326
+ filter_block)
327
+ end
328
+
329
+ # Collecting all the ControllerPermission objects from the controller
330
+ # hierarchy. Permissions for actions are overwritten by calls to
331
+ # filter_access_to in child controllers with the same action.
332
+ def all_filter_access_permissions # :nodoc:
333
+ ancestors.inject([]) do |perms, mod|
334
+ if mod.respond_to?(:filter_access_permissions, true)
335
+ perms +
336
+ mod.filter_access_permissions.collect do |p1|
337
+ p1.clone.remove_actions(perms.inject(Set.new) {|actions, p2| actions + p2.actions})
338
+ end
339
+ else
340
+ perms
341
+ end
342
+ end
343
+ end
344
+
345
+ # To DRY up the filter_access_to statements in restful controllers,
346
+ # filter_resource_access combines typical filter_access_to and
347
+ # before_filter calls, which set up the instance variables.
348
+ #
349
+ # The simplest case are top-level resource controllers with only the
350
+ # seven CRUD methods, e.g.
351
+ # class CompanyController < ApplicationController
352
+ # filter_resource_access
353
+ #
354
+ # def index...
355
+ # end
356
+ # Here, all CRUD actions are protected through a filter_access_to :all
357
+ # statement. :+attribute_check+ is enabled for all actions except for
358
+ # the collection action :+index+. To have an object for attribute checks
359
+ # available, filter_resource_access will set the instance variable
360
+ # @+company+ in before filters. For the member actions (:+show+, :+edit+,
361
+ # :+update+, :+destroy+) @company is set to Company.find(params[:id]).
362
+ # For +new+ actions (:+new+, :+create+), filter_resource_access creates
363
+ # a new object from company parameters: Company.new(params[:company].
364
+ #
365
+ # For nested resources, the parent object may be loaded automatically.
366
+ # class BranchController < ApplicationController
367
+ # filter_resource_access :nested_in => :companies
368
+ # end
369
+ # Again, the CRUD actions are protected. Now, for all CRUD actions,
370
+ # the parent object @company is loaded from params[:company_id]. It is
371
+ # also used when creating @branch for +new+ actions. Here, attribute_check
372
+ # is enabled for the collection :+index+ as well, checking attributes on a
373
+ # @company.branches.new method.
374
+ #
375
+ # In many cases, the default seven CRUD actions are not sufficient. As in
376
+ # the resource definition for routing you may thus give additional member,
377
+ # new and collection methods. The +options+ allow you to specify the
378
+ # required privileges for each action by providing a hash or an array of
379
+ # pairs. By default, for each action the action name is taken as privilege
380
+ # (action search in the example below requires the privilege :index
381
+ # :companies). Any controller action that is not specified and does not
382
+ # belong to the seven CRUD actions is handled as a member method.
383
+ # class CompanyController < ApplicationController
384
+ # filter_resource_access :collection => [[:search, :index], :index],
385
+ # :additional_member => {:mark_as_key_company => :update}
386
+ # end
387
+ # The +additional_+* options add to the respective CRUD actions,
388
+ # the other options (:+member+, :+collection+, :+new+) replace their
389
+ # respective CRUD actions.
390
+ # filter_resource_access :member => { :toggle_open => :update }
391
+ # Would declare :toggle_open as the only member action in the controller and
392
+ # require that permission :update is granted for the current user.
393
+ # filter_resource_access :additional_member => { :toggle_open => :update }
394
+ # Would add a member action :+toggle_open+ to the default members, such as :+show+.
395
+ #
396
+ # If :+collection+ is an array of method names filter_resource_access will
397
+ # associate a permission with the method that is the same as the method
398
+ # name and no attribute checks will be performed unless
399
+ # :attribute_check => true
400
+ # is added in the options.
401
+ #
402
+ # You can override the default object loading by implementing any of the
403
+ # following instance methods on the controller. Examples are given for the
404
+ # BranchController (with +nested_in+ set to :+companies+):
405
+ # [+new_branch_from_params+]
406
+ # Used for +new+ actions.
407
+ # [+new_branch_for_collection+]
408
+ # Used for +collection+ actions if the +nested_in+ option is set.
409
+ # [+load_branch+]
410
+ # Used for +member+ actions.
411
+ # [+load_company+]
412
+ # Used for all +new+, +member+, and +collection+ actions if the
413
+ # +nested_in+ option is set.
414
+ #
415
+ # All options:
416
+ # [:+member+]
417
+ # Member methods are actions like +show+, which have an params[:id] from
418
+ # which to load the controller object and assign it to @controller_name,
419
+ # e.g. @+branch+.
420
+ #
421
+ # By default, member actions are [:+show+, :+edit+, :+update+,
422
+ # :+destroy+]. Also, any action not belonging to the seven CRUD actions
423
+ # are handled as member actions.
424
+ #
425
+ # There are three different syntax to specify member, collection and
426
+ # new actions.
427
+ # * Hash: Lets you set the required privilege for each action:
428
+ # {:+show+ => :+show+, :+mark_as_important+ => :+update+}
429
+ # * Array of actions or pairs: [:+show+, [:+mark_as_important+, :+update+]],
430
+ # with single actions requiring the privilege of the same name as the method.
431
+ # * Single method symbol: :+show+
432
+ # [:+additional_member+]
433
+ # Allows to add additional member actions to the default resource +member+
434
+ # actions.
435
+ # [:+collection+]
436
+ # Collection actions are like :+index+, actions without any controller object
437
+ # to check attributes of. If +nested_in+ is given, a new object is
438
+ # created from the parent object, e.g. @company.branches.new. Without
439
+ # +nested_in+, attribute check is deactivated for these actions. By
440
+ # default, collection is set to :+index+.
441
+ # [:+additional_collection+]
442
+ # Allows to add additional collection actions to the default resource +collection+
443
+ # actions.
444
+ # [:+new+]
445
+ # +new+ methods are actions such as +new+ and +create+, which don't
446
+ # receive a params[:id] to load an object from, but
447
+ # a params[:controller_name_singular] hash with attributes for a new
448
+ # object. The attributes will be used here to create a new object and
449
+ # check the object against the authorization rules. The object is
450
+ # assigned to @controller_name_singular, e.g. @branch.
451
+ #
452
+ # If +nested_in+ is given, the new object
453
+ # is created from the parent_object.controller_name
454
+ # proxy, e.g. company.branches.new(params[:branch]). By default,
455
+ # +new+ is set to [:new, :create].
456
+ # [:+additional_new+]
457
+ # Allows to add additional new actions to the default resource +new+ actions.
458
+ # [:+context+]
459
+ # The context is used to determine the model to load objects from for the
460
+ # before_filters and the context of privileges to use in authorization
461
+ # checks.
462
+ # [:+nested_in+]
463
+ # Specifies the parent controller if the resource is nested in another
464
+ # one. This is used to automatically load the parent object, e.g.
465
+ # @+company+ from params[:company_id] for a BranchController nested in
466
+ # a CompanyController.
467
+ # [:+shallow+]
468
+ # Only relevant when used in conjunction with +nested_in+. Specifies a nested resource
469
+ # as being a shallow nested resource, resulting in the controller not attempting to
470
+ # load a parent object for all member actions defined by +member+ and
471
+ # +additional_member+ or rather the default member actions (:+show+, :+edit+,
472
+ # :+update+, :+destroy+).
473
+ # [:+no_attribute_check+]
474
+ # Allows to set actions for which no attribute check should be performed.
475
+ # See filter_access_to on details. By default, with no +nested_in+,
476
+ # +no_attribute_check+ is set to all collections. If +nested_in+ is given
477
+ # +no_attribute_check+ is empty by default.
478
+ # [:+strong_parameters+]
479
+ # If set to true, relies on controller to provide instance variable and
480
+ # create new object in :create action. Set true if you use strong_params
481
+ # and false if you use protected_attributes.
482
+ #
483
+ def filter_resource_access(options = {})
484
+ options = {
485
+ :new => [:new, :create],
486
+ :additional_new => nil,
487
+ :member => [:show, :edit, :update, :destroy],
488
+ :additional_member => nil,
489
+ :collection => [:index],
490
+ :additional_collection => nil,
491
+ #:new_method_for_collection => nil, # only symbol method name
492
+ #:new_method => nil, # only symbol method name
493
+ #:load_method => nil, # only symbol method name
494
+ :no_attribute_check => nil,
495
+ :context => nil,
496
+ :model => nil,
497
+ :nested_in => nil,
498
+ :strong_parameters => nil
499
+ }.merge(options)
500
+ options.merge!({ :strong_parameters => true }) if Rails.version >= '4' && options[:strong_parameters] == nil
501
+ options.merge!({ :strong_parameters => false }) if Rails.version < '4' && options[:strong_parameters] == nil
502
+
503
+ new_actions = actions_from_option( options[:new] ).merge(
504
+ actions_from_option(options[:additional_new]) )
505
+ members = actions_from_option(options[:member]).merge(
506
+ actions_from_option(options[:additional_member]))
507
+ collections = actions_from_option(options[:collection]).merge(
508
+ actions_from_option(options[:additional_collection]))
509
+
510
+ no_attribute_check_actions = options[:strong_parameters] ? collections.merge(actions_from_option([:create])) : collections
511
+
512
+ options[:no_attribute_check] ||= no_attribute_check_actions.keys unless options[:nested_in]
513
+
514
+ unless options[:nested_in].blank?
515
+ load_parent_method = :"load_#{options[:nested_in].to_s.singularize}"
516
+ shallow_exceptions = options[:shallow] ? {:except => members.keys} : {}
517
+ before_filter shallow_exceptions do |controller|
518
+ if controller.respond_to?(load_parent_method, true)
519
+ controller.send(load_parent_method)
520
+ else
521
+ controller.send(:load_parent_controller_object, options[:nested_in])
522
+ end
523
+ end
524
+
525
+ new_for_collection_method = :"new_#{controller_name.singularize}_for_collection"
526
+ before_filter :only => collections.keys do |controller|
527
+ # new_for_collection
528
+ if controller.respond_to?(new_for_collection_method, true)
529
+ controller.send(new_for_collection_method)
530
+ else
531
+ controller.send(:new_controller_object_for_collection,
532
+ options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
533
+ end
534
+ end
535
+ end
536
+
537
+ unless options[:strong_parameters]
538
+ new_from_params_method = :"new_#{controller_name.singularize}_from_params"
539
+ before_filter :only => new_actions.keys do |controller|
540
+ # new_from_params
541
+ if controller.respond_to?(new_from_params_method, true)
542
+ controller.send(new_from_params_method)
543
+ else
544
+ controller.send(:new_controller_object_from_params,
545
+ options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
546
+ end
547
+ end
548
+ else
549
+ new_object_method = :"new_#{controller_name.singularize}"
550
+ before_filter :only => :new do |controller|
551
+ # new_from_params
552
+ if controller.respond_to?(new_object_method, true)
553
+ controller.send(new_object_method)
554
+ else
555
+ controller.send(:new_blank_controller_object,
556
+ options[:context] || controller_name, options[:nested_in], options[:strong_parameters], options[:model])
557
+ end
558
+ end
559
+ end
560
+
561
+ load_method = :"load_#{controller_name.singularize}"
562
+ before_filter :only => members.keys do |controller|
563
+ # load controller object
564
+ if controller.respond_to?(load_method, true)
565
+ controller.send(load_method)
566
+ else
567
+ controller.send(:load_controller_object, options[:context] || controller_name, options[:model])
568
+ end
569
+ end
570
+ filter_access_to :all, :attribute_check => true, :context => options[:context], :model => options[:model]
571
+
572
+ members.merge(new_actions).merge(collections).each do |action, privilege|
573
+ if action != privilege or (options[:no_attribute_check] and options[:no_attribute_check].include?(action))
574
+ filter_options = {
575
+ :strong_parameters => options[:strong_parameters],
576
+ :context => options[:context],
577
+ :attribute_check => !options[:no_attribute_check] || !options[:no_attribute_check].include?(action),
578
+ :model => options[:model]
579
+ }
580
+ filter_options[:require] = privilege if action != privilege
581
+ filter_access_to(action, filter_options)
582
+ end
583
+ end
584
+ end
585
+
586
+ # Returns the context for authorization checks in the current controller.
587
+ # Uses the controller_name and prepends any namespaces underscored and
588
+ # joined with underscores.
589
+ #
590
+ # E.g.
591
+ # AllThosePeopleController => :all_those_people
592
+ # AnyName::Space::ThingsController => :any_name_space_things
593
+ #
594
+ def decl_auth_context
595
+ prefixes = name.split('::')[0..-2].map(&:underscore)
596
+ ((prefixes + [controller_name]) * '_').to_sym
597
+ end
598
+
599
+ protected
600
+ def filter_access_permissions # :nodoc:
601
+ unless filter_access_permissions?
602
+ ancestors[1..-1].reverse.each do |mod|
603
+ mod.filter_access_permissions if mod.respond_to?(:filter_access_permissions, true)
604
+ end
605
+ end
606
+ class_variable_set(:@@declarative_authorization_permissions, {}) unless filter_access_permissions?
607
+ class_variable_get(:@@declarative_authorization_permissions)[self.name] ||= []
608
+ end
609
+
610
+ def filter_access_permissions? # :nodoc:
611
+ class_variable_defined?(:@@declarative_authorization_permissions)
612
+ end
613
+
614
+ def actions_from_option (option) # :nodoc:
615
+ case option
616
+ when nil
617
+ {}
618
+ when Symbol, String
619
+ {option.to_sym => option.to_sym}
620
+ when Hash
621
+ option
622
+ when Enumerable
623
+ option.each_with_object({}) do |action, hash|
624
+ if action.is_a?(Array)
625
+ raise "Unexpected option format: #{option.inspect}" if action.length != 2
626
+ hash[action.first] = action.last
627
+ else
628
+ hash[action.to_sym] = action.to_sym
629
+ end
630
+ end
631
+ end
632
+ end
633
+ end
634
+ end
635
+
636
+ class ControllerPermission # :nodoc:
637
+ attr_reader :actions, :privilege, :context, :attribute_check, :strong_params
638
+ def initialize (actions, privilege, context, strong_params, attribute_check = false,
639
+ load_object_model = nil, load_object_method = nil,
640
+ filter_block = nil)
641
+ @actions = actions.to_set
642
+ @privilege = privilege
643
+ @context = context
644
+ @load_object_model = load_object_model
645
+ @load_object_method = load_object_method
646
+ @filter_block = filter_block
647
+ @attribute_check = attribute_check
648
+ @strong_params = strong_params
649
+ end
650
+
651
+ def matches? (action_name)
652
+ @actions.include?(action_name.to_sym)
653
+ end
654
+
655
+ def permit! (contr)
656
+ if @filter_block
657
+ return contr.instance_eval(&@filter_block)
658
+ end
659
+ object = @attribute_check ? load_object(contr) : nil
660
+ privilege = @privilege || :"#{contr.action_name}"
661
+
662
+ contr.authorization_engine.permit!(privilege,
663
+ :user => contr.send(:current_user),
664
+ :object => object,
665
+ :skip_attribute_test => !@attribute_check,
666
+ :context => @context || contr.class.decl_auth_context)
667
+ end
668
+
669
+ def remove_actions (actions)
670
+ @actions -= actions
671
+ self
672
+ end
673
+
674
+ private
675
+
676
+ def load_object(contr)
677
+ if @load_object_method and @load_object_method.is_a?(Symbol)
678
+ contr.send(@load_object_method)
679
+ elsif @load_object_method and @load_object_method.is_a?(Proc)
680
+ contr.instance_eval(&@load_object_method)
681
+ else
682
+ load_object_model = @load_object_model ||
683
+ (@context ? @context.to_s.classify.constantize : contr.class.controller_name.classify.constantize)
684
+ load_object_model = load_object_model.classify.constantize if load_object_model.is_a?(String)
685
+ instance_var = "@#{load_object_model.name.demodulize.underscore}"
686
+ object = contr.instance_variable_get(instance_var)
687
+ unless object
688
+ begin
689
+ object = @strong_params ? load_object_model.find_or_initialize_by(:id => contr.params[:id]) : load_object_model.find(contr.params[:id])
690
+ rescue => e
691
+ contr.logger.debug("filter_access_to tried to find " +
692
+ "#{load_object_model} from params[:id] " +
693
+ "(#{contr.params[:id].inspect}), because attribute_check is enabled " +
694
+ "and #{instance_var.to_s} isn't set, but failed: #{e.class.name}: #{e}")
695
+ raise if AuthorizationInController.failed_auto_loading_is_not_found?
696
+ end
697
+ contr.instance_variable_set(instance_var, object)
698
+ end
699
+ object
700
+ end
701
+ end
702
+ end
703
+ end