lib/heimdallr/proxy/record.rb in heimdallr-0.0.2 vs lib/heimdallr/proxy/record.rb in heimdallr-1.0.0.RC2

- old
+ new

@@ -3,16 +3,22 @@ # 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. class Proxy::Record # Create a record proxy. - # @param context security context - # @param object proxified record - def initialize(context, record) - @context, @record = context, record + # + # @param context security context + # @param object proxified record + # @option options [Boolean] implicit proxy type + def initialize(context, record, options={}) + @context, @record, @options = context, record, options @restrictions = @record.class.restrictions(context) end # @method decrement(field, by=1) @@ -35,32 +41,38 @@ delegate :touch, :to => :@record # A proxy for +attributes+ method which removes all attributes # without +:view+ permission. def attributes - @restrictions.filter_attributes(:view, @record.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 - # A proxy for +update_attributes+ method which removes all attributes - # without +:update+ permission and invokes +#save+. + # A proxy for +update_attributes+ method. + # See also {#save}. # # @raise [Heimdallr::PermissionError] def update_attributes(attributes, options={}) @record.with_transaction_returning_status do @record.assign_attributes(attributes, options) - self.save + save end end - # A proxy for +update_attributes!+ method which removes all attributes - # without +:update+ permission and invokes +#save!+. + # A proxy for +update_attributes!+ method. + # See also {#save!}. # # @raise [Heimdallr::PermissionError] - def update_attributes(attributes, options={}) + def update_attributes!(attributes, options={}) @record.with_transaction_returning_status do @record.assign_attributes(attributes, options) - self.save! + save! end end # A proxy for +save+ method which verifies all of the dirty attributes to # be valid for current security context. @@ -91,17 +103,39 @@ [:delete, :destroy].each do |method| class_eval(<<-EOM, __FILE__, __LINE__) def #{method} scope = @restrictions.request_scope(:delete) - if scope.where({ @record.primary_key => @record.to_key }).count != 0 + if scope.where({ @record.class.primary_key => @record.to_key }).count != 0 @record.#{method} end end EOM end + # @method valid? + # @macro delegate + delegate :valid?, :to => :@record + + # @method invalid? + # @macro delegate + delegate :invalid?, :to => :@record + + # @method errors + # @macro delegate + delegate :errors, :to => :@record + + # @method assign_attributes + # @macro delegate + delegate :assign_attributes, :to => :@record + + # Class name of the underlying model. + # @return [String] + def class_name + @record.class.name + end + # Records cannot be restricted twice. # # @raise [RuntimeError] def restrict(context) raise RuntimeError, "Records cannot be restricted twice" @@ -129,32 +163,40 @@ else normalized_method = method suffix = nil end - if defined?(ActiveRecord) && @record.is_a?(ActiveRecord::Base) && - association = @record.class.reflect_on_association(method) + 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.respond_to? :restrict - referenced.restrict(@context) + 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." + "Attempt to fetch insecure association #{method}. Try #insecure" end elsif @record.respond_to? method - if [nil, '?'].include?(suffix) && - @restrictions.allowed_fields[:view].include?(normalized_method) - # Reading an attribute - @record.send method, *args, &block + 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} on #{@action}." + "Non-whitelisted method #{method} is called for #{@record.inspect} " end else super end end @@ -164,30 +206,46 @@ # @return [ActiveRecord::Base] def insecure @record end + # Return an implicit variant of this proxy. + # + # @return [Heimdallr::Proxy::Record] + def implicit + Proxy::Record.new(@context, @record, @options.merge(implicit: true)) + end + + # Return an explicit variant of this proxy. + # + # @return [Heimdallr::Proxy::Record] + def explicit + Proxy::Record.new(@context, @record, @options.merge(implicit: false)) + end + # Describes the proxy and proxified object. # # @return [String] def inspect - "#<Heimdallr::Proxy(#{@action}): #{@record.inspect}>" + "#<Heimdallr::Proxy::Record: #{@record.inspect}>" end # Return the associated security metadata. The returned hash will contain keys - # +:context+ and +:object+, corresponding to the parameters in - # {#initialize}. + # +: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. # # @return [Hash] def reflect_on_security { + model: @record.class, context: @context, - object: @record - } + record: @record, + options: @options + }.merge(@restrictions.reflection) end protected # Raises an exception if any of the changed attributes are not valid @@ -201,21 +259,25 @@ action = :create else action = :update end - fixtures = @restrictions.fixtures[action] - validators = @restrictions.validators[action] + allowed_fields = @restrictions.allowed_fields[action] + fixtures = @restrictions.fixtures[action] + validators = @restrictions.validators[action] - @record.changed.each do |attribute| + @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 @@ -228,9 +290,21 @@ # methods are potentially unsafe. 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 end end \ No newline at end of file