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/CHANGELOG +2 -0
- data/Rakefile +8 -0
- data/app/controllers/authorization_rules_controller.rb +103 -0
- data/app/controllers/authorization_usages_controller.rb +19 -0
- data/app/helpers/authorization_rules_helper.rb +84 -0
- data/app/views/authorization_rules/graph.dot.erb +49 -0
- data/app/views/authorization_rules/graph.html.erb +39 -0
- data/app/views/authorization_rules/index.html.erb +15 -0
- data/app/views/authorization_usages/index.html.erb +45 -0
- data/config/routes.rb +6 -0
- data/lib/authorization.rb +514 -0
- data/lib/helper.rb +51 -0
- data/lib/in_controller.rb +311 -0
- data/lib/in_model.rb +130 -0
- data/lib/maintenance.rb +174 -0
- data/lib/obligation_scope.rb +281 -0
- data/lib/rails_legacy.rb +14 -0
- data/lib/reader.rb +391 -0
- data/test/authorization_test.rb +576 -0
- data/test/controller_test.rb +361 -0
- data/test/dsl_reader_test.rb +157 -0
- data/test/helper_test.rb +96 -0
- data/test/maintenance_test.rb +15 -0
- data/test/model_test.rb +794 -0
- data/test/schema.sql +32 -0
- data/test/test_helper.rb +99 -0
- metadata +26 -2
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
|