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:



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



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

delegate :assign_attributes, :to => :@record

#attributesObject

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_attributesObject (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 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)


152
153
154
# File 'lib/heimdallr/proxy/record.rb', line 152

def class_name
  @record.class.name
end

#creatable?Boolean

Returns:

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

Returns:

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

#errorsObject

Delegates to the corresponding method of underlying object.



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

delegate :errors, :to => :@record

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

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

#insecureActiveRecord::Base

Return the underlying object.

Returns:

  • (ActiveRecord::Base)


248
249
250
# File 'lib/heimdallr/proxy/record.rb', line 248

def insecure
  @record
end

#inspectString

Describes the proxy and proxified object.

Returns:

  • (String)


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_nameObject

Delegates to the corresponding method of underlying object.



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

delegate :model_name, :to => :@record

#modifiable?Boolean

Returns:

  • (Boolean)


300
301
302
# File 'lib/heimdallr/proxy/record.rb', line 300

def modifiable?
  @restrictions.can? :update
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)


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.

Returns:

  • self

Raises:

  • (RuntimeError)


160
161
162
163
164
165
166
# File 'lib/heimdallr/proxy/record.rb', line 160

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.



94
95
96
97
98
99
100
# File 'lib/heimdallr/proxy/record.rb', line 94

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:



109
110
111
112
113
114
115
# File 'lib/heimdallr/proxy/record.rb', line 109

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.



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

delegate :to_key, :to => :@record

#to_paramObject

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

Returns:

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