zeiv-declarative_authorization 1.0.0.pre

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