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
attributesmethod which removes all attributes without:viewpermission. -
#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
optionsintended for use insavemethods 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
-
#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
savemethod which verifies all of the dirty attributes to be valid for current security context. -
#save!(options = {}) ⇒ Object
A proxy for
savemethod which verifies all of the dirty attributes to be valid for current security context and mandates the current record to be valid. -
#toggle(field) ⇒ Object
Delegates to the corresponding method of underlying object.
-
#touch(field) ⇒ Object
Delegates to the corresponding method of underlying object.
-
#update_attributes(attributes, options = {}) ⇒ Object
A proxy for
update_attributesmethod. -
#update_attributes!(attributes, options = {}) ⇒ Object
A proxy for
update_attributes!method. -
#valid? ⇒ Object
Delegates to the corresponding method of underlying object.
Constructor Details
#initialize(context, record, options = {}) ⇒ Record
Create a record proxy.
18 19 20 21 22 |
# File 'lib/heimdallr/proxy/record.rb', line 18 def initialize(context, record, ={}) @context, @record, = context, record, @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.
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, ) 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, ) else result end elsif [: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.
131 |
# File 'lib/heimdallr/proxy/record.rb', line 131 delegate :assign_attributes, :to => :@record |
#attributes ⇒ Object
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_attributes ⇒ Object (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 () 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.
139 140 141 |
# File 'lib/heimdallr/proxy/record.rb', line 139 def class_name @record.class.name end |
#creatable? ⇒ 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
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 |
#errors ⇒ Object
Delegates to the corresponding method of underlying object.
127 |
# File 'lib/heimdallr/proxy/record.rb', line 127 delegate :errors, :to => :@record |
#explicit ⇒ Heimdallr::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, .merge(implicit: false)) end |
#implicit ⇒ Heimdallr::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, .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 |
#insecure ⇒ ActiveRecord::Base
Return the underlying object.
223 224 225 |
# File 'lib/heimdallr/proxy/record.rb', line 223 def insecure @record end |
#inspect ⇒ String
Describes the proxy and proxified object.
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
270 271 272 |
# File 'lib/heimdallr/proxy/record.rb', line 270 def modifiable? @restrictions.can? :update 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.
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: , restrictions: @restrictions, }.merge(@restrictions.reflection) end |
#restrict(context, options = nil) ⇒ Object
Records cannot be restricted with different context or options.
147 148 149 150 151 152 153 |
# File 'lib/heimdallr/proxy/record.rb', line 147 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.
81 82 83 84 85 86 87 |
# File 'lib/heimdallr/proxy/record.rb', line 81 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.
96 97 98 99 100 101 102 |
# File 'lib/heimdallr/proxy/record.rb', line 96 def save!(={}) check_attributes do @record.save!() 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, ={}) @record.with_transaction_returning_status do @record.assign_attributes(attributes, ) 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, ={}) @record.with_transaction_returning_status do @record.assign_attributes(attributes, ) 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 |