Class: Heimdallr::Evaluator
- Inherits:
-
Object
- Object
- Heimdallr::Evaluator
- Defined in:
- lib/heimdallr/evaluator.rb
Overview
Evaluator is a DSL for managing permissions on records with the field granularity. It works by evaluating a block of code within a given security context.
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.
Instance Attribute Summary collapse
-
#allowed_fields ⇒ Object
readonly
Returns the value of attribute allowed_fields.
-
#fixtures ⇒ Object
readonly
Returns the value of attribute fixtures.
-
#validators ⇒ Object
readonly
Returns the value of attribute validators.
DSL collapse
-
#can(actions, fields = @model_class.to_adapter.column_names) ⇒ Object
Define allowed operations for action(s).
-
#cannot(actions, fields) ⇒ Object
Revoke a permission on fields.
-
#scope(name, explicit_block = nil, &implicit_block) ⇒ Object
Define a scope.
Instance Method Summary collapse
-
#can?(action) ⇒ Boolean
Check if any explicit restrictions were defined for
action
. -
#create_validators(fields) ⇒ Array<ActiveModel::Validator>
protected
Create validators for
fields
inActiveModel::Validations
-like way. -
#evaluate(context, record = nil) ⇒ Object
Compute the restrictions for a given
context
and possibly a specificrecord
. -
#extract_fixtures(fields) ⇒ Object
protected
Collects fixtures from the
fields
definition. -
#initialize(model_class, block) ⇒ Evaluator
constructor
Create a new Evaluator for the
ActiveRecord
-derived classmodel_class
, and useblock
to infer restrictions for any security context passed. -
#reflection ⇒ Object
Return a Hash to be mixed in in
reflect_on_security
methods of Proxy::Collection and Proxy::Record. -
#request_scope(name = :fetch, basic_scope = nil) ⇒ Object
Request a scope.
Constructor Details
#initialize(model_class, block) ⇒ Evaluator
Create a new Evaluator for the ActiveRecord
-derived class model_class
, and use block
to infer restrictions for any security context passed.
17 18 19 20 21 22 23 24 |
# File 'lib/heimdallr/evaluator.rb', line 17 def initialize(model_class, block) @model_class, @block = model_class, block @scopes = {} @allowed_fields = {} @validators = {} @fixtures = {} end |
Instance Attribute Details
#allowed_fields ⇒ Object (readonly)
Returns the value of attribute allowed_fields.
13 14 15 |
# File 'lib/heimdallr/evaluator.rb', line 13 def allowed_fields @allowed_fields end |
#fixtures ⇒ Object (readonly)
Returns the value of attribute fixtures.
13 14 15 |
# File 'lib/heimdallr/evaluator.rb', line 13 def fixtures @fixtures end |
#validators ⇒ Object (readonly)
Returns the value of attribute validators.
13 14 15 |
# File 'lib/heimdallr/evaluator.rb', line 13 def validators @validators end |
Instance Method Details
#can(actions, fields = @model_class.to_adapter.column_names) ⇒ Object
Define allowed operations for action(s).
The fields
parameter accepts both Arrays and Hashes.
-
If an
Array
is passed, then all fields present in the array are whitelised. -
If a
Hash
is passed, then all fields present as hash keys are whitelisted, and:-
If a corresponding value is a
Hash
, it will be processed as a security validator. Security validators make records invalid when they are saved through a Proxy::Record. -
If the corresponding value is any other object, it will be added as a security fixture. Fixtures are merged when objects are created through restricted scopes, and cause exceptions to be raised when a record is saved, even through the
#save
method.
-
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/heimdallr/evaluator.rb', line 81 def can(actions, fields=@model_class.to_adapter.column_names) Array(actions).map(&:to_sym).each do |action| case fields when Hash # a list of validations @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).map(&:to_sym) end end end |
#can?(action) ⇒ Boolean
Check if any explicit restrictions were defined for action
. can :create, [] is an explicit restriction for action :create
.
136 137 138 |
# File 'lib/heimdallr/evaluator.rb', line 136 def can?(action) @allowed_fields.include? action end |
#cannot(actions, fields) ⇒ Object
Revoke validating restrictions.
Revoke a permission on fields.
103 104 105 106 107 108 |
# File 'lib/heimdallr/evaluator.rb', line 103 def cannot(actions, fields) Array(actions).map(&:to_sym).each do |action| @allowed_fields[action] -= fields.map(&:to_sym) fields.each { |field| @fixtures.delete field } end end |
#create_validators(fields) ⇒ Array<ActiveModel::Validator> (protected)
Create validators for fields
in ActiveModel::Validations
-like way.
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/heimdallr/evaluator.rb', line 185 def create_validators(fields) validators = [] fields.each do |attribute, validations| next unless validations.is_a? Hash validations.each do |key, | key = "#{key.to_s.camelize}Validator" begin validator = key.include?('::') ? key.constantize : ActiveModel::Validations.const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end validators << validator.new(().merge(:attributes => [ attribute ])) end end validators end |
#evaluate(context, record = nil) ⇒ Object
Compute the restrictions for a given context
and possibly a specific record
. Invokes a block
passed to the initialize
once.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/heimdallr/evaluator.rb', line 152 def evaluate(context, record=nil) if [context, record] != @last_context @scopes = {} @allowed_fields = Hash.new { [] } @validators = Hash.new { [] } @fixtures = Hash.new { {} } @allowed_fields[:view] += [ :id ] instance_exec context, record, &@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, record] end self end |
#extract_fixtures(fields) ⇒ Object (protected)
Collects fixtures from the fields
definition.
208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/heimdallr/evaluator.rb', line 208 def extract_fixtures(fields) fixtures = {} fields.each do |attribute, | next if .is_a? Hash fixtures[attribute.to_sym] = end fixtures end |
#reflection ⇒ Object
Return a Hash to be mixed in in reflect_on_security
methods of Proxy::Collection and Proxy::Record.
142 143 144 145 146 |
# File 'lib/heimdallr/evaluator.rb', line 142 def reflection { operations: [ :view, :create, :update ].select { |op| can? op } } end |
#request_scope(name = :fetch, basic_scope = nil) ⇒ Object
Request a scope.
120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/heimdallr/evaluator.rb', line 120 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 || request_scope(:fetch)).instance_exec(&@scopes[name]) end end |
#scope(name, block = nil) ⇒ Object #scope(name) ⇒ Object
Define a scope. A special :fetch
scope is applied to any other scope automatically.
49 50 51 52 53 54 55 |
# File 'lib/heimdallr/evaluator.rb', line 49 def scope(name, explicit_block=nil, &implicit_block) unless [:fetch, :delete].include?(name) raise "There is no such scope as #{name}" end @scopes[name] = explicit_block || implicit_block || -> { scoped } end |