declarative_authorization-dta 0.1
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/CHANGELOG +148 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +504 -0
- data/Rakefile +35 -0
- data/app/controllers/authorization_rules_controller.rb +259 -0
- data/app/controllers/authorization_usages_controller.rb +23 -0
- data/app/helpers/authorization_rules_helper.rb +218 -0
- data/app/views/authorization_rules/_change.erb +58 -0
- data/app/views/authorization_rules/_show_graph.erb +37 -0
- data/app/views/authorization_rules/_suggestions.erb +48 -0
- data/app/views/authorization_rules/change.html.erb +169 -0
- data/app/views/authorization_rules/graph.dot.erb +68 -0
- data/app/views/authorization_rules/graph.html.erb +40 -0
- data/app/views/authorization_rules/index.html.erb +17 -0
- data/app/views/authorization_usages/index.html.erb +36 -0
- data/authorization_rules.dist.rb +20 -0
- data/config/routes.rb +10 -0
- data/garlic_example.rb +20 -0
- data/init.rb +5 -0
- data/lib/declarative_authorization.rb +17 -0
- data/lib/declarative_authorization/authorization.rb +687 -0
- data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
- data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
- data/lib/declarative_authorization/development_support/change_supporter.rb +620 -0
- data/lib/declarative_authorization/development_support/development_support.rb +243 -0
- data/lib/declarative_authorization/helper.rb +60 -0
- data/lib/declarative_authorization/in_controller.rb +623 -0
- data/lib/declarative_authorization/in_model.new.rb +298 -0
- data/lib/declarative_authorization/in_model.rb +463 -0
- data/lib/declarative_authorization/maintenance.rb +212 -0
- data/lib/declarative_authorization/obligation_scope.rb +354 -0
- data/lib/declarative_authorization/rails_legacy.rb +22 -0
- data/lib/declarative_authorization/railsengine.rb +6 -0
- data/lib/declarative_authorization/reader.rb +521 -0
- data/lib/tasks/authorization_tasks.rake +82 -0
- data/test/authorization_test.rb +1065 -0
- data/test/controller_filter_resource_access_test.rb +511 -0
- data/test/controller_test.rb +465 -0
- data/test/dsl_reader_test.rb +178 -0
- data/test/helper_test.rb +172 -0
- data/test/maintenance_test.rb +46 -0
- data/test/model_test.rb +2216 -0
- data/test/schema.sql +62 -0
- data/test/test_helper.rb +152 -0
- metadata +108 -0
@@ -0,0 +1,298 @@
|
|
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
|
+
# If the user meets the given privilege, permitted_to? returns true
|
10
|
+
# and yields to the optional block.
|
11
|
+
def permitted_to? (privilege, options = {}, &block)
|
12
|
+
options = {
|
13
|
+
:user => Authorization.current_user,
|
14
|
+
:object => self
|
15
|
+
}.merge(options)
|
16
|
+
Authorization::Engine.instance.permit?(privilege,
|
17
|
+
{:user => options[:user],
|
18
|
+
:object => options[:object]},
|
19
|
+
&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Works similar to the permitted_to? method, but doesn't accept a block
|
23
|
+
# and throws the authorization exceptions, just like Engine#permit!
|
24
|
+
def permitted_to! (privilege, options = {} )
|
25
|
+
options = {
|
26
|
+
:user => Authorization.current_user,
|
27
|
+
:object => self
|
28
|
+
}.merge(options)
|
29
|
+
Authorization::Engine.instance.permit!(privilege,
|
30
|
+
{:user => options[:user],
|
31
|
+
:object => options[:object]})
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.included(base) # :nodoc:
|
35
|
+
#base.extend(ClassMethods)
|
36
|
+
base.module_eval do
|
37
|
+
scopes[:with_permissions_to] = lambda do |parent_scope, *args|
|
38
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
39
|
+
privilege = (args[0] || :read).to_sym
|
40
|
+
privileges = [privilege]
|
41
|
+
context =
|
42
|
+
if options[:context]
|
43
|
+
options[:context]
|
44
|
+
elsif parent_scope.respond_to?(:proxy_reflection)
|
45
|
+
parent_scope.proxy_reflection.klass.name.tableize.to_sym
|
46
|
+
elsif parent_scope.respond_to?(:decl_auth_context)
|
47
|
+
parent_scope.decl_auth_context
|
48
|
+
else
|
49
|
+
parent_scope.name.tableize.to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
user = options[:user] || Authorization.current_user
|
53
|
+
|
54
|
+
engine = options[:engine] || Authorization::Engine.instance
|
55
|
+
engine.permit!(privileges, :user => user, :skip_attribute_test => true,
|
56
|
+
:context => context)
|
57
|
+
|
58
|
+
obligation_scope_for( privileges, :user => user,
|
59
|
+
:context => context, :engine => engine, :model => parent_scope)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Builds and returns a scope with joins and conditions satisfying all obligations.
|
63
|
+
def self.obligation_scope_for( privileges, options = {} )
|
64
|
+
options = {
|
65
|
+
:user => Authorization.current_user,
|
66
|
+
:context => nil,
|
67
|
+
:model => self,
|
68
|
+
:engine => nil,
|
69
|
+
}.merge(options)
|
70
|
+
engine = options[:engine] || Authorization::Engine.instance
|
71
|
+
|
72
|
+
obligation_scope = ObligationScope.new( options[:model], {} )
|
73
|
+
engine.obligations( privileges, :user => options[:user], :context => options[:context] ).each do |obligation|
|
74
|
+
obligation_scope.parse!( obligation )
|
75
|
+
end
|
76
|
+
|
77
|
+
obligation_scope.scope
|
78
|
+
end
|
79
|
+
|
80
|
+
# Named scope for limiting query results according to the authorization
|
81
|
+
# of the current user. If no privilege is given, :+read+ is assumed.
|
82
|
+
#
|
83
|
+
# User.with_permissions_to
|
84
|
+
# User.with_permissions_to(:update)
|
85
|
+
# User.with_permissions_to(:update, :context => :users)
|
86
|
+
#
|
87
|
+
# As in the case of other named scopes, this one may be chained:
|
88
|
+
# User.with_permission_to.find(:all, :conditions...)
|
89
|
+
#
|
90
|
+
# Options
|
91
|
+
# [:+context+]
|
92
|
+
# Context for the privilege to be evaluated in; defaults to the
|
93
|
+
# model's table name.
|
94
|
+
# [:+user+]
|
95
|
+
# User to be used for gathering obligations; defaults to the
|
96
|
+
# current user.
|
97
|
+
#
|
98
|
+
def self.with_permissions_to (*args)
|
99
|
+
scopes[:with_permissions_to].call(self, *args)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Activates model security for the current model. Then, CRUD operations
|
103
|
+
# are checked against the authorization of the current user. The
|
104
|
+
# privileges are :+create+, :+read+, :+update+ and :+delete+ in the
|
105
|
+
# context of the model. By default, :+read+ is not checked because of
|
106
|
+
# performance impacts, especially with large result sets.
|
107
|
+
#
|
108
|
+
# class User < ActiveRecord::Base
|
109
|
+
# using_access_control
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# If an operation is not permitted, a Authorization::AuthorizationError
|
113
|
+
# is raised.
|
114
|
+
#
|
115
|
+
# To activate model security on all models, call using_access_control
|
116
|
+
# on ActiveRecord::Base
|
117
|
+
# ActiveRecord::Base.using_access_control
|
118
|
+
#
|
119
|
+
# Available options
|
120
|
+
# [:+context+] Specify context different from the models table name.
|
121
|
+
# [:+include_read+] Also check for :+read+ privilege after find.
|
122
|
+
#
|
123
|
+
def self.using_access_control (options = {})
|
124
|
+
options = {
|
125
|
+
:context => nil,
|
126
|
+
:include_read => false
|
127
|
+
}.merge(options)
|
128
|
+
|
129
|
+
class_eval do
|
130
|
+
[:create, :update, [:destroy, :delete]].each do |action, privilege|
|
131
|
+
send(:"before_#{action}") do |object|
|
132
|
+
Authorization::Engine.instance.permit!(privilege || action,
|
133
|
+
:object => object, :context => options[:context])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if options[:include_read]
|
138
|
+
# after_find is only called if after_find is implemented
|
139
|
+
after_find do |object|
|
140
|
+
Authorization::Engine.instance.permit!(:read, :object => object,
|
141
|
+
:context => options[:context])
|
142
|
+
end
|
143
|
+
#Patch - Taking it down to attribute level
|
144
|
+
|
145
|
+
#Protect ActiveRecord-attribute
|
146
|
+
# By calling this method, the ActiveRecord-attribute "name" is protected
|
147
|
+
# * reading requires :read_name or :read
|
148
|
+
# * writing requires :write_name or :write
|
149
|
+
# This method is called for all ActiveRecord attributes by default - in contrast to def protect_attribute(name) this methods
|
150
|
+
# uses def read_attribute(:name) and write_attribute(:name,value) as fall-backs
|
151
|
+
def self.protect_ar_attribute(name)
|
152
|
+
#Alias old methods (if no alias is available), override methods
|
153
|
+
class_eval <<-EOL
|
154
|
+
alias_method :no_acl_#{name}, :#{name} if respond_to?(:#{name}) && !respond_to(:no_acl_#{name})
|
155
|
+
alias_method :no_acl_#{name}=, :#{name}= if respond_to?(:#{name}=) && !respond_to(:no_acl_#{name}=)
|
156
|
+
def #{name}()
|
157
|
+
permitted_to!(:read_#{name}) if !permitted_to?(:read)
|
158
|
+
if(respond_to?(:no_acl_#{name}))
|
159
|
+
return send(:no_acl_#{name})
|
160
|
+
else
|
161
|
+
return read_attribute(:no_acl_#{name})
|
162
|
+
end
|
163
|
+
end
|
164
|
+
def #{name}=(v)
|
165
|
+
permitted_to!(:read_#{write}) if !permitted_to?(:write)
|
166
|
+
if(respond_to?(:no_acl_#{name}=))
|
167
|
+
return send(:no_acl_#{name}=,v)
|
168
|
+
else
|
169
|
+
return write_attribute(:no_acl_#{name})
|
170
|
+
end
|
171
|
+
end
|
172
|
+
EOL
|
173
|
+
end
|
174
|
+
|
175
|
+
#Protect attribute
|
176
|
+
# By calling this method, the attribute "name" is protected
|
177
|
+
# * reading requires :read_name or :read
|
178
|
+
# * writing requires :write_name or :write
|
179
|
+
def protect_attribute(name)
|
180
|
+
#Protecting an attributes means protecting its setter and getters
|
181
|
+
protect_method(name,:read)
|
182
|
+
protect_method("#{name}=",:write)
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
#Protect method from beeing called without permission
|
187
|
+
# * name: Name of method
|
188
|
+
# * mode: :read or :write (or anything else in obscure scenarios)
|
189
|
+
def protect_instance_method(name,mode)
|
190
|
+
#Alias old method (if no alias is available), override method
|
191
|
+
class_eval <<-EOL
|
192
|
+
alias_method :no_acl_#{name}, :#{name} if !respond_to(:no_acl_#{name})
|
193
|
+
EOL
|
194
|
+
instance_eval <<-EOL
|
195
|
+
def #{name}(*args,&block)
|
196
|
+
permitted_to!(:#{mode}_#{name}) if !permitted_to?(:#{mode})
|
197
|
+
return send(:#{name},*args,&block)
|
198
|
+
end
|
199
|
+
EOL
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
|
204
|
+
if(options[:include_attributes]) #If attribute / getter-setter-access ought to be checekd#
|
205
|
+
#Try to parse input - if there's any
|
206
|
+
#Provide defaults
|
207
|
+
require_read_for = []
|
208
|
+
require_write_for = []
|
209
|
+
whitelist = []
|
210
|
+
#try reading parameters
|
211
|
+
begin
|
212
|
+
require_read_for = options[:include_attributes][0][:require_read_for] || []
|
213
|
+
require_write_for = options[:include_attributes][0][:require_write_for] || []
|
214
|
+
whitelist = options[:include_attributes][0][:whitelist] || []
|
215
|
+
rescue;end
|
216
|
+
|
217
|
+
#convert arrays to sets
|
218
|
+
require_read_for = require_read_for.to_set
|
219
|
+
require_write_for = require_write_for.to_set
|
220
|
+
whitelist = whitelist.to_set
|
221
|
+
|
222
|
+
#Enable callback for instance-level meta programming
|
223
|
+
def after_initialize; end
|
224
|
+
|
225
|
+
|
226
|
+
#1 Generate guards for ar-attributes
|
227
|
+
column_names.each do |name|
|
228
|
+
protect_ar_attribute(name) unless name.to_s == self.primary_key.to_s || whitelist.include?(name)
|
229
|
+
end
|
230
|
+
|
231
|
+
#2 Evaluate :require_read_for, :require_write_for
|
232
|
+
if protect_attributes
|
233
|
+
after_initialize do |object|
|
234
|
+
require_write_for.each {|attr| object.protect_instance_method(attr,:write) }
|
235
|
+
require_read_for.each {|attr| object.protect_instance_method(attr,:read) }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
#3 Generate guards for ar-proxies
|
240
|
+
after_initialize do |object|
|
241
|
+
end
|
242
|
+
|
243
|
+
#2nd Generate guards for ar-proxies
|
244
|
+
after_initialize do |object|
|
245
|
+
reflect_on_all_associations.each do |assoc|
|
246
|
+
#Respect excludes
|
247
|
+
#Ok, we've to intercept these calls (See: ActiveRecord::Associations::ClassMethods)
|
248
|
+
# one-to-one: other_id, other_id=(id), other, other=(other), build_other(attributes={}), create_other(attributes={})
|
249
|
+
# one-to-many / many-to-many: others, others=(other,other,...), other_ids, other_ids=(id,id,...), others<<
|
250
|
+
object.inject_acl_object_getter_setter(assoc.name.to_s) unless whitelist.include?(assoc.name)
|
251
|
+
|
252
|
+
if(assoc.collection?) #Collection? if so, many-to-many case
|
253
|
+
object.protect_instance_method("#{assoc.name.to_s.singularize}_ids",:read) unless whitelist.include?("#{assoc.name.to_s.singularize}_ids".to_sym)
|
254
|
+
object.protect_instance_method("#{assoc.name.to_s.singularize}_ids",:write) unless whitelist.include?("#{assoc.name.to_s.singularize}_ids".to_sym)
|
255
|
+
#inject_acl_write_check("#{assoc.name}<<")
|
256
|
+
else
|
257
|
+
object.protect_instance_method("#{assoc.name}_id",:read) unless assoc.macro != :has_one || whitelist.include?("#{assoc.name}_id".to_sym)
|
258
|
+
object.protect_instance_method("#{assoc.name}_id",:write) unless assoc.macro != :has_one || whitelist.include?("#{assoc.name}_id".to_sym)
|
259
|
+
object.protect_instance_method("build_#{assoc.name}",:write) unless whitelist.include?("build_#{assoc.name}".to_sym)
|
260
|
+
object.protect_instance_method("create_#{assoc.name}",:read) unless whitelist.include?("create_#{assoc.name}".to_sym)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def readable_attributes #Define Attribute - Arrays as a replacement for model.attributes
|
266
|
+
return attributes if permitted_to?(:read)
|
267
|
+
attributes.reject do |k,v|
|
268
|
+
!permitted_to?("read_#{k}".to_sym) && k.to_s != self.class.primary_key.to_s
|
269
|
+
end
|
270
|
+
end
|
271
|
+
def writable_attributes
|
272
|
+
return attributes if permitted_to?(:write)
|
273
|
+
attributes.reject do |k,v|
|
274
|
+
!permitted_to?("write_#{k}#".to_sym) && k.to_s != self.class.primary_key.to_s
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
# #End patch - Taking it down to attribute level
|
279
|
+
|
280
|
+
if Rails.version < "3"
|
281
|
+
def after_find; end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.using_access_control?
|
286
|
+
true
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Returns true if the model is using model security.
|
292
|
+
def self.using_access_control?
|
293
|
+
false
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,463 @@
|
|
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
|
+
# If the user meets the given privilege, permitted_to? returns true
|
10
|
+
# and yields to the optional block.
|
11
|
+
def permitted_to? (privilege, options = {}, &block)
|
12
|
+
options = {
|
13
|
+
:user => Authorization.current_user,
|
14
|
+
:object => self
|
15
|
+
}.merge(options)
|
16
|
+
Authorization::Engine.instance.permit?(privilege,
|
17
|
+
{:user => options[:user],
|
18
|
+
:object => options[:object]},
|
19
|
+
&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Works similar to the permitted_to? method, but doesn't accept a block
|
23
|
+
# and throws the authorization exceptions, just like Engine#permit!
|
24
|
+
def permitted_to! (privilege, options = {} )
|
25
|
+
options = {
|
26
|
+
:user => Authorization.current_user,
|
27
|
+
:object => self
|
28
|
+
}.merge(options)
|
29
|
+
Authorization::Engine.instance.permit!(privilege,
|
30
|
+
{:user => options[:user],
|
31
|
+
:object => options[:object]})
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Returns true or false, depending on whether we can read/write a column based on all our rules
|
36
|
+
#
|
37
|
+
# PARAMS
|
38
|
+
#
|
39
|
+
# mode Symbol. :read/:write
|
40
|
+
# attribute String. the column we want to check
|
41
|
+
# application_defaults Boolean, whether we want to incude the application defaults or not
|
42
|
+
#
|
43
|
+
# RETURNS
|
44
|
+
#
|
45
|
+
# boolean, true/false
|
46
|
+
#
|
47
|
+
def allowed?(mode, attribute, exclude_application_defaults = false)
|
48
|
+
# Return false if mode is not read or write
|
49
|
+
return false unless [:read, :write].include?(mode)
|
50
|
+
|
51
|
+
# Variables needed to make checks
|
52
|
+
access_all_columns_sym = (mode == :read) ? self.class.read_all_privilege.to_sym : self.class.write_all_privilege.to_sym
|
53
|
+
whitelist_sym = (mode == :read) ? attribute.to_sym : (attribute + '=').to_sym
|
54
|
+
acl_sym = (mode == :read) ? ('read_' + attribute).to_sym : ('write_' + attribute).to_sym
|
55
|
+
|
56
|
+
# Perform checks, returns early on success
|
57
|
+
return true if attribute.to_s == self.class.primary_key.to_s # Always return true on primary key
|
58
|
+
return true if !exclude_application_defaults && get_application_default_attributes.include?(attribute.to_sym) # Test application defaults first
|
59
|
+
return true if permitted_to_without_include_attributes?(access_all_columns_sym) # Are we allowed read/write all?
|
60
|
+
return true if get_white_list.include?(whitelist_sym) # White Listed
|
61
|
+
return true if permitted_to_without_include_attributes?(acl_sym) # read/write_{attribute} given explicitly
|
62
|
+
false # Not allowed, return false
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.included(base) # :nodoc:
|
66
|
+
#base.extend(ClassMethods)
|
67
|
+
base.module_eval do
|
68
|
+
scopes[:with_permissions_to] = lambda do |parent_scope, *args|
|
69
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
70
|
+
privilege = (args[0] || :read).to_sym
|
71
|
+
privileges = [privilege]
|
72
|
+
context =
|
73
|
+
if options[:context]
|
74
|
+
options[:context]
|
75
|
+
elsif parent_scope.respond_to?(:proxy_reflection)
|
76
|
+
parent_scope.proxy_reflection.klass.name.tableize.to_sym
|
77
|
+
elsif parent_scope.respond_to?(:decl_auth_context)
|
78
|
+
parent_scope.decl_auth_context
|
79
|
+
else
|
80
|
+
parent_scope.name.tableize.to_sym
|
81
|
+
end
|
82
|
+
|
83
|
+
user = options[:user] || Authorization.current_user
|
84
|
+
|
85
|
+
engine = options[:engine] || Authorization::Engine.instance
|
86
|
+
engine.permit!(privileges, :user => user, :skip_attribute_test => true,
|
87
|
+
:context => context)
|
88
|
+
|
89
|
+
obligation_scope_for( privileges, :user => user,
|
90
|
+
:context => context, :engine => engine, :model => parent_scope)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Builds and returns a scope with joins and conditions satisfying all obligations.
|
94
|
+
def self.obligation_scope_for( privileges, options = {} )
|
95
|
+
options = {
|
96
|
+
:user => Authorization.current_user,
|
97
|
+
:context => nil,
|
98
|
+
:model => self,
|
99
|
+
:engine => nil,
|
100
|
+
}.merge(options)
|
101
|
+
engine = options[:engine] || Authorization::Engine.instance
|
102
|
+
|
103
|
+
obligation_scope = ObligationScope.new( options[:model], {} )
|
104
|
+
engine.obligations( privileges, :user => options[:user], :context => options[:context] ).each do |obligation|
|
105
|
+
obligation_scope.parse!( obligation )
|
106
|
+
end
|
107
|
+
|
108
|
+
obligation_scope.scope
|
109
|
+
end
|
110
|
+
|
111
|
+
# Named scope for limiting query results according to the authorization
|
112
|
+
# of the current user. If no privilege is given, :+read+ is assumed.
|
113
|
+
#
|
114
|
+
# User.with_permissions_to
|
115
|
+
# User.with_permissions_to(:update)
|
116
|
+
# User.with_permissions_to(:update, :context => :users)
|
117
|
+
#
|
118
|
+
# As in the case of other named scopes, this one may be chained:
|
119
|
+
# User.with_permission_to.find(:all, :conditions...)
|
120
|
+
#
|
121
|
+
# Options
|
122
|
+
# [:+context+]
|
123
|
+
# Context for the privilege to be evaluated in; defaults to the
|
124
|
+
# model's table name.
|
125
|
+
# [:+user+]
|
126
|
+
# User to be used for gathering obligations; defaults to the
|
127
|
+
# current user.
|
128
|
+
#
|
129
|
+
def self.with_permissions_to (*args)
|
130
|
+
scopes[:with_permissions_to].call(self, *args)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Activates model security for the current model. Then, CRUD operations
|
134
|
+
# are checked against the authorization of the current user. The
|
135
|
+
# privileges are :+create+, :+read+, :+update+ and :+delete+ in the
|
136
|
+
# context of the model. By default, :+read+ is not checked because of
|
137
|
+
# performance impacts, especially with large result sets.
|
138
|
+
#
|
139
|
+
# class User < ActiveRecord::Base
|
140
|
+
# using_access_control
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# If an operation is not permitted, a Authorization::AuthorizationError
|
144
|
+
# is raised.
|
145
|
+
#
|
146
|
+
# To activate model security on all models, call using_access_control
|
147
|
+
# on ActiveRecord::Base
|
148
|
+
# ActiveRecord::Base.using_access_control
|
149
|
+
#
|
150
|
+
# Available options
|
151
|
+
# [:+context+] Specify context different from the models table name.
|
152
|
+
# [:+include_read+] Also check for :+read+ privilege after find.
|
153
|
+
#
|
154
|
+
def self.using_access_control (options = {})
|
155
|
+
options = {
|
156
|
+
:context => nil,
|
157
|
+
:include_read => false
|
158
|
+
}.merge(options)
|
159
|
+
|
160
|
+
class_eval do
|
161
|
+
if options[:include_read]
|
162
|
+
# If we are limiting access by options[:include_attributes], then we do not want to do the check on the entire object
|
163
|
+
# instead we will allow the individual checks to determine what passes and what failes
|
164
|
+
unless(options[:include_attributes])
|
165
|
+
# after_find is only called if after_find is implemented
|
166
|
+
after_find do |object|
|
167
|
+
Authorization::Engine.instance.permit!(:read, :object => object,
|
168
|
+
:context => options[:context])
|
169
|
+
end
|
170
|
+
|
171
|
+
if Rails.version < "3"
|
172
|
+
def after_find; end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# If we are limiting access by options[:include_attributes], then we do not want to do the check on the entire object
|
178
|
+
# instead we will allow the individual checks to determine what passes and what failes
|
179
|
+
unless(options[:include_attributes])
|
180
|
+
[:create, :update, [:destroy, :delete]].each do |action, privilege|
|
181
|
+
send(:"before_#{action}") do |object|
|
182
|
+
Authorization::Engine.instance.permit!(privilege || action, :object => object, :context => options[:context])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
#Inject an acl_write check for a given methid into method-chain
|
188
|
+
def self.inject_acl_write_check(method_name)
|
189
|
+
inject_acl_check(method_name,:write)
|
190
|
+
end
|
191
|
+
|
192
|
+
#Inject an acl_read check for a given methid into method-chain
|
193
|
+
def self.inject_acl_read_check(method_name)
|
194
|
+
inject_acl_check(method_name,:read)
|
195
|
+
end
|
196
|
+
|
197
|
+
#routine for helper methods
|
198
|
+
def self.inject_acl_check(method_name,mode)
|
199
|
+
command = <<-EOV
|
200
|
+
alias_method :no_acl_#{method_name}, :#{method_name} unless respond_to?(:no_acl_#{method_name})
|
201
|
+
def #{method_name}(*args,&block)
|
202
|
+
permitted_to!(:#{mode}_#{method_name}) if !permitted_to?(:#{mode})
|
203
|
+
return no_acl_#{method_name}(args#{',block' unless method_name.to_s.match(/=$/)}) unless args.blank? || block.blank?
|
204
|
+
return no_acl_#{method_name}(#{'block' unless method_name.to_s.match(/=$/)}) if args.blank? && block
|
205
|
+
return no_acl_#{method_name}(args) if !args.blank? && block.blank?
|
206
|
+
return no_acl_#{method_name}()
|
207
|
+
end
|
208
|
+
EOV
|
209
|
+
class_eval command
|
210
|
+
end
|
211
|
+
|
212
|
+
#Protecting an instance (used for generated code, ie ActiveRecord)
|
213
|
+
def inject_acl_object_check(method_name,mode)
|
214
|
+
command = <<-EOV
|
215
|
+
alias_method :no_acl_#{method_name}, :#{method_name} unless respond_to?(:no_acl_#{method_name})
|
216
|
+
def #{method_name}(*args,&block)
|
217
|
+
permitted_to!(:#{mode}_#{method_name}) if (!permitted_to?(:#{mode}))
|
218
|
+
return no_acl_#{method_name}(args#{',block' unless method_name.to_s.match(/=$/)}) unless args.blank? || block.blank?
|
219
|
+
return no_acl_#{method_name}(#{'block' unless method_name.to_s.match(/=$/)}) if args.blank? && block
|
220
|
+
return no_acl_#{method_name}(args) if !args.blank? && block.blank?
|
221
|
+
return no_acl_#{method_name}()
|
222
|
+
end
|
223
|
+
EOV
|
224
|
+
class_eval command
|
225
|
+
end
|
226
|
+
|
227
|
+
#Inject acl-aware setter / getter methods into method-chain
|
228
|
+
def inject_acl_object_getter_setter(method_name)
|
229
|
+
class_eval <<-EOV
|
230
|
+
alias_method :no_acl_#{method_name}, :#{method_name} unless respond_to? :no_acl_#{method_name}
|
231
|
+
alias_method :no_acl_#{method_name}=, :#{method_name}= unless respond_to? :no_acl_#{method_name}=
|
232
|
+
EOV
|
233
|
+
logger.info "Injecting #{method_name} to #{self}"
|
234
|
+
instance_eval <<-EOV
|
235
|
+
|
236
|
+
def #{method_name}
|
237
|
+
permitted_to!(:read_#{method_name}) unless permitted_to?(:#{read_all_privilege})
|
238
|
+
return no_acl_#{method_name}
|
239
|
+
end
|
240
|
+
def #{method_name}=(value)
|
241
|
+
permitted_to!(:write_#{method_name}) unless permitted_to?(:#{write_all_privilege})
|
242
|
+
return no_acl_#{method_name}=(value)
|
243
|
+
end
|
244
|
+
EOV
|
245
|
+
end
|
246
|
+
|
247
|
+
if(options[:include_attributes]) #If attribute / getter-setter-access ought to be checekd#
|
248
|
+
#parse attribute hash - sane input?
|
249
|
+
raise "Illegal syntax - :include_attributes must point to an array" unless options[:include_attributes][0].is_a?(Hash)
|
250
|
+
|
251
|
+
protect_ar = options[:include_attributes][0][:protect_ar] || []
|
252
|
+
raise "Illegal syntax :protect_ar must point to an array" unless protect_ar.blank? || protect_ar.is_a?(Array)
|
253
|
+
protect_ar = protect_ar.to_set
|
254
|
+
|
255
|
+
protect_read = options[:include_attributes][0][:protect_read]
|
256
|
+
raise "Illegal syntax :protect_read must point to an array" unless protect_read.nil? || protect_read.is_a?(Array)
|
257
|
+
|
258
|
+
protect_write = options[:include_attributes][0][:protect_write]
|
259
|
+
raise "Illegal syntax :protect_write must point to an array" unless protect_write.nil? || protect_write.is_a?(Array)
|
260
|
+
|
261
|
+
protect_attributes = options[:include_attributes][0][:protect_attributes]
|
262
|
+
raise "Illegal syntax :protect_attributes point to an array" unless protect_attributes.nil? || protect_attributes.is_a?(Array)
|
263
|
+
|
264
|
+
whitelist = options[:include_attributes][0][:whitelist] || []
|
265
|
+
raise "Illegal syntax :whitelist must point to an array" unless whitelist.blank? || whitelist.is_a?(Array)
|
266
|
+
whitelist = whitelist.to_set
|
267
|
+
|
268
|
+
application_default_attributes = options[:include_attributes][0][:application_default_attributes] || []
|
269
|
+
raise "Illegal syntax :application_default_attributes must point to an array" unless application_default_attributes.blank? || application_default_attributes.is_a?(Array)
|
270
|
+
application_default_attributes = application_default_attributes.to_set
|
271
|
+
|
272
|
+
#Enable callback for instance-level meta programming
|
273
|
+
def after_initialize; end
|
274
|
+
|
275
|
+
# Create helper methods, that can be called from within our code to access
|
276
|
+
# variables that are set up during initilization
|
277
|
+
instance_eval <<-EOV
|
278
|
+
#
|
279
|
+
# Determine what privilege to use for read all
|
280
|
+
#
|
281
|
+
def read_all_privilege
|
282
|
+
'#{options[:include_attributes][0][:read_all_privilege].blank? ? 'read' : options[:include_attributes][0][:read_all_privilege]}'
|
283
|
+
end
|
284
|
+
|
285
|
+
#
|
286
|
+
# Determine what privilege to use for write all
|
287
|
+
#
|
288
|
+
def write_all_privilege
|
289
|
+
'#{options[:include_attributes][0][:write_all_privilege].blank? ? 'write' : options[:include_attributes][0][:write_all_privilege]}'
|
290
|
+
end
|
291
|
+
EOV
|
292
|
+
|
293
|
+
class_eval <<-EOV
|
294
|
+
#
|
295
|
+
# Method to return the white list
|
296
|
+
#
|
297
|
+
def get_white_list
|
298
|
+
[#{whitelist.to_a.collect{|c| ":#{c}"}.join(',')}]
|
299
|
+
end
|
300
|
+
|
301
|
+
#
|
302
|
+
# Method to return the application_default_attributes
|
303
|
+
#
|
304
|
+
def get_application_default_attributes
|
305
|
+
[#{application_default_attributes.to_a.collect{|c| ":#{c}"}.join(',')}]
|
306
|
+
end
|
307
|
+
EOV
|
308
|
+
|
309
|
+
#1a Generate guards for ar-attributes
|
310
|
+
if protect_ar.include?(:attributes)
|
311
|
+
column_names.each do |name|
|
312
|
+
class_eval "begin; alias_method :no_acl_#{name}, :#{name};rescue;end #Alias-Methods - put acl stuff into method-chain
|
313
|
+
begin; alias_method :no_acl_#{name}=, :#{name}=; rescue; end
|
314
|
+
def #{name} #Define getters / setter with ACL-Checks
|
315
|
+
permitted_to!(:read_#{name}) if !permitted_to?(:#{read_all_privilege});
|
316
|
+
if(respond_to? 'no_acl_#{name}')
|
317
|
+
return no_acl_#{name}
|
318
|
+
else
|
319
|
+
return read_attribute(:#{name})
|
320
|
+
end
|
321
|
+
end" unless name.to_s == self.primary_key.to_s || whitelist.include?(name.to_sym) || application_default_attributes.include?(name.to_sym) || !options[:include_read] # Do not do reads, unless told so
|
322
|
+
class_eval %{def #{name}=(n)
|
323
|
+
permitted_to!(:write_#{name}) if !permitted_to?(:#{write_all_privilege});
|
324
|
+
if(respond_to? 'no_acl_#{name}=')
|
325
|
+
return no_acl_#{name}=(n)
|
326
|
+
else
|
327
|
+
return write_attribute(:#{name},n)
|
328
|
+
end
|
329
|
+
end} unless name.to_s == self.primary_key.to_s || whitelist.include?("#{name}=".to_sym) || application_default_attributes.include?(name.to_sym)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
#1b Generate guards for non-ar attributes
|
334
|
+
if protect_attributes
|
335
|
+
after_initialize do |object|
|
336
|
+
protect_attributes.each { |attr| object.inject_acl_object_getter_setter(attr) }
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
#2nd Generate guards for ar-proxies
|
342
|
+
if protect_ar.include?(:proxies)
|
343
|
+
after_initialize do |object|
|
344
|
+
reflect_on_all_associations.each do |assoc|
|
345
|
+
#Respect excludes
|
346
|
+
#Ok, we've to intercept these calls (See: ActiveRecord::Associations::ClassMethods)
|
347
|
+
# one-to-one: other_id, other_id=(id), other, other=(other), build_other(attributes={}), create_other(attributes={})
|
348
|
+
# one-to-many / many-to-many: others, others=(other,other,...), other_ids, other_ids=(id,id,...), others<<
|
349
|
+
object.inject_acl_object_getter_setter(assoc.name.to_s) unless whitelist.include?(assoc.name)
|
350
|
+
|
351
|
+
if(assoc.collection?) #Collection? if so, many-to-many case
|
352
|
+
object.inject_acl_object_getter_setter("#{assoc.name.to_s.singularize}_ids") unless whitelist.include?("#{assoc.name.to_s.singularize}_ids".to_sym)
|
353
|
+
#inject_acl_write_check("#{assoc.name}<<")
|
354
|
+
else
|
355
|
+
object.inject_acl_object_getter_setter("#{assoc.name}_id") unless assoc.macro != :has_one || whitelist.include?("#{assoc.name}_id".to_sym)
|
356
|
+
object.inject_acl_object_check("build_#{assoc.name}",:write) unless whitelist.include?("build_#{assoc.name}".to_sym)
|
357
|
+
object.inject_acl_object_check("create_#{assoc.name}",:read) unless whitelist.include?("create_#{assoc.name}".to_sym)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
#3rd - generate guards for specified methods
|
364
|
+
#3a - read-permission required
|
365
|
+
if(protect_read)
|
366
|
+
after_initialize do |object|
|
367
|
+
protect_read.each { |method| object.inject_acl_object_check(method,:read) }
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
#3b - write permission required
|
372
|
+
if(protect_write)
|
373
|
+
after_initialize do |object|
|
374
|
+
protect_write.each { |method| object.inject_acl_object_check(method,:write) }
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
#
|
379
|
+
# Returns a hash of key, value paris that are readable
|
380
|
+
#
|
381
|
+
def readable_attributes
|
382
|
+
return attributes if permitted_to?(self.class.read_all_privilege.to_sym)
|
383
|
+
attributes.reject do |k,v|
|
384
|
+
!allowed?(:read, k)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
#
|
389
|
+
# Returns a hash of key, value paris that are showable, excluding application_default_attributes
|
390
|
+
#
|
391
|
+
def showable_attributes
|
392
|
+
return attributes if permitted_to?(self.class.read_all_privilege.to_sym)
|
393
|
+
attributes.reject do |k,v|
|
394
|
+
!allowed?(:read, k, true)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
#
|
399
|
+
# Returns a hash of key, value paris that are writable
|
400
|
+
#
|
401
|
+
def writable_attributes
|
402
|
+
return attributes if permitted_to?(self.class.write_all_privilege.to_sym)
|
403
|
+
attributes.reject do |k,v|
|
404
|
+
!allowed?(:write, k)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
#
|
409
|
+
# Returns a list of columns that are readable
|
410
|
+
#
|
411
|
+
def readable_columns
|
412
|
+
readable_attributes.keys
|
413
|
+
end
|
414
|
+
|
415
|
+
#
|
416
|
+
# Returns a list of columns that are writable
|
417
|
+
#
|
418
|
+
def writable_columns
|
419
|
+
writable_attributes.keys
|
420
|
+
end
|
421
|
+
|
422
|
+
#
|
423
|
+
# Returns a list of columns that are showable to the user
|
424
|
+
#
|
425
|
+
def showable_columns
|
426
|
+
showable_attributes.keys
|
427
|
+
end
|
428
|
+
|
429
|
+
#
|
430
|
+
# When calling permitted_to? on the model, we return true if whitelist or read/write all
|
431
|
+
# excluding application_default_attributes
|
432
|
+
#
|
433
|
+
def permitted_to_with_include_attributes?(privilege, options = {}, &block)
|
434
|
+
# Figure out what priv/attribute was passed, if it begins with read_ or write_
|
435
|
+
if reg = privilege.to_s.match(/(^write_|^read_)(.+)/)
|
436
|
+
mode, attribute = reg[1].chop.to_sym, reg[2] # Split the regular expression accordingly
|
437
|
+
if allowed?(mode, attribute, true) # Exclude application_default_attributes
|
438
|
+
yield if block_given?
|
439
|
+
return true
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Default back to old call
|
444
|
+
permitted_to_without_include_attributes?(privilege, options, &block)
|
445
|
+
end
|
446
|
+
|
447
|
+
alias_method_chain :permitted_to?, :include_attributes
|
448
|
+
end
|
449
|
+
|
450
|
+
def self.using_access_control?
|
451
|
+
true
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# Returns true if the model is using model security.
|
457
|
+
def self.using_access_control?
|
458
|
+
false
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|