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.
- #modifiable? ⇒ Boolean
-
#persisted? ⇒ Object
Delegates to the corresponding method of underlying object.
- #primary_key ⇒ Object protected
- #record_in_scope?(scope) ⇒ Boolean protected
-
#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.
-
#to_partial_path ⇒ 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.
- #try_transaction ⇒ Object protected
-
#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
- #wrap_key(key) ⇒ Object protected
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.
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 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/heimdallr/proxy/record.rb', line 187 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 builder_method = method.to_s.gsub(/\Abuild_/, '').to_sym association = if @record.class.respond_to?(:reflect_on_association) @record.class.reflect_on_association(method) || @record.class.reflect_on_association(builder_method) end pass = unless association @record.class.heimdallr_relations.respond_to?(:include?) && @record.class.heimdallr_relations.include?(normalized_method) end if association || pass 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 && 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.
148 |
# File 'lib/heimdallr/proxy/record.rb', line 148 delegate :assign_attributes, :to => :@record |
#attributes ⇒ Object
A proxy for attributes
method which removes all attributes without :view
permission.
62 63 64 65 66 67 68 69 70 |
# File 'lib/heimdallr/proxy/record.rb', line 62 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.
152 |
# File 'lib/heimdallr/proxy/record.rb', line 152 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.
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
# File 'lib/heimdallr/proxy/record.rb', line 326 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 action == :create and attribute == :_id and @record.is_a?(Mongoid::Document) # Everything is ok, continue (Mongoid sets _id before saving as opposed to ActiveRecord) elsif 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.
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 |
# File 'lib/heimdallr/proxy/record.rb', line 364 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.
156 157 158 |
# File 'lib/heimdallr/proxy/record.rb', line 156 def class_name @record.class.name end |
#creatable? ⇒ Boolean
307 308 309 |
# File 'lib/heimdallr/proxy/record.rb', line 307 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
315 316 317 318 |
# File 'lib/heimdallr/proxy/record.rb', line 315 def destroyable? scope = @restrictions.request_scope(:delete) record_in_scope? scope end |
#errors ⇒ Object
Delegates to the corresponding method of underlying object.
144 |
# File 'lib/heimdallr/proxy/record.rb', line 144 delegate :errors, :to => :@record |
#explicit ⇒ Heimdallr::Proxy::Record
Return an explicit variant of this proxy.
273 274 275 |
# File 'lib/heimdallr/proxy/record.rb', line 273 def explicit Proxy::Record.new(@context, @record, @options.merge(implicit: false)) end |
#implicit ⇒ Heimdallr::Proxy::Record
Return an implicit variant of this proxy.
266 267 268 |
# File 'lib/heimdallr/proxy/record.rb', line 266 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.
259 260 261 |
# File 'lib/heimdallr/proxy/record.rb', line 259 def insecure @record end |
#inspect ⇒ String
Describes the proxy and proxified object.
280 281 282 |
# File 'lib/heimdallr/proxy/record.rb', line 280 def inspect "#<Heimdallr::Proxy::Record: #{@record.inspect}>" end |
#invalid? ⇒ Object
Delegates to the corresponding method of underlying object.
140 |
# File 'lib/heimdallr/proxy/record.rb', line 140 delegate :invalid?, :to => :@record |
#modifiable? ⇒ Boolean
311 312 313 |
# File 'lib/heimdallr/proxy/record.rb', line 311 def modifiable? @restrictions.can? :update end |
#persisted? ⇒ Object
Delegates to the corresponding method of underlying object.
58 |
# File 'lib/heimdallr/proxy/record.rb', line 58 delegate :persisted?, :to => :@record |
#primary_key ⇒ Object (protected)
387 388 389 |
# File 'lib/heimdallr/proxy/record.rb', line 387 def primary_key @record.class.respond_to?(:primary_key) && @record.class.primary_key || :id end |
#record_in_scope?(scope) ⇒ Boolean (protected)
383 384 385 |
# File 'lib/heimdallr/proxy/record.rb', line 383 def record_in_scope?(scope) scope.where(primary_key => wrap_key(@record.to_key)).any? 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.
292 293 294 295 296 297 298 299 300 |
# File 'lib/heimdallr/proxy/record.rb', line 292 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.
164 165 166 167 168 169 170 |
# File 'lib/heimdallr/proxy/record.rb', line 164 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.
98 99 100 101 102 103 104 |
# File 'lib/heimdallr/proxy/record.rb', line 98 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.
113 114 115 116 117 118 119 |
# File 'lib/heimdallr/proxy/record.rb', line 113 def save!(={}) check_attributes do @record.save!() end end |
#to_key ⇒ Object
Delegates to the corresponding method of underlying object.
46 |
# File 'lib/heimdallr/proxy/record.rb', line 46 delegate :to_key, :to => :@record |
#to_param ⇒ Object
Delegates to the corresponding method of underlying object.
50 |
# File 'lib/heimdallr/proxy/record.rb', line 50 delegate :to_param, :to => :@record |
#to_partial_path ⇒ Object
Delegates to the corresponding method of underlying object.
54 |
# File 'lib/heimdallr/proxy/record.rb', line 54 delegate :to_partial_path, :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 |
#try_transaction ⇒ Object (protected)
395 396 397 398 399 400 401 402 403 |
# File 'lib/heimdallr/proxy/record.rb', line 395 def try_transaction if @record.respond_to?(:with_transaction_returning_status) @record.with_transaction_returning_status do yield end else yield end end |
#update_attributes(attributes, options = {}) ⇒ Object
A proxy for update_attributes
method. See also #save.
76 77 78 79 80 81 |
# File 'lib/heimdallr/proxy/record.rb', line 76 def update_attributes(attributes, ={}) try_transaction do @record.assign_attributes(attributes, ) save end end |
#update_attributes!(attributes, options = {}) ⇒ Object
A proxy for update_attributes!
method. See also #save!.
87 88 89 90 91 92 |
# File 'lib/heimdallr/proxy/record.rb', line 87 def update_attributes!(attributes, ={}) try_transaction do @record.assign_attributes(attributes, ) save! end end |
#valid? ⇒ Object
Delegates to the corresponding method of underlying object.
136 |
# File 'lib/heimdallr/proxy/record.rb', line 136 delegate :valid?, :to => :@record |
#visible? ⇒ Boolean
302 303 304 305 |
# File 'lib/heimdallr/proxy/record.rb', line 302 def visible? scope = @restrictions.request_scope(:fetch) record_in_scope? scope end |
#wrap_key(key) ⇒ Object (protected)
391 392 393 |
# File 'lib/heimdallr/proxy/record.rb', line 391 def wrap_key(key) key.is_a?(Enumerable) ? key.first : key end |