Class: Heimdallr::Proxy::Record

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(context, record, options = {}) ⇒ Record

Create a record proxy.

Parameters:

  • context

    security context

  • object

    proxified record

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • implicit (Boolean)

    proxy type



18
19
20
21
22
23
# File 'lib/heimdallr/proxy/record.rb', line 18

def initialize(context, record, options={})
  @context, @record, @options = context, record, options.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.

Raises:



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 = @options.merge(eager_loaded: @eager_loaded[method])
      else
        options = @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, options)
      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_attributesObject

Delegates to the corresponding method of underlying object.



148
# File 'lib/heimdallr/proxy/record.rb', line 148

delegate :assign_attributes, :to => :@record

#attributesObject

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_attributesObject (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 check_save_options(options)
  if options[: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_nameString

Class name of the underlying model.

Returns:

  • (String)


156
157
158
# File 'lib/heimdallr/proxy/record.rb', line 156

def class_name
  @record.class.name
end

#creatable?Boolean

Returns:

  • (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

Returns:

  • (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

#errorsObject

Delegates to the corresponding method of underlying object.



144
# File 'lib/heimdallr/proxy/record.rb', line 144

delegate :errors, :to => :@record

#explicitHeimdallr::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

#implicitHeimdallr::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

#insecureActiveRecord::Base

Return the underlying object.

Returns:

  • (ActiveRecord::Base)


259
260
261
# File 'lib/heimdallr/proxy/record.rb', line 259

def insecure
  @record
end

#inspectString

Describes the proxy and proxified object.

Returns:

  • (String)


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

Returns:

  • (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_keyObject (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)

Returns:

  • (Boolean)


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_securityHash

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.

Returns:

  • (Hash)


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.

Returns:

  • self

Raises:

  • (RuntimeError)


164
165
166
167
168
169
170
# File 'lib/heimdallr/proxy/record.rb', line 164

def restrict(context, options=nil)
  if @context == context && options.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(options={})
  check_save_options options

  check_attributes do
    @record.save(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 and mandates the current record to be valid.

Raises:



113
114
115
116
117
118
119
# File 'lib/heimdallr/proxy/record.rb', line 113

def save!(options={})
  check_save_options options

  check_attributes do
    @record.save!(options)
  end
end

#to_keyObject

Delegates to the corresponding method of underlying object.



46
# File 'lib/heimdallr/proxy/record.rb', line 46

delegate :to_key, :to => :@record

#to_paramObject

Delegates to the corresponding method of underlying object.



50
# File 'lib/heimdallr/proxy/record.rb', line 50

delegate :to_param, :to => :@record

#to_partial_pathObject

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_transactionObject (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, options={})
  try_transaction do
    @record.assign_attributes(attributes, options)
    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, options={})
  try_transaction do
    @record.assign_attributes(attributes, options)
    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

Returns:

  • (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