stffn-declarative_authorization 0.2.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/helper.rb ADDED
@@ -0,0 +1,51 @@
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
+ end
51
+ end
@@ -0,0 +1,311 @@
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
+ protected
73
+ def filter_access_filter # :nodoc:
74
+ permissions = self.class.all_filter_access_permissions
75
+ all_permissions = permissions.select {|p| p.actions.include?(:all)}
76
+ matching_permissions = permissions.select {|p| p.matches?(action_name)}
77
+ allowed = false
78
+ auth_exception = nil
79
+ begin
80
+ allowed = if !matching_permissions.empty?
81
+ matching_permissions.all? {|perm| perm.permit!(self)}
82
+ elsif !all_permissions.empty?
83
+ all_permissions.all? {|perm| perm.permit!(self)}
84
+ else
85
+ !DEFAULT_DENY
86
+ end
87
+ rescue AuthorizationError => e
88
+ auth_exception = e
89
+ end
90
+
91
+ unless allowed
92
+ if all_permissions.empty? and matching_permissions.empty?
93
+ logger.warn "Permission denied: No matching filter access " +
94
+ "rule found for #{self.class.controller_name}.#{action_name}"
95
+ elsif auth_exception
96
+ logger.info "Permission denied: #{auth_exception}"
97
+ end
98
+ if respond_to?(:permission_denied)
99
+ # permission_denied needs to render or redirect
100
+ send(:permission_denied)
101
+ else
102
+ send(:render, :text => "You are not allowed to access this action.",
103
+ :status => :forbidden)
104
+ end
105
+ end
106
+ end
107
+
108
+ module ClassMethods
109
+ #
110
+ # Defines a filter to be applied according to the authorization of the
111
+ # current user. Requires at least one symbol corresponding to an
112
+ # action as parameter. The special symbol :+all+ refers to all action.
113
+ # The all :+all+ statement is only employed if no specific statement is
114
+ # present.
115
+ # class UserController < ActionController
116
+ # filter_access_to :index
117
+ # filter_access_to :new, :edit
118
+ # filter_access_to :all
119
+ # ...
120
+ # end
121
+ #
122
+ # The default is to allow access unconditionally if no rule matches.
123
+ # Thus, including the +filter_access_to+ :+all+ statement is a good
124
+ # idea, implementing a default-deny policy.
125
+ #
126
+ # When the access is denied, the method +permission_denied+ is called
127
+ # on the current controller, if defined. Else, a simple "you are not
128
+ # allowed" string is output. Log.info is given more information on the
129
+ # reasons of denial.
130
+ #
131
+ # def permission_denied
132
+ # flash[:error] = 'Sorry, you are not allowed to the requested page.'
133
+ # respond_to do |format|
134
+ # format.html { redirect_to(:back) rescue redirect_to('/') }
135
+ # format.xml { head :unauthorized }
136
+ # format.js { head :unauthorized }
137
+ # end
138
+ # end
139
+ #
140
+ # By default, required privileges are infered from the action name and
141
+ # the controller name. Thus, in UserController :+edit+ requires
142
+ # :+edit+ +users+. To specify required privilege, use the option :+require+
143
+ # filter_access_to :new, :create, :require => :create, :context => :users
144
+ #
145
+ # For further customization, a custom filter expression may be formulated
146
+ # in a block, which is then evaluated in the context of the controller
147
+ # on a matching request. That is, for checking two objects, use the
148
+ # following:
149
+ # filter_access_to :merge do
150
+ # permitted_to!(:update, User.find(params[:original_id])) and
151
+ # permitted_to!(:delete, User.find(params[:id]))
152
+ # end
153
+ # The block should raise a Authorization::AuthorizationError or return
154
+ # false if the access is to be denied.
155
+ #
156
+ # Later calls to filter_access_to with overlapping actions overwrite
157
+ # previous ones for that action.
158
+ #
159
+ # All options:
160
+ # [:+require+]
161
+ # Privilege required; defaults to action_name
162
+ # [:+context+]
163
+ # The privilege's context, defaults to controller_name, pluralized.
164
+ # [:+attribute_check+]
165
+ # Enables the check of attributes defined in the authorization rules.
166
+ # Defaults to false. If enabled, filter_access_to will try to load
167
+ # a context object employing either
168
+ # * the method from the :+load_method+ option or
169
+ # * a find on the context model, using +params+[:id] as id value.
170
+ # Any of these loading methods will only be employed if :+attribute_check+
171
+ # is enabled.
172
+ # [:+model+]
173
+ # The data model to load a context object from. Defaults to the
174
+ # context, singularized.
175
+ # [:+load_method+]
176
+ # Specify a method by symbol or a Proc object which should be used
177
+ # to load the object. Both should return the loaded object.
178
+ # If a Proc object is given, e.g. by way of
179
+ # +lambda+, it is called in the instance of the controller.
180
+ # Example demonstrating the default behaviour:
181
+ # filter_access_to :show, :attribute_check => true,
182
+ # :load_method => lambda { User.find(params[:id]) }
183
+ #
184
+
185
+ def filter_access_to (*args, &filter_block)
186
+ options = args.last.is_a?(Hash) ? args.pop : {}
187
+ options = {
188
+ :require => nil,
189
+ :context => nil,
190
+ :attribute_check => false,
191
+ :model => nil,
192
+ :load_method => nil
193
+ }.merge!(options)
194
+ privilege = options[:require]
195
+ context = options[:context]
196
+ actions = args.flatten
197
+
198
+ # collect permits in controller array for use in one before_filter
199
+ unless filter_chain.any? {|filter| filter.method == :filter_access_filter}
200
+ before_filter :filter_access_filter
201
+ end
202
+
203
+ filter_access_permissions.each do |perm|
204
+ perm.remove_actions(actions)
205
+ end
206
+ filter_access_permissions <<
207
+ ControllerPermission.new(actions, privilege, context,
208
+ options[:attribute_check],
209
+ options[:model],
210
+ options[:load_method],
211
+ filter_block)
212
+ end
213
+
214
+ # Collecting all the ControllerPermission objects from the controller
215
+ # hierarchy. Permissions for actions are overwritten by calls to
216
+ # filter_access_to in child controllers with the same action.
217
+ def all_filter_access_permissions # :nodoc:
218
+ ancestors.inject([]) do |perms, mod|
219
+ if mod.respond_to?(:filter_access_permissions)
220
+ perms +
221
+ mod.filter_access_permissions.collect do |p1|
222
+ p1.clone.remove_actions(perms.inject(Set.new) {|actions, p2| actions + p2.actions})
223
+ end
224
+ else
225
+ perms
226
+ end
227
+ end
228
+ end
229
+
230
+ protected
231
+ def filter_access_permissions # :nodoc:
232
+ unless filter_access_permissions?
233
+ ancestors[1..-1].reverse.each do |mod|
234
+ mod.filter_access_permissions if mod.respond_to?(:filter_access_permissions)
235
+ end
236
+ end
237
+ class_variable_set(:@@declarative_authorization_permissions, {}) unless filter_access_permissions?
238
+ class_variable_get(:@@declarative_authorization_permissions)[self.name] ||= []
239
+ end
240
+
241
+ def filter_access_permissions? # :nodoc:
242
+ class_variable_defined?(:@@declarative_authorization_permissions)
243
+ end
244
+ end
245
+ end
246
+
247
+ class ControllerPermission # :nodoc:
248
+ attr_reader :actions, :privilege, :context, :attribute_check
249
+ def initialize (actions, privilege, context, attribute_check = false,
250
+ load_object_model = nil, load_object_method = nil,
251
+ filter_block = nil)
252
+ @actions = actions.to_set
253
+ @privilege = privilege
254
+ @context = context
255
+ @load_object_model = load_object_model
256
+ @load_object_method = load_object_method
257
+ @filter_block = filter_block
258
+ @attribute_check = attribute_check
259
+ end
260
+
261
+ def matches? (action_name)
262
+ @actions.include?(action_name.to_sym)
263
+ end
264
+
265
+ def permit! (contr)
266
+ if @filter_block
267
+ return contr.instance_eval(&@filter_block)
268
+ end
269
+ context = @context || contr.class.controller_name.to_sym
270
+ object = @attribute_check ? load_object(contr, context) : nil
271
+ privilege = @privilege || :"#{contr.action_name}"
272
+
273
+ #puts "Trying permit?(#{privilege.inspect}, "
274
+ #puts " :user => #{contr.send(:current_user).inspect}, "
275
+ #puts " :object => #{object.inspect},"
276
+ #puts " :skip_attribute_test => #{!@attribute_check},"
277
+ #puts " :context => #{contr.class.controller_name.pluralize.to_sym})"
278
+ res = contr.authorization_engine.permit!(privilege,
279
+ :user => contr.send(:current_user),
280
+ :object => object,
281
+ :skip_attribute_test => !@attribute_check,
282
+ :context => context)
283
+ #puts "permit? result: #{res.inspect}"
284
+ res
285
+ end
286
+
287
+ def remove_actions (actions)
288
+ @actions -= actions
289
+ self
290
+ end
291
+
292
+ private
293
+ def load_object(contr, context)
294
+ if @load_object_method and @load_object_method.is_a?(Symbol)
295
+ contr.send(@load_object_method)
296
+ elsif @load_object_method and @load_object_method.is_a?(Proc)
297
+ contr.instance_eval(&@load_object_method)
298
+ else
299
+ load_object_model = @load_object_model || context.to_s.classify.constantize
300
+ instance_var = :"@#{load_object_model.name.underscore}"
301
+ object = contr.instance_variable_get(instance_var)
302
+ unless object
303
+ # catch ActiveRecord::RecordNotFound?
304
+ object = load_object_model.find(contr.params[:id])
305
+ contr.instance_variable_set(instance_var, object)
306
+ end
307
+ object
308
+ end
309
+ end
310
+ end
311
+ end
data/lib/in_model.rb ADDED
@@ -0,0 +1,130 @@
1
+ # Authorization::AuthorizationInModel
2
+ require File.dirname(__FILE__) + '/authorization.rb'
3
+ require File.dirname(__FILE__) + '/obligation_scope.rb'
4
+
5
+ module Authorization
6
+
7
+ module AuthorizationInModel
8
+
9
+ def self.included(base) # :nodoc:
10
+ #base.extend(ClassMethods)
11
+ base.module_eval do
12
+ scopes[:with_permissions_to] = lambda do |parent_scope, *args|
13
+ options = args.last.is_a?(Hash) ? args.pop : {}
14
+ privilege = (args[0] || :read).to_sym
15
+ privileges = [privilege]
16
+ context = options[:context] || :"#{parent_scope.table_name}"
17
+
18
+ user = options[:user] || Authorization.current_user
19
+
20
+ engine = Authorization::Engine.instance
21
+ engine.permit!(privileges, :user => user, :skip_attribute_test => true,
22
+ :context => context)
23
+
24
+ obligation_scope_for( privileges, :user => user,
25
+ :context => context, :engine => engine, :model => parent_scope)
26
+ end
27
+
28
+ # Builds and returns a scope with joins and conditions satisfying all obligations.
29
+ def self.obligation_scope_for( privileges, options = {} )
30
+ options = {
31
+ :user => Authorization.current_user,
32
+ :context => nil,
33
+ :model => self,
34
+ :engine => nil,
35
+ }.merge(options)
36
+ engine ||= Authorization::Engine.instance
37
+
38
+ scope = ObligationScope.new( options[:model], {} )
39
+ engine.obligations( privileges, :user => options[:user], :context => options[:context] ).each do |obligation|
40
+ scope.parse!( obligation )
41
+ end
42
+ scope
43
+ end
44
+
45
+ # Named scope for limiting query results according to the authorization
46
+ # of the current user. If no privilege is given, :+read+ is assumed.
47
+ #
48
+ # User.with_permissions_to
49
+ # User.with_permissions_to(:update)
50
+ # User.with_permissions_to(:update, :context => :users)
51
+ #
52
+ # As in the case of other named scopes, this one may be chained:
53
+ # User.with_permission_to.find(:all, :conditions...)
54
+ #
55
+ # Options
56
+ # [:+context+]
57
+ # Context for the privilege to be evaluated in; defaults to the
58
+ # model's table name.
59
+ # [:+user+]
60
+ # User to be used for gathering obligations; defaults to the
61
+ # current user.
62
+ #
63
+ def self.with_permissions_to (*args)
64
+ scopes[:with_permissions_to].call(self, *args)
65
+ end
66
+
67
+ # Activates model security for the current model. Then, CRUD operations
68
+ # are checked against the authorization of the current user. The
69
+ # privileges are :+create+, :+read+, :+update+ and :+delete+ in the
70
+ # context of the model. By default, :+read+ is not checked because of
71
+ # performance impacts, especially with large result sets.
72
+ #
73
+ # class User < ActiveRecord::Base
74
+ # using_access_control
75
+ # end
76
+ #
77
+ # If an operation is not permitted, a Authorization::AuthorizationError
78
+ # is raised.
79
+ #
80
+ # Available options
81
+ # [:+context+] Specify context different from the models table name.
82
+ # [:+include_read+] Also check for :+read+ privilege after find.
83
+ #
84
+ def self.using_access_control (options = {})
85
+ options = {
86
+ :context => nil,
87
+ :include_read => false
88
+ }.merge(options)
89
+ context = (options[:context] || self.table_name).to_sym
90
+
91
+ class_eval do
92
+ before_create do |object|
93
+ Authorization::Engine.instance.permit!(:create, :object => object,
94
+ :context => context)
95
+ end
96
+
97
+ before_update do |object|
98
+ Authorization::Engine.instance.permit!(:update, :object => object,
99
+ :context => context)
100
+ end
101
+
102
+ before_destroy do |object|
103
+ Authorization::Engine.instance.permit!(:delete, :object => object,
104
+ :context => context)
105
+ end
106
+
107
+ # only called if after_find is implemented
108
+ after_find do |object|
109
+ Authorization::Engine.instance.permit!(:read, :object => object,
110
+ :context => context)
111
+ end
112
+
113
+ if options[:include_read]
114
+ def after_find; end
115
+ end
116
+
117
+ def self.using_access_control?
118
+ true
119
+ end
120
+ end
121
+ end
122
+
123
+ # Returns true if the model is using model security.
124
+ def self.using_access_control?
125
+ false
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end