lib/heimdallr/evaluator.rb in heimdallr-0.0.2 vs lib/heimdallr/evaluator.rb in heimdallr-1.0.0.RC2

- old
+ new

@@ -4,22 +4,24 @@ # # The default resolution is to forbid everything--that is, Heimdallr security policy # is whitelisting safe actions, not blacklisting unsafe ones. This is by design # and is not going to change. # + # The field +#id+ is whitelisted by default. + # # The DSL consists of three functions: {#scope}, {#can} and {#cannot}. class Evaluator attr_reader :allowed_fields, :fixtures, :validators - # Create a new Evaluator for the ActiveModel-descending class +model_class+, + # Create a new Evaluator for the +ActiveRecord+-derived class +model_class+, # and use +block+ to infer restrictions for any security context passed. def initialize(model_class, block) @model_class, @block = model_class, block @scopes = {} @allowed_fields = {} - @validations = {} + @validators = {} @fixtures = {} end # @group DSL @@ -34,11 +36,11 @@ # # @overload scope(name) # This form accepts an implicit lambda. # # @example - # scope :fetch do + # scope :fetch do |user| # if user.manager? # scoped # else # where(:invisible => false) # end @@ -70,31 +72,34 @@ # can [:create, :update], { priority: { inclusion: 1..10 } } # # @param [Symbol, Array<Symbol>] actions one or more action names # @param [Hash<Hash, Object>] fields field restrictions def can(actions, fields=@model_class.attribute_names) - Array(actions).each do |action| + Array(actions).map(&:to_sym).each do |action| case fields when Hash # a list of validations - @allowed_fields[action] += fields.keys - @validations[action] += create_validators(fields) - @fixtures[action].merge extract_fixtures(fields) + @allowed_fields[action] += fields.keys.map(&:to_sym) + @validators[action] += create_validators(fields) + fixtures = extract_fixtures(fields) + @fixtures[action] = @fixtures[action].merge fixtures + @allowed_fields[action] -= fixtures.keys + else # an array or a field name - @allowed_fields[action] += Array(fields) + @allowed_fields[action] += Array(fields).map(&:to_sym) end end end # Revoke a permission on fields. # # @todo Revoke validating restrictions. # @param [Symbol, Array<Symbol>] actions one or more action names # @param [Array<Symbol>] fields field list def cannot(actions, fields) - Array(actions).each do |action| - @allowed_fields[action] -= fields + Array(actions).map(&:to_sym).each do |action| + @allowed_fields[action] -= fields.map(&:to_sym) @fixtures.delete_at *fields end end # @endgroup @@ -102,30 +107,64 @@ # Request a scope. # # @param scope name of the scope # @param basic_scope the scope to which scope +name+ will be applied. Defaults to +:fetch+. # - # @return ActiveRecord scope - def request_scope(name=:fetch, basic_scope=request_scope(:fetch)) - if name == :fetch || !@scopes.has_key?(name) - fetch_scope = @model_class.instance_exec(&@scopes[:fetch]) + # @return +ActiveRecord+ scope. + # + # @raise [RuntimeError] if the scope is not defined + def request_scope(name=:fetch, basic_scope=nil) + unless @scopes.has_key?(name) + raise RuntimeError, "The #{name.inspect} scope does not exist" + end + + if name == :fetch && basic_scope.nil? + @model_class.instance_exec(&@scopes[:fetch]) else - basic_scope.instance_exec(&@scopes[name]) + (basic_scope || request_scope(:fetch)).instance_exec(&@scopes[name]) end end + # Check if any explicit restrictions were defined for +action+. + # +can :create, []+ _is_ an explicit restriction for action +:create+. + # + # @return Boolean + def can?(action) + @allowed_fields.include? action + end + + # Return a Hash to be mixed in in +reflect_on_security+ methods of {Proxy::Collection} + # and {Proxy::Record}. + def reflection + { + operations: [ :view, :create, :update ].select { |op| can? op } + } + end + # Compute the restrictions for a given +context+. Invokes a +block+ passed to the # +initialize+ once. + # + # @raise [RuntimeError] if the evaluated block did not define a set of valid restrictions def evaluate(context) if context != @last_context @scopes = {} @allowed_fields = Hash.new { [] } @validators = Hash.new { [] } - @fixtures = Hash.new { [] } + @fixtures = Hash.new { {} } - instance_exec context, &block + @allowed_fields[:view] += [ :id ] + instance_exec context, &@block + + unless @scopes[:fetch] + raise RuntimeError, "A :fetch scope must be defined" + end + + @allowed_fields.each do |action, fields| + fields.uniq! + end + [@scopes, @allowed_fields, @validators, @fixtures]. map(&:freeze) @last_context = context end @@ -137,11 +176,11 @@ # Create validators for +fields+ in +ActiveModel::Validations+-like way. # # @return [Array<ActiveModel::Validator>] def create_validators(fields) - validators = {} + validators = [] fields.each do |attribute, validations| next unless validations.is_a? Hash validations.each do |key, options| @@ -151,11 +190,11 @@ validator = key.include?('::') ? key.constantize : ActiveModel::Validations.const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end - validators[attribute] = validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ])) + validators << validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ])) end end validators end @@ -165,10 +204,10 @@ fixtures = {} fields.each do |attribute, options| next if options.is_a? Hash - fixtures[attribute] = options + fixtures[attribute.to_sym] = options end fixtures end \ No newline at end of file