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