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
# File 'lib/heimdallr/proxy/record.rb', line 18

def initialize(context, record, options={})
  @context, @record, @options = context, record, options

  @restrictions = @record.class.restrictions(context, record)
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:



170
171
172
173
174
175
176
177
178
179
180
181
182
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
# File 'lib/heimdallr/proxy/record.rb', line 170

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 (defined?(ActiveRecord) && @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
      referenced.restrict(@context, @options)
    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.



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

delegate :assign_attributes, :to => :@record

#attributesObject

A proxy for attributes method which removes all attributes without :view permission.



45
46
47
48
49
50
51
52
53
# File 'lib/heimdallr/proxy/record.rb', line 45

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.



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

delegate :attributes=, :to => :@record

#check_attributesObject (protected)

Raises an exception if any of the changed attributes are not valid for the current security context.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/heimdallr/proxy/record.rb', line 285

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.



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/heimdallr/proxy/record.rb', line 321

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)


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

def class_name
  @record.class.name
end

#creatable?Boolean

Returns:

  • (Boolean)


266
267
268
# File 'lib/heimdallr/proxy/record.rb', line 266

def creatable?
  @restrictions.can? :create
end

#decrement(field, by = 1) ⇒ Object

Delegates to the corresponding method of underlying object.



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

delegate :decrement, :to => :@record

#destroyable?Boolean

Returns:

  • (Boolean)


274
275
276
277
# File 'lib/heimdallr/proxy/record.rb', line 274

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.



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

delegate :errors, :to => :@record

#explicitHeimdallr::Proxy::Record

Return an explicit variant of this proxy.



237
238
239
# File 'lib/heimdallr/proxy/record.rb', line 237

def explicit
  Proxy::Record.new(@context, @record, @options.merge(implicit: false))
end

#implicitHeimdallr::Proxy::Record

Return an implicit variant of this proxy.



230
231
232
# File 'lib/heimdallr/proxy/record.rb', line 230

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.



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

delegate :increment, :to => :@record

#insecureActiveRecord::Base

Return the underlying object.

Returns:

  • (ActiveRecord::Base)


223
224
225
# File 'lib/heimdallr/proxy/record.rb', line 223

def insecure
  @record
end

#inspectString

Describes the proxy and proxified object.

Returns:

  • (String)


244
245
246
# File 'lib/heimdallr/proxy/record.rb', line 244

def inspect
  "#<Heimdallr::Proxy::Record: #{@record.inspect}>"
end

#invalid?Object

Delegates to the corresponding method of underlying object.



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

delegate :invalid?, :to => :@record

#modifiable?Boolean

Returns:

  • (Boolean)


270
271
272
# File 'lib/heimdallr/proxy/record.rb', line 270

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)


256
257
258
259
260
261
262
263
264
# File 'lib/heimdallr/proxy/record.rb', line 256

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)


147
148
149
150
151
152
153
# File 'lib/heimdallr/proxy/record.rb', line 147

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.



81
82
83
84
85
86
87
# File 'lib/heimdallr/proxy/record.rb', line 81

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:



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

def save!(options={})
  check_save_options options

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

#toggle(field) ⇒ Object

Delegates to the corresponding method of underlying object.



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

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.



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

delegate :touch, :to => :@record

#update_attributes(attributes, options = {}) ⇒ Object

A proxy for update_attributes method. See also #save.



59
60
61
62
63
64
# File 'lib/heimdallr/proxy/record.rb', line 59

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!.



70
71
72
73
74
75
# File 'lib/heimdallr/proxy/record.rb', line 70

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.



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

delegate :valid?, :to => :@record