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

- old
+ new

@@ -1,28 +1,226 @@ module Heimdallr # A security-aware proxy for +ActiveRecord+ scopes. This class validates all the # method calls and either forwards them to the encapsulated scope or raises # an exception. + # + # There are two kinds of collection proxies, explicit and implicit, which instantiate + # the corresponding types of record proxies. See also {Proxy::Record}. class Proxy::Collection + include Enumerable + # Create a collection proxy. - # @param context security context - # @param object proxified scope - def initialize(context, scope) - @context, @scope = context, scope + # + # The +scope+ is expected to be already restricted with +:fetch+ scope. + # + # @param context security context + # @param scope proxified scope + # @option options [Boolean] implicit proxy type + def initialize(context, scope, options={}) + @context, @scope, @options = context, scope, options - @restrictions = @object.class.restrictions(context) + @restrictions = @scope.restrictions(context) end # Collections cannot be restricted twice. # # @raise [RuntimeError] - def restrict(context) + def restrict(*args) raise RuntimeError, "Collections cannot be restricted twice" end - # Dummy method_missing. - # @todo Write some actual dispatching logic. - def method_missing(method, *args, &block) - @scope.send method, *args + # @private + # @macro [attach] delegate_as_constructor + # A proxy for +$1+ method which adds fixtures to the attribute list and + # returns a restricted record. + def self.delegate_as_constructor(name, method) + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(attributes={}) + record = @restrictions.request_scope(:fetch).new.restrict(@context, @options) + record.#{method}(attributes.merge(@restrictions.fixtures[:create])) + record + end + EOM + end + + # @private + # @macro [attach] delegate_as_scope + # A proxy for +$1+ method which returns a restricted scope. + def self.delegate_as_scope(name) + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args) + Proxy::Collection.new(@context, @scope.#{name}(*args), @options) + end + EOM + end + + # @private + # @macro [attach] delegate_as_destroyer + # A proxy for +$1+ method which works on a +:delete+ scope. + def self.delegate_as_destroyer(name) + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args) + @restrictions.request_scope(:delete, @scope).#{name}(*args) + end + EOM + end + + # @private + # @macro [attach] delegate_as_record + # A proxy for +$1+ method which returns a restricted record. + def self.delegate_as_record(name) + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args) + @scope.#{name}(*args).restrict(@context, @options) + end + EOM + end + + # @private + # @macro [attach] delegate_as_records + # A proxy for +$1+ method which returns an array of restricted records. + def self.delegate_as_records(name) + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args) + @scope.#{name}(*args).map do |element| + element.restrict(@context, @options) + end + end + EOM + end + + # @private + # @macro [attach] delegate_as_value + # A proxy for +$1+ method which returns a raw value. + def self.delegate_as_value(name) + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args) + @scope.#{name}(*args) + end + EOM + end + + delegate_as_constructor :build, :assign_attributes + delegate_as_constructor :new, :assign_attributes + delegate_as_constructor :create, :update_attributes + delegate_as_constructor :create!, :update_attributes! + + delegate_as_scope :scoped + delegate_as_scope :uniq + delegate_as_scope :where + delegate_as_scope :joins + delegate_as_scope :includes + delegate_as_scope :eager_load + delegate_as_scope :preload + delegate_as_scope :lock + delegate_as_scope :limit + delegate_as_scope :offset + delegate_as_scope :order + delegate_as_scope :reorder + delegate_as_scope :reverse_order + delegate_as_scope :extending + + delegate_as_value :empty? + delegate_as_value :any? + delegate_as_value :many? + delegate_as_value :include? + delegate_as_value :exists? + delegate_as_value :size + delegate_as_value :length + + delegate_as_value :calculate + delegate_as_value :count + delegate_as_value :average + delegate_as_value :sum + delegate_as_value :maximum + delegate_as_value :minimum + delegate_as_value :pluck + + delegate_as_destroyer :delete + delegate_as_destroyer :delete_all + delegate_as_destroyer :destroy + delegate_as_destroyer :destroy_all + + delegate_as_record :first + delegate_as_record :first! + delegate_as_record :last + delegate_as_record :last! + + delegate_as_records :all + delegate_as_records :to_a + delegate_as_records :to_ary + + # A proxy for +find+ which restricts the returned record or records. + # + # @return [Proxy::Record, Array<Proxy::Record>] + def find(*args) + result = @scope.find(*args) + + if result.is_a? Enumerable + result.map do |element| + element.restrict(@context, @options) + end + else + result.restrict(@context, @options) + end + end + + # A proxy for +each+ which restricts the yielded records. + # + # @yield [record] + # @yieldparam [Proxy::Record] record + def each + @scope.each do |record| + yield record.restrict(@context, @options) + end + end + + # Wraps a scope or a record in a corresponding proxy. + def method_missing(method, *args) + if method =~ /^find_all_by/ + @scope.send(method, *args).map do |element| + element.restrict(@context, @options) + end + elsif method =~ /^find_by/ + @scope.send(method, *args).restrict(@context, @options) + elsif @scope.heimdallr_scopes && @scope.heimdallr_scopes.include?(method) + Proxy::Collection.new(@context, @scope.send(method, *args), @options) + elsif @scope.respond_to? method + raise InsecureOperationError, + "Potentially insecure method #{method} was called" + else + super + end + end + + # Return the underlying scope. + # + # @return ActiveRecord scope + def insecure + @scope + end + + # Describes the proxy and proxified scope. + # + # @return [String] + def inspect + "#<Heimdallr::Proxy::Collection: #{@scope.to_sql}>" + end + + # Return the associated security metadata. The returned hash will contain keys + # +:context+, +:scope+ and +: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: @scope, + context: @context, + scope: @scope, + options: @options + }.merge(@restrictions.reflection) end end end \ No newline at end of file