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

- (Record) initialize(context, record, options = {})

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)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

- (Object) method_missing(method, *args, &block)

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:



159
160
161
162
163
164
165
166
167
168
169
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
# File 'lib/heimdallr/proxy/record.rb', line 159

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)
        @record.send method, *args, &block
      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

- (Object) assign_attributes

Delegates to the corresponding method of underlying object.



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

delegate :assign_attributes, :to => :@record

- (Object) attributes

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

- (Object) check_attributes (protected)

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



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/heimdallr/proxy/record.rb', line 255

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

- (Object) check_save_options(options) (protected)

Raises an exception if any of the options intended for use in save methods are potentially unsafe.



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/heimdallr/proxy/record.rb', line 291

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

- (String) class_name

Class name of the underlying model.

Returns:

  • (String)


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

def class_name
  @record.class.name
end

- (Object) decrement(field, by = 1)

Delegates to the corresponding method of underlying object.



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

delegate :decrement, :to => :@record

- (Object) errors

Delegates to the corresponding method of underlying object.



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

delegate :errors, :to => :@record

- (Heimdallr::Proxy::Record) explicit

Return an explicit variant of this proxy.



221
222
223
# File 'lib/heimdallr/proxy/record.rb', line 221

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

- (Heimdallr::Proxy::Record) implicit

Return an implicit variant of this proxy.



214
215
216
# File 'lib/heimdallr/proxy/record.rb', line 214

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

- (Object) increment(field, by = 1)

Delegates to the corresponding method of underlying object.



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

delegate :increment, :to => :@record

- (ActiveRecord::Base) insecure

Return the underlying object.

Returns:

  • (ActiveRecord::Base)


207
208
209
# File 'lib/heimdallr/proxy/record.rb', line 207

def insecure
  @record
end

- (String) inspect

Describes the proxy and proxified object.

Returns:

  • (String)


228
229
230
# File 'lib/heimdallr/proxy/record.rb', line 228

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

- (Object) invalid?

Delegates to the corresponding method of underlying object.



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

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

- (Hash) reflect_on_security

Return the associated security metadata. The returned hash will contain keys :context, :record, :options, corresponding to the parameters in #initialize, and :model, representing the model class.

Such a name was deliberately selected for this method in order to reduce namespace pollution.

Returns:

  • (Hash)


240
241
242
243
244
245
246
247
# File 'lib/heimdallr/proxy/record.rb', line 240

def reflect_on_security
  {
    model:   @record.class,
    context: @context,
    record:  @record,
    options: @options
  }.merge(@restrictions.reflection)
end

- (Object) restrict(context)

Records cannot be restricted twice.

Raises:

  • (RuntimeError)


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

def restrict(context)
  raise RuntimeError, "Records cannot be restricted twice"
end

- (Object) save(options = {})

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

- (Object) save!(options = {})

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

- (Object) toggle(field)

Delegates to the corresponding method of underlying object.



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

delegate :toggle, :to => :@record

- (Object) touch(field)

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

- (Object) update_attributes(attributes, options = {})

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

- (Object) update_attributes!(attributes, options = {})

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

- (Object) valid?

Delegates to the corresponding method of underlying object.



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

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