stffn-declarative_authorization 0.2.1 → 0.2.3

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