Class: Heimdallr::Proxy::Record
- Inherits:
-
Object
- Object
- Heimdallr::Proxy::Record
- Defined in:
- lib/heimdallr/proxy/record.rb
Overview
A security-aware proxy for individual records. This class validates all the method calls and either forwards them to the encapsulated object or raises an exception.
The #touch method call isn’t considered a security threat and as such, it is forwarded to the underlying object directly.
Record proxies can be of two types, implicit and explicit. Implicit proxies return nil
on access to methods forbidden by the current security context; explicit proxies raise an Heimdallr::PermissionError instead.
Instance Method Summary collapse
-
#assign_attributes ⇒ Object
Delegates to the corresponding method of underlying object.
-
#attributes ⇒ Object
A proxy for
attributes
method which removes all attributes without:view
permission. -
#attributes= ⇒ Object
Delegates to the corresponding method of underlying object.
-
#check_attributes ⇒ Object
protected
Raises an exception if any of the changed attributes are not valid for the current security context.
-
#check_save_options(options) ⇒ Object
protected
Raises an exception if any of the
options
intended for use insave
methods are potentially unsafe. -
#class_name ⇒ String
Class name of the underlying model.
- #creatable? ⇒ Boolean
-
#decrement(field, by = 1) ⇒ Object
Delegates to the corresponding method of underlying object.
- #destroyable? ⇒ Boolean
-
#errors ⇒ Object
Delegates to the corresponding method of underlying object.
-
#explicit ⇒ Heimdallr::Proxy::Record
Return an explicit variant of this proxy.
-
#implicit ⇒ Heimdallr::Proxy::Record
Return an implicit variant of this proxy.
-
#increment(field, by = 1) ⇒ Object
Delegates to the corresponding method of underlying object.
-
#initialize(context, record, options = {}) ⇒ Record
constructor
Create a record proxy.
-
#insecure ⇒ ActiveRecord::Base
Return the underlying object.
-
#inspect ⇒ String
Describes the proxy and proxified object.
-
#invalid? ⇒ Object
Delegates to the corresponding method of underlying object.
-
#method_missing(method, *args, &block) ⇒ Object
A whitelisting dispatcher for attribute-related method calls.
-
#model_name ⇒ Object
Delegates to the corresponding method of underlying object.
- #modifiable? ⇒ Boolean
-
#reflect_on_security ⇒ Hash
Return the associated security metadata.
-
#restrict(context, options = nil) ⇒ Object
Records cannot be restricted with different context or options.
-
#save(options = {}) ⇒ Object
A proxy for
save
method which verifies all of the dirty attributes to be valid for current security context. -
#save!(options = {}) ⇒ Object
A proxy for
save
method which verifies all of the dirty attributes to be valid for current security context and mandates the current record to be valid. -
#to_key ⇒ Object
Delegates to the corresponding method of underlying object.
-
#to_param ⇒ Object
Delegates to the corresponding method of underlying object.
-
#toggle(field) ⇒ Object
Delegates to the corresponding method of underlying object.
-
#touch(field) ⇒ Object
Delegates to the corresponding method of underlying object.
-
#update_attributes(attributes, options = {}) ⇒ Object
A proxy for
update_attributes
method. -
#update_attributes!(attributes, options = {}) ⇒ Object
A proxy for
update_attributes!
method. -
#valid? ⇒ Object
Delegates to the corresponding method of underlying object.
- #visible? ⇒ Boolean
Constructor Details
#initialize(context, record, options = {}) ⇒ Record
Create a record proxy.
18 19 20 21 22 23 |
# File 'lib/heimdallr/proxy/record.rb', line 18 def initialize(context, record, ={}) @context, @record, @options = context, record, .dup @restrictions = @record.class.restrictions(context, record) @eager_loaded = @options.delete(:eager_loaded) || {} end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method, *args, &block) ⇒ Object
A whitelisting dispatcher for attribute-related method calls. Every unknown method is first normalized (that is, stripped of its ? or = suffix). Then, if the normalized form is whitelisted, it is passed to the underlying object as-is. Otherwise, an exception is raised.
If the underlying object is an instance of ActiveRecord, then all association accesses are resolved and proxified automatically.
Note that only the attribute and collection getters and setters are dispatched through this method. Every other model method should be defined as an instance method of this class in order to work.
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/heimdallr/proxy/record.rb', line 183 def method_missing(method, *args, &block) suffix = method.to_s[-1] if %w(? = !).include? suffix normalized_method = method[0..-2].to_sym else normalized_method = method suffix = nil end if (@record.is_a?(ActiveRecord::Reflection) && association = @record.class.reflect_on_association(method)) || (!@record.class.heimdallr_relations.nil? && @record.class.heimdallr_relations.include?(normalized_method)) referenced = @record.send(method, *args) if referenced.nil? nil elsif referenced.respond_to? :restrict if @eager_loaded.include?(method) = @options.merge(eager_loaded: @eager_loaded[method]) else = @options end if association.collection? && @eager_loaded.include?(method) # Don't re-restrict eagerly loaded collections to not # discard preloaded data. Proxy::Collection.new(@context, referenced, ) else referenced.restrict(@context, @options) end elsif Heimdallr.allow_insecure_associations referenced else raise Heimdallr::InsecureOperationError, "Attempt to fetch insecure association #{method}. Try #insecure" end elsif @record.respond_to? method if [nil, '?'].include?(suffix) if @restrictions.allowed_fields[:view].include?(normalized_method) result = @record.send method, *args, &block if result.respond_to? :restrict result.restrict(@context, @options) else result end elsif @options[:implicit] nil else raise Heimdallr::PermissionError, "Attempt to fetch non-whitelisted attribute #{method}" end elsif suffix == '=' @record.send method, *args else raise Heimdallr::PermissionError, "Non-whitelisted method #{method} is called for #{@record.inspect} " end else super end end |
Instance Method Details
#assign_attributes ⇒ Object
Delegates to the corresponding method of underlying object.
144 |
# File 'lib/heimdallr/proxy/record.rb', line 144 delegate :assign_attributes, :to => :@record |
#attributes ⇒ Object
A proxy for attributes
method which removes all attributes without :view
permission.
58 59 60 61 62 63 64 65 66 |
# File 'lib/heimdallr/proxy/record.rb', line 58 def attributes @record.attributes.tap do |attributes| attributes.keys.each do |key| unless @restrictions.allowed_fields[:view].include? key.to_sym attributes[key] = nil end end end end |
#attributes= ⇒ Object
Delegates to the corresponding method of underlying object.
148 |
# File 'lib/heimdallr/proxy/record.rb', line 148 delegate :attributes=, :to => :@record |
#check_attributes ⇒ Object (protected)
Raises an exception if any of the changed attributes are not valid for the current security context.
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# File 'lib/heimdallr/proxy/record.rb', line 315 def check_attributes @record.errors.clear if @record.new_record? action = :create else action = :update end allowed_fields = @restrictions.allowed_fields[action] fixtures = @restrictions.fixtures[action] validators = @restrictions.validators[action] @record.changed.map(&:to_sym).each do |attribute| value = @record.send attribute if fixtures.has_key? attribute if fixtures[attribute] != value raise Heimdallr::PermissionError, "Attribute #{attribute} value (#{value}) is not equal to a fixture (#{fixtures[attribute]})" end elsif !allowed_fields.include? attribute raise Heimdallr::PermissionError, "Attribute #{attribute} is not allowed to change" end end @record.heimdallr_validators = validators yield ensure @record.heimdallr_validators = nil end |
#check_save_options(options) ⇒ Object (protected)
Raises an exception if any of the options
intended for use in save
methods are potentially unsafe.
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/heimdallr/proxy/record.rb', line 351 def () if [:validate] == false raise Heimdallr::InsecureOperationError, "Saving while omitting validation would omit security validations too" end if @record.new_record? unless @restrictions.can? :create raise Heimdallr::InsecureOperationError, "Creating was not explicitly allowed" end else unless @restrictions.can? :update raise Heimdallr::InsecureOperationError, "Updating was not explicitly allowed" end end end |
#class_name ⇒ String
Class name of the underlying model.
152 153 154 |
# File 'lib/heimdallr/proxy/record.rb', line 152 def class_name @record.class.name end |
#creatable? ⇒ Boolean
296 297 298 |
# File 'lib/heimdallr/proxy/record.rb', line 296 def creatable? @restrictions.can? :create end |
#decrement(field, by = 1) ⇒ Object
Delegates to the corresponding method of underlying object.
28 |
# File 'lib/heimdallr/proxy/record.rb', line 28 delegate :decrement, :to => :@record |
#destroyable? ⇒ Boolean
304 305 306 307 |
# File 'lib/heimdallr/proxy/record.rb', line 304 def destroyable? scope = @restrictions.request_scope(:delete) scope.where({ @record.class.primary_key => @record.to_key }).any? end |
#errors ⇒ Object
Delegates to the corresponding method of underlying object.
140 |
# File 'lib/heimdallr/proxy/record.rb', line 140 delegate :errors, :to => :@record |
#explicit ⇒ Heimdallr::Proxy::Record
Return an explicit variant of this proxy.
262 263 264 |
# File 'lib/heimdallr/proxy/record.rb', line 262 def explicit Proxy::Record.new(@context, @record, @options.merge(implicit: false)) end |
#implicit ⇒ Heimdallr::Proxy::Record
Return an implicit variant of this proxy.
255 256 257 |
# File 'lib/heimdallr/proxy/record.rb', line 255 def implicit Proxy::Record.new(@context, @record, @options.merge(implicit: true)) end |
#increment(field, by = 1) ⇒ Object
Delegates to the corresponding method of underlying object.
32 |
# File 'lib/heimdallr/proxy/record.rb', line 32 delegate :increment, :to => :@record |
#insecure ⇒ ActiveRecord::Base
Return the underlying object.
248 249 250 |
# File 'lib/heimdallr/proxy/record.rb', line 248 def insecure @record end |
#inspect ⇒ String
Describes the proxy and proxified object.
269 270 271 |
# File 'lib/heimdallr/proxy/record.rb', line 269 def inspect "#<Heimdallr::Proxy::Record: #{@record.inspect}>" end |
#invalid? ⇒ Object
Delegates to the corresponding method of underlying object.
136 |
# File 'lib/heimdallr/proxy/record.rb', line 136 delegate :invalid?, :to => :@record |
#model_name ⇒ Object
Delegates to the corresponding method of underlying object.
46 |
# File 'lib/heimdallr/proxy/record.rb', line 46 delegate :model_name, :to => :@record |
#modifiable? ⇒ Boolean
300 301 302 |
# File 'lib/heimdallr/proxy/record.rb', line 300 def modifiable? @restrictions.can? :update end |
#reflect_on_security ⇒ Hash
Return the associated security metadata. The returned hash will contain keys :context
, :record
, :options
, corresponding to the parameters in #initialize, :model
and :restrictions
, representing the model class.
Such a name was deliberately selected for this method in order to reduce namespace pollution.
281 282 283 284 285 286 287 288 289 |
# File 'lib/heimdallr/proxy/record.rb', line 281 def reflect_on_security { model: @record.class, context: @context, record: @record, options: @options, restrictions: @restrictions, }.merge(@restrictions.reflection) end |
#restrict(context, options = nil) ⇒ Object
Records cannot be restricted with different context or options.
160 161 162 163 164 165 166 |
# File 'lib/heimdallr/proxy/record.rb', line 160 def restrict(context, =nil) if @context == context && .nil? self else raise RuntimeError, "Heimdallr proxies cannot be restricted with nonmatching context or options" end end |
#save(options = {}) ⇒ Object
A proxy for save
method which verifies all of the dirty attributes to be valid for current security context.
94 95 96 97 98 99 100 |
# File 'lib/heimdallr/proxy/record.rb', line 94 def save(={}) check_attributes do @record.save() end end |
#save!(options = {}) ⇒ Object
A proxy for save
method which verifies all of the dirty attributes to be valid for current security context and mandates the current record to be valid.
109 110 111 112 113 114 115 |
# File 'lib/heimdallr/proxy/record.rb', line 109 def save!(={}) check_attributes do @record.save!() end end |
#to_key ⇒ Object
Delegates to the corresponding method of underlying object.
50 |
# File 'lib/heimdallr/proxy/record.rb', line 50 delegate :to_key, :to => :@record |
#to_param ⇒ Object
Delegates to the corresponding method of underlying object.
54 |
# File 'lib/heimdallr/proxy/record.rb', line 54 delegate :to_param, :to => :@record |
#toggle(field) ⇒ Object
Delegates to the corresponding method of underlying object.
36 |
# File 'lib/heimdallr/proxy/record.rb', line 36 delegate :toggle, :to => :@record |
#touch(field) ⇒ Object
Delegates to the corresponding method of underlying object. This method does not modify any fields except for the timestamp itself and thus is not considered as a potential security threat.
42 |
# File 'lib/heimdallr/proxy/record.rb', line 42 delegate :touch, :to => :@record |
#update_attributes(attributes, options = {}) ⇒ Object
A proxy for update_attributes
method. See also #save.
72 73 74 75 76 77 |
# File 'lib/heimdallr/proxy/record.rb', line 72 def update_attributes(attributes, ={}) @record.with_transaction_returning_status do @record.assign_attributes(attributes, ) save end end |
#update_attributes!(attributes, options = {}) ⇒ Object
A proxy for update_attributes!
method. See also #save!.
83 84 85 86 87 88 |
# File 'lib/heimdallr/proxy/record.rb', line 83 def update_attributes!(attributes, ={}) @record.with_transaction_returning_status do @record.assign_attributes(attributes, ) save! end end |
#valid? ⇒ Object
Delegates to the corresponding method of underlying object.
132 |
# File 'lib/heimdallr/proxy/record.rb', line 132 delegate :valid?, :to => :@record |
#visible? ⇒ Boolean
291 292 293 294 |
# File 'lib/heimdallr/proxy/record.rb', line 291 def visible? scope = @restrictions.request_scope(:fetch) scope.where({ @record.class.primary_key => @record.to_key }).any? end |