forest_liana 5.1.3 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 403847f23770b011b6551b87228a5d1f12cff04d9fa96b2389137279517ee48c
4
- data.tar.gz: 2900ede987dfbff932097a05f367e1247a37ac2a00be6e611f6ee14b730c15c2
3
+ metadata.gz: e035848d2e7346973187338d78579301dcf062b2b6192ba383cd1f50dcccf584
4
+ data.tar.gz: 5f8a3ecdea7a8f4643922c27626397bfa3d497c14302f77f377153a22f064b0f
5
5
  SHA512:
6
- metadata.gz: c5ee8dd97d27c5c78f6b9146cf98f41d1dbc6f72c4e4e70d662fae4d4b1f3b6d45f4339f6a9f43d74d84d193fb9001bce8662f5f6c2846b5f49ccbdec7cc8a22
7
- data.tar.gz: 423aea81ffed1126246a21ba1205a514d45b7fd5f924cd041be01f94b003099c1e6079af83660e6fd405f9d2e8481634677d772ac53eaf7ae793a15f5a4776aa
6
+ metadata.gz: a30b760625155ae65acf9c8d6941e1e8ddd408b98cd377fdd6f6385c943ceef74e72cf3318d1373f966f8eda48dd9c35c4198bdaeecba2c9c6ffd52f2285e465
7
+ data.tar.gz: 52b468bfea30b0c115c318ab60eefd2caf382c94df171ea53ec0e203d6869d823f0deb23b20f66939f2560a8611d55b2c6b64f1dcb0b5ffb3d461de3471db8a5
@@ -1,7 +1,89 @@
1
1
  module ForestLiana
2
2
  class ActionsController < ForestLiana::BaseController
3
+
3
4
  def values
4
5
  render serializer: nil, json: {}, status: :ok
5
6
  end
7
+
8
+ def get_collection(collection_name)
9
+ ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
10
+ end
11
+
12
+ def get_action(collection_name)
13
+ collection = get_collection(collection_name)
14
+ begin
15
+ collection.actions.find {|action| ActiveSupport::Inflector.parameterize(action.name) == params[:action_name]}
16
+ rescue => error
17
+ FOREST_LOGGER.error "Smart Action get action retrieval error: #{error}"
18
+ nil
19
+ end
20
+ end
21
+
22
+ def get_record
23
+ model = ForestLiana::SchemaUtils.find_model_from_collection_name(params[:collectionName])
24
+ redord_getter = ForestLiana::ResourceGetter.new(model, {:id => params[:recordIds][0]})
25
+ redord_getter.perform
26
+ redord_getter.record
27
+ end
28
+
29
+ def get_smart_action_load_ctx(fields)
30
+ fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.merge!(value: nil))}
31
+ {:record => get_record, :fields => fields}
32
+ end
33
+
34
+ def get_smart_action_change_ctx(fields)
35
+ fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.permit!.to_h)}
36
+ {:record => get_record, :fields => fields}
37
+ end
38
+
39
+ def handle_result(result, formatted_fields, action)
40
+ if result.nil? || !result.is_a?(Hash)
41
+ return render status: 500, json: { error: 'Error in smart action load hook: hook must return an object' }
42
+ end
43
+ is_same_data_structure = ForestLiana::IsSameDataStructureHelper::Analyser.new(formatted_fields, result, 1)
44
+ unless is_same_data_structure.perform
45
+ return render status: 500, json: { error: 'Error in smart action hook: fields must be unchanged (no addition nor deletion allowed)' }
46
+ end
47
+
48
+ # Apply result on fields (transform the object back to an array), preserve order.
49
+ fields = action.fields.map { |field| result[field[:field]] }
50
+
51
+ render serializer: nil, json: { fields: fields}, status: :ok
52
+ end
53
+
54
+ def load
55
+ action = get_action(params[:collectionName])
56
+
57
+ if !action
58
+ render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
59
+ else
60
+ # Transform fields from array to an object to ease usage in hook, adds null value.
61
+ context = get_smart_action_load_ctx(action.fields)
62
+ formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
63
+
64
+ # Call the user-defined load hook.
65
+ result = action.hooks[:load].(context)
66
+
67
+ handle_result(result, formatted_fields, action)
68
+ end
69
+ end
70
+
71
+ def change
72
+ action = get_action(params[:collectionName])
73
+
74
+ if !action
75
+ render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
76
+ else
77
+ # Transform fields from array to an object to ease usage in hook.
78
+ context = get_smart_action_change_ctx(params[:fields])
79
+ formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
80
+
81
+ # Call the user-defined change hook.
82
+ field_name = params[:fields].select { |field| field[:value] != field[:previousValue] }[0][:field]
83
+ result = action.hooks[:change][field_name].(context)
84
+
85
+ handle_result(result, formatted_fields, action)
86
+ end
87
+ end
6
88
  end
7
89
  end
@@ -22,7 +22,13 @@ module ForestLiana
22
22
  checker = ForestLiana::PermissionsChecker.new(@resource, 'searchToEdit', @rendering_id)
23
23
  return head :forbidden unless checker.is_authorized?
24
24
  else
25
- checker = ForestLiana::PermissionsChecker.new(@resource, 'list', @rendering_id)
25
+ checker = ForestLiana::PermissionsChecker.new(
26
+ @resource,
27
+ 'list',
28
+ @rendering_id,
29
+ nil,
30
+ get_collection_list_permission_info(forest_user, request)
31
+ )
26
32
  return head :forbidden unless checker.is_authorized?
27
33
  end
28
34
 
@@ -51,7 +57,13 @@ module ForestLiana
51
57
 
52
58
  def count
53
59
  begin
54
- checker = ForestLiana::PermissionsChecker.new(@resource, 'list', @rendering_id)
60
+ checker = ForestLiana::PermissionsChecker.new(
61
+ @resource,
62
+ 'list',
63
+ @rendering_id,
64
+ nil,
65
+ get_collection_list_permission_info(forest_user, request)
66
+ )
55
67
  return head :forbidden unless checker.is_authorized?
56
68
 
57
69
  getter = ForestLiana::ResourcesGetter.new(@resource, params)
@@ -232,5 +244,15 @@ module ForestLiana
232
244
  collection_name = ForestLiana.name_for(@resource)
233
245
  @collection ||= ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
234
246
  end
247
+
248
+ # NOTICE: Return a formatted object containing the request condition filters and
249
+ # the user id used by the scope validator class to validate if scope is
250
+ # in request
251
+ def get_collection_list_permission_info(user, collection_list_request)
252
+ {
253
+ user_id: user['id'],
254
+ filters: collection_list_request[:filters],
255
+ }
256
+ end
235
257
  end
236
258
  end
@@ -0,0 +1,44 @@
1
+ require 'set'
2
+
3
+ module ForestLiana
4
+ module IsSameDataStructureHelper
5
+ class Analyser
6
+ def initialize(object, other, deep = 0)
7
+ @object = object
8
+ @other = other
9
+ @deep = deep
10
+ end
11
+
12
+ def are_objects(object, other)
13
+ object && other && object.is_a?(Hash) && other.is_a?(Hash)
14
+ end
15
+
16
+ def check_keys(object, other, step = 0)
17
+ unless are_objects(object, other)
18
+ return false
19
+ end
20
+
21
+ object_keys = object.keys
22
+ other_keys = other.keys
23
+
24
+ if object_keys.length != other_keys.length
25
+ return false
26
+ end
27
+
28
+ object_keys_set = object_keys.to_set
29
+ other_keys.each { |key|
30
+ if !object_keys_set.member?(key) || (step + 1 <= @deep && !check_keys(object[key], other[key], step + 1))
31
+ return false
32
+ end
33
+ }
34
+
35
+ return true
36
+ end
37
+
38
+ def perform
39
+ check_keys(@object, @other)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -5,7 +5,7 @@ class ForestLiana::Model::Action
5
5
  extend ActiveModel::Naming
6
6
 
7
7
  attr_accessor :id, :name, :base_url, :endpoint, :http_method, :fields, :redirect,
8
- :type, :download
8
+ :type, :download, :hooks
9
9
 
10
10
  def initialize(attributes = {})
11
11
  if attributes.key?(:global)
@@ -66,6 +66,7 @@ class ForestLiana::Model::Action
66
66
  @base_url ||= nil
67
67
  @type ||= "bulk"
68
68
  @download ||= false
69
+ @hooks = !@hooks.nil? ? @hooks.symbolize_keys : nil
69
70
  end
70
71
 
71
72
  def persisted?
@@ -39,6 +39,7 @@ module ForestLiana
39
39
  'redirect',
40
40
  'download',
41
41
  'fields',
42
+ 'hooks',
42
43
  ]
43
44
  KEYS_ACTION_FIELD = [
44
45
  'field',
@@ -81,11 +81,15 @@ module ForestLiana
81
81
  parsed_field = parse_field_name(field)
82
82
  parsed_operator = parse_operator(operator)
83
83
  parsed_value = parse_value(operator, value)
84
+ field_and_operator = "#{parsed_field} #{parsed_operator}"
84
85
 
85
- if Rails::VERSION::MAJOR >= 5
86
- ActiveRecord::Base.sanitize_sql(["#{parsed_field} #{parsed_operator} ?", parsed_value])
86
+ if Rails::VERSION::MAJOR < 5
87
+ "#{field_and_operator} #{ActiveRecord::Base.sanitize(parsed_value)}"
88
+ # NOTICE: sanitize method as been removed in Rails 5.1 and sanitize_sql introduced in Rails 5.2.
89
+ elsif Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR == 1
90
+ "#{field_and_operator} #{ActiveRecord::Base.connection.quote(parsed_value)}"
87
91
  else
88
- "#{parsed_field} #{parsed_operator} #{ActiveRecord::Base.sanitize(parsed_value)}"
92
+ ActiveRecord::Base.sanitize_sql(["#{field_and_operator} ?", parsed_value])
89
93
  end
90
94
  end
91
95
 
@@ -3,11 +3,12 @@ module ForestLiana
3
3
  @@permissions_per_rendering = Hash.new
4
4
  @@expiration_in_seconds = (ENV['FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS'] || 3600).to_i
5
5
 
6
- def initialize(resource, permission_name, rendering_id, smart_action_parameters = nil)
6
+ def initialize(resource, permission_name, rendering_id, smart_action_parameters = nil, collection_list_parameters = nil)
7
7
  @collection_name = ForestLiana.name_for(resource)
8
8
  @permission_name = permission_name
9
9
  @rendering_id = rendering_id
10
10
  @smart_action_parameters = smart_action_parameters
11
+ @collection_list_parameters = collection_list_parameters
11
12
  end
12
13
 
13
14
  def is_authorized?
@@ -46,12 +47,23 @@ module ForestLiana
46
47
  return @allowed && (@users.nil?|| @users.include?(@user_id.to_i));
47
48
  end
48
49
 
50
+ def collection_list_allowed?(scope_permissions)
51
+ return ForestLiana::ScopeValidator.new(
52
+ scope_permissions['filter'],
53
+ scope_permissions['dynamicScopesValues']['users']
54
+ ).is_scope_in_request?(@collection_list_parameters)
55
+ end
56
+
49
57
  def is_allowed?
50
58
  permissions = get_permissions
51
59
  if permissions && permissions[@collection_name] &&
52
60
  permissions[@collection_name]['collection']
53
61
  if @permission_name === 'actions'
54
62
  return smart_action_allowed?(permissions[@collection_name]['actions'])
63
+ # NOTICE: Permissions[@collection_name]['scope'] will either contains conditions filter and
64
+ # dynamic user values definition, or null for collection that does not use scopes
65
+ elsif @permission_name === 'list' and permissions[@collection_name]['scope']
66
+ return collection_list_allowed?(permissions[@collection_name]['scope'])
55
67
  else
56
68
  return permissions[@collection_name]['collection'][@permission_name]
57
69
  end
@@ -12,11 +12,11 @@ module ForestLiana
12
12
  @collection = get_collection(@collection_name)
13
13
  @fields_to_serialize = get_fields_to_serialize
14
14
  @field_names_requested = field_names_requested
15
- get_segment()
16
- compute_includes()
15
+ get_segment
16
+ compute_includes
17
17
  @search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection)
18
18
 
19
- prepare_query()
19
+ prepare_query
20
20
  end
21
21
 
22
22
  def self.get_ids_from_request(params)
@@ -0,0 +1,97 @@
1
+ module ForestLiana
2
+ class ScopeValidator
3
+ def initialize(scope_permissions, users_variable_values)
4
+ @scope_filters = scope_permissions
5
+ @users_variable_values = users_variable_values
6
+ end
7
+
8
+ def is_scope_in_request?(scope_request)
9
+ begin
10
+ filters = JSON.parse(scope_request[:filters])
11
+ rescue JSON::ParserError
12
+ raise ForestLiana::Errors::HTTP422Error.new('Invalid filters JSON format')
13
+ end
14
+ @computed_scope = compute_condition_filters_from_scope(scope_request[:user_id])
15
+
16
+ # NOTICE: Perfom a travel in the request condition filters tree to find the scope
17
+ tagged_scope_filters = get_scope_found_in_request(filters)
18
+
19
+ # NOTICE: Permission system always send an aggregator even if there is only one condition
20
+ # In that case, if the condition is valid, then request was not edited
21
+ return !tagged_scope_filters.nil? if @scope_filters['conditions'].length == 1
22
+
23
+ # NOTICE: If there is more than one condition, do a final validation on the condition filters
24
+ return tagged_scope_filters != nil &&
25
+ tagged_scope_filters[:aggregator] == @scope_filters['aggregator'] &&
26
+ tagged_scope_filters[:conditions] &&
27
+ tagged_scope_filters[:conditions].length == @scope_filters['conditions'].length
28
+ end
29
+
30
+ private
31
+
32
+ def compute_condition_filters_from_scope(user_id)
33
+ computed_condition_filters = @scope_filters.clone
34
+ computed_condition_filters['conditions'].each do |condition|
35
+ if condition.include?('value') &&
36
+ !condition['value'].nil? &&
37
+ condition['value'].start_with?('$') &&
38
+ @users_variable_values.include?(user_id)
39
+ condition['value'] = @users_variable_values[user_id][condition['value']]
40
+ end
41
+ end
42
+ return computed_condition_filters
43
+ end
44
+
45
+ def get_scope_found_in_request(filters)
46
+ return nil unless filters
47
+ return search_scope_aggregation(filters)
48
+ end
49
+
50
+ def search_scope_aggregation(node)
51
+ ensure_valid_aggregation(node)
52
+
53
+ return is_scope_condition?(node) unless node['aggregator']
54
+
55
+ # NOTICE: Remove conditions that are not from the scope
56
+ filtered_conditions = node['conditions'].map { |condition|
57
+ search_scope_aggregation(condition)
58
+ }.select { |condition|
59
+ condition
60
+ }
61
+
62
+ # NOTICE: If there is only one condition filter left and its current aggregator is
63
+ # an "and", this condition filter is the searched scope
64
+ if (filtered_conditions.length == 1 &&
65
+ filtered_conditions.first.is_a?(Hash) &&
66
+ filtered_conditions.first.include?(:aggregator) &&
67
+ node['aggregator'] == 'and')
68
+ return filtered_conditions.first
69
+ end
70
+
71
+ # NOTICE: Otherwise, validate if the current node is the scope and return nil
72
+ # if it's not
73
+ return (filtered_conditions.length == @scope_filters['conditions'].length &&
74
+ node['aggregator'] == @scope_filters['aggregator']) ?
75
+ { aggregator: node['aggregator'], conditions: filtered_conditions } :
76
+ nil
77
+ end
78
+
79
+ def is_scope_condition?(condition)
80
+ ensure_valid_condition(condition)
81
+ return @computed_scope['conditions'].include?(condition)
82
+ end
83
+
84
+ def ensure_valid_aggregation(node)
85
+ raise ForestLiana::Errors::HTTP422Error.new('Filters cannot be a raw value') unless node.is_a?(Hash)
86
+ raise_empty_condition_in_filter_error if node.empty?
87
+ end
88
+
89
+ def ensure_valid_condition(condition)
90
+ raise_empty_condition_in_filter_error if condition.empty?
91
+ raise ForestLiana::Errors::HTTP422Error.new('Condition cannot be a raw value') unless condition.is_a?(Hash)
92
+ unless condition['field'].is_a?(String) and condition['operator'].is_a?(String)
93
+ raise ForestLiana::Errors::HTTP422Error.new('Invalid condition format')
94
+ end
95
+ end
96
+ end
97
+ end
@@ -57,4 +57,6 @@ ForestLiana::Engine.routes.draw do
57
57
 
58
58
  # Smart Actions forms value
59
59
  post 'actions/:action_name/values' => 'actions#values'
60
+ post 'actions/:action_name/hooks/load' => 'actions#load'
61
+ post 'actions/:action_name/hooks/change' => 'actions#change'
60
62
  end
@@ -38,6 +38,14 @@ module ForestLiana
38
38
 
39
39
  private
40
40
 
41
+ def get_collection(collection_name)
42
+ ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
43
+ end
44
+
45
+ def get_action(collection, action_name)
46
+ collection.actions.find {|action| action.name == action_name}
47
+ end
48
+
41
49
  def generate_apimap
42
50
  create_apimap
43
51
  require_lib_forest_liana
@@ -45,6 +53,17 @@ module ForestLiana
45
53
 
46
54
  if Rails.env.development?
47
55
  @collections_sent = ForestLiana.apimap.as_json
56
+
57
+ @collections_sent.each do |collection|
58
+ collection['actions'].each do |action|
59
+ c = get_collection(collection['name'])
60
+ a = get_action(c, action['name'])
61
+ load = !a.hooks.nil? && a.hooks.key?(:load) && a.hooks[:load].is_a?(Proc)
62
+ change = !a.hooks.nil? && a.hooks.key?(:change) && a.hooks[:change].is_a?(Hash) ? a.hooks[:change].keys : []
63
+ action['hooks'] = {:load => load, :change => change}
64
+ end
65
+ end
66
+
48
67
  @meta_sent = ForestLiana.meta
49
68
  SchemaFileUpdater.new(SCHEMA_FILENAME, @collections_sent, @meta_sent).perform()
50
69
  else
@@ -175,7 +194,6 @@ module ForestLiana
175
194
  def setup_forest_liana_meta
176
195
  ForestLiana.meta = {
177
196
  database_type: database_type,
178
- framework_version: Gem.loaded_specs["rails"].version.version,
179
197
  liana: 'forest-rails',
180
198
  liana_version: ForestLiana::VERSION,
181
199
  orm_version: Gem.loaded_specs["activerecord"].version.version
@@ -50,6 +50,7 @@ module ForestLiana
50
50
  'redirect',
51
51
  'download',
52
52
  'fields',
53
+ 'hooks',
53
54
  ]
54
55
  KEYS_ACTION_FIELD = [
55
56
  'field',
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "5.1.3"
2
+ VERSION = "5.3.0"
3
3
  end
@@ -0,0 +1,87 @@
1
+ module ForestLiana
2
+ context 'IsSameDataStructure class' do
3
+ it 'should: be valid with simple data' do
4
+ object = {:a => 'a', :b => 'b'}
5
+ other = {:a => 'a', :b => 'b'}
6
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
7
+ expect(result).to be true
8
+ end
9
+
10
+ it 'should: be invalid with simple data' do
11
+ object = {:a => 'a', :b => 'b'}
12
+ other = {:a => 'a', :c => 'c'}
13
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
14
+ expect(result).to be false
15
+ end
16
+
17
+ it 'should: be invalid with not same hash' do
18
+ object = {:a => 'a', :b => 'b'}
19
+ other = {:a => 'a', :b => 'b', :c => 'c'}
20
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
21
+ expect(result).to be false
22
+ end
23
+
24
+ it 'should: be invalid with nil' do
25
+ object = nil
26
+ other = {:a => 'a', :b => 'b', :c => 'c'}
27
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
28
+ expect(result).to be false
29
+ end
30
+
31
+ it 'should: be invalid with not hash' do
32
+ object = nil
33
+ other = {:a => 'a', :b => 'b', :c => 'c'}
34
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
35
+ expect(result).to be false
36
+ end
37
+
38
+ it 'should: be invalid with integer' do
39
+ object = 1
40
+ other = {:a => 'a', :b => 'b', :c => 'c'}
41
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
42
+ expect(result).to be false
43
+ end
44
+
45
+ it 'should: be invalid with string' do
46
+ object = 'a'
47
+ other = {:a => 'a', :b => 'b', :c => 'c'}
48
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
49
+ expect(result).to be false
50
+ end
51
+
52
+ it 'should: be valid with depth 1' do
53
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
54
+ other = {:a => {:c => 'c'}, :b => {:d => 'd'}}
55
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
56
+ expect(result).to be true
57
+ end
58
+
59
+ it 'should: be invalid with depth 1' do
60
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
61
+ other = {:a => {:c => 'c'}, :b => {:e => 'e'}}
62
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
63
+ expect(result).to be false
64
+ end
65
+
66
+ it 'should: be invalid with depth 1 and nil' do
67
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
68
+ other = {:a => {:c => 'c'}, :b => nil}
69
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
70
+ expect(result).to be false
71
+ end
72
+
73
+ it 'should: be invalid with depth 1 and integer' do
74
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
75
+ other = {:a => {:c => 'c'}, :b => 1}
76
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
77
+ expect(result).to be false
78
+ end
79
+
80
+ it 'should: be invalid with depth 1 and string' do
81
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
82
+ other = {:a => {:c => 'c'}, :b => 'b'}
83
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
84
+ expect(result).to be false
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,136 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'Requesting Actions routes', :type => :request do
4
+ before(:each) do
5
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
6
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
7
+ Island.create(name: 'Corsica')
8
+ end
9
+
10
+ after(:each) do
11
+ Island.destroy_all
12
+ end
13
+
14
+ describe 'call /values' do
15
+ it 'should respond 200' do
16
+ post '/forest/actions/foo/values', {}
17
+ expect(response.status).to eq(200)
18
+ expect(response.body).to be {}
19
+ end
20
+ end
21
+
22
+ describe 'hooks' do
23
+ foo = {
24
+ field: 'foo',
25
+ type: 'String',
26
+ default_value: nil,
27
+ enums: nil,
28
+ is_required: false,
29
+ reference: nil,
30
+ description: nil,
31
+ widget: nil,
32
+ }
33
+ action_definition = {
34
+ name: 'my_action',
35
+ fields: [foo],
36
+ hooks: {
37
+ :load => -> (context) {
38
+ context[:fields]
39
+ },
40
+ :change => {
41
+ 'foo' => -> (context) {
42
+ fields = context[:fields]
43
+ fields['foo'][:value] = 'baz'
44
+ return fields
45
+ }
46
+ }
47
+ }
48
+ }
49
+ fail_action_definition = {
50
+ name: 'fail_action',
51
+ fields: [foo],
52
+ hooks: {
53
+ :load => -> (context) {
54
+ 1
55
+ },
56
+ :change => {
57
+ 'foo' => -> (context) {
58
+ 1
59
+ }
60
+ }
61
+ }
62
+ }
63
+ cheat_action_definition = {
64
+ name: 'cheat_action',
65
+ fields: [foo],
66
+ hooks: {
67
+ :load => -> (context) {
68
+ context[:fields]['baz'] = foo.clone.update({field: 'baz'})
69
+ context[:fields]
70
+ },
71
+ :change => {
72
+ 'foo' => -> (context) {
73
+ context[:fields]['baz'] = foo.clone.update({field: 'baz'})
74
+ context[:fields]
75
+ }
76
+ }
77
+ }
78
+ }
79
+ action = ForestLiana::Model::Action.new(action_definition)
80
+ fail_action = ForestLiana::Model::Action.new(fail_action_definition)
81
+ cheat_action = ForestLiana::Model::Action.new(cheat_action_definition)
82
+ island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
83
+ island.actions = [action, fail_action, cheat_action]
84
+
85
+ describe 'call /load' do
86
+ params = {recordIds: [1], collectionName: 'Island'}
87
+
88
+ it 'should respond 200' do
89
+ post '/forest/actions/my_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
90
+ expect(response.status).to eq(200)
91
+ expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).stringify_keys]})
92
+ end
93
+
94
+ it 'should respond 500 with bad params' do
95
+ post '/forest/actions/my_action/hooks/load', {}
96
+ expect(response.status).to eq(500)
97
+ end
98
+
99
+ it 'should respond 500 with bad hook result type' do
100
+ post '/forest/actions/fail_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
101
+ expect(response.status).to eq(500)
102
+ end
103
+
104
+ it 'should respond 500 with bad hook result data structure' do
105
+ post '/forest/actions/cheat_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
106
+ expect(response.status).to eq(500)
107
+ end
108
+ end
109
+
110
+ describe 'call /change' do
111
+ updated_foo = foo.clone.merge({:previousValue => nil, :value => 'bar'})
112
+ params = {recordIds: [1], fields: [updated_foo], collectionName: 'Island'}
113
+
114
+ it 'should respond 200' do
115
+ post '/forest/actions/my_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
116
+ expect(response.status).to eq(200)
117
+ expect(JSON.parse(response.body)).to eq({'fields' => [updated_foo.merge({:value => 'baz'}).stringify_keys]})
118
+ end
119
+
120
+ it 'should respond 500 with bad params' do
121
+ post '/forest/actions/my_action/hooks/change', {}
122
+ expect(response.status).to eq(500)
123
+ end
124
+
125
+ it 'should respond 500 with bad hook result type' do
126
+ post '/forest/actions/fail_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
127
+ expect(response.status).to eq(500)
128
+ end
129
+
130
+ it 'should respond 500 with bad hook result data structure' do
131
+ post '/forest/actions/cheat_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
132
+ expect(response.status).to eq(500)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -75,7 +75,8 @@ module ForestLiana
75
75
  type: 'File',
76
76
  field: 'File'
77
77
  }],
78
- http_method: nil
78
+ http_method: nil,
79
+ hooks: nil,
79
80
  }
80
81
  }, {
81
82
  attributes: {
@@ -95,7 +96,8 @@ module ForestLiana
95
96
  download: nil,
96
97
  endpoint: nil,
97
98
  redirect: nil,
98
- 'http_method': nil
99
+ 'http_method': nil,
100
+ hooks: nil,
99
101
  }
100
102
  }]
101
103
  }
@@ -148,8 +150,8 @@ module ForestLiana
148
150
  end
149
151
 
150
152
  it 'should sort the included actions and segments objects attributes values' do
151
- expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download'])
152
- expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields'])
153
+ expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download', 'hooks'])
154
+ expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields', 'hooks'])
153
155
  expect(apimap_sorted['included'][2]['attributes'].keys).to eq(['name'])
154
156
  expect(apimap_sorted['included'][3]['attributes'].keys).to eq(['name'])
155
157
  end
@@ -9,7 +9,7 @@ module ForestLiana
9
9
 
10
10
  expect(collection.fields.map { |field| field[:field] }).to eq(
11
11
  ["id", "name", "created_at", "updated_at", "trees"]
12
- );
12
+ )
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,185 @@
1
+ module ForestLiana
2
+ class ScopeValidatorTest < ActiveSupport::TestCase
3
+ test 'Request with aggregated condition filters should be allowed if it matches the scope exactly' do
4
+ scope_validator = ForestLiana::ScopeValidator.new({
5
+ 'aggregator' => 'and',
6
+ 'conditions' => [
7
+ { 'field' => 'name', 'value' => 'john', 'operator' => 'equal' },
8
+ { 'field' => 'price', 'value' => '2500', 'operator' => 'equal' }
9
+ ]
10
+ }, [])
11
+
12
+ allowed = scope_validator.is_scope_in_request?({
13
+ user_id: '1',
14
+ filters: JSON.generate({
15
+ aggregator: 'and',
16
+ conditions: [
17
+ { field: 'name', value: 'john', operator: 'equal' },
18
+ { field: 'price', value: '2500', operator: 'equal' }
19
+ ]
20
+ })
21
+ })
22
+ assert allowed == true
23
+ end
24
+
25
+ test 'Request with simple condition filter should be allowed if it matches the scope exactly' do
26
+ scope_validator = ForestLiana::ScopeValidator.new({
27
+ 'aggregator' => 'and',
28
+ 'conditions' => [
29
+ { 'field' => 'field', 'value' => 'value', 'operator' => 'equal' }
30
+ ]
31
+ }, [])
32
+ allowed = scope_validator.is_scope_in_request?({
33
+ user_id: '1',
34
+ filters: JSON.generate({
35
+ field: 'field', value: 'value', operator: 'equal'
36
+ })
37
+ })
38
+ assert allowed == true
39
+ end
40
+
41
+ test 'Request with multiples condition filters should be allowed if it contains the scope ' do
42
+ scope_validator = ForestLiana::ScopeValidator.new({
43
+ 'aggregator' => 'and',
44
+ 'conditions' => [
45
+ { 'field' => 'name', 'value' => 'doe', 'operator' => 'equal' }
46
+ ]
47
+ }, []
48
+ )
49
+
50
+ allowed = scope_validator.is_scope_in_request?({
51
+ user_id: '1',
52
+ filters: JSON.generate({
53
+ aggregator: 'and',
54
+ conditions: [
55
+ { field: 'name', value: 'doe', operator: 'equal' },
56
+ { field: 'field2', value: 'value2', operator: 'equal' }
57
+ ]
58
+ })
59
+ })
60
+ assert allowed == true
61
+ end
62
+
63
+ test 'Request with dynamic user values should be allowed if it matches the scope exactly' do
64
+ scope_validator = ForestLiana::ScopeValidator.new({
65
+ 'aggregator' => 'and',
66
+ 'conditions' => [
67
+ { 'field' => 'name', 'value' => '$currentUser.lastname', 'operator' => 'equal' }
68
+ ],
69
+ }, {
70
+ '1' => { '$currentUser.lastname' => 'john' }
71
+ })
72
+
73
+ allowed = scope_validator.is_scope_in_request?({
74
+ user_id: '1',
75
+ filters: JSON.generate({
76
+ 'field' => 'name', 'value' => 'john', 'operator' => 'equal'
77
+ })
78
+ })
79
+ assert allowed == true
80
+ end
81
+
82
+ test 'Request with multiples aggregation and dynamic values should be allowed if it contains the scope' do
83
+ scope_validator = ForestLiana::ScopeValidator.new({
84
+ 'aggregator' => 'or',
85
+ 'conditions' => [
86
+ { 'field' => 'price', 'value' => '2500', 'operator' => 'equal' },
87
+ { 'field' => 'name', 'value' => '$currentUser.lastname', 'operator' => 'equal' }
88
+ ]
89
+ }, {
90
+ '1' => { '$currentUser.lastname' => 'john' }
91
+ })
92
+
93
+ allowed = scope_validator.is_scope_in_request?({
94
+ user_id: '1',
95
+ filters: JSON.generate({
96
+ aggregator: 'and',
97
+ conditions: [
98
+ { field: 'field', value: 'value', operator: 'equal' },
99
+ {
100
+ aggregator: 'or',
101
+ conditions: [
102
+ { field: 'price', value: '2500', operator: 'equal' },
103
+ { field: 'name', value: 'john', operator: 'equal' }
104
+ ]
105
+ }
106
+ ]
107
+ })
108
+ })
109
+ assert allowed == true
110
+ end
111
+
112
+ test 'Request that does not match the expect scope should not be allowed' do
113
+ scope_validator = ForestLiana::ScopeValidator.new({
114
+ 'aggregator' => 'and',
115
+ 'conditions' => [
116
+ { 'field' => 'name', 'value' => 'john', 'operator' => 'equal' },
117
+ { 'field' => 'price', 'value' => '2500', 'operator' => 'equal' }
118
+ ]
119
+ }, [])
120
+
121
+ allowed = scope_validator.is_scope_in_request?({
122
+ user_id: '1',
123
+ filters: JSON.generate({
124
+ aggregator: 'and',
125
+ conditions: [
126
+ { field: 'name', value: 'definitely_not_john', operator: 'equal' },
127
+ { field: 'price', value: '0', operator: 'equal' }
128
+ ]
129
+ })
130
+ })
131
+ assert allowed == false
132
+ end
133
+
134
+ test 'Request that are missing part of the scope should not be allowed' do
135
+ scope_validator = ForestLiana::ScopeValidator.new({
136
+ 'aggregator' => 'and',
137
+ 'conditions' => [
138
+ { 'field' => 'name', 'value' => 'john', 'operator' => 'equal' },
139
+ { 'field' => 'price', 'value' => '2500', 'operator' => 'equal' }
140
+ ]
141
+ }, [])
142
+
143
+ allowed = scope_validator.is_scope_in_request?({
144
+ user_id: '1',
145
+ filters: JSON.generate({
146
+ aggregator: 'and',
147
+ conditions: [
148
+ { field: 'name', value: 'john', operator: 'equal' },
149
+ ]
150
+ })
151
+ })
152
+ assert allowed == false
153
+ end
154
+
155
+ test 'Request that does not have a top aggregator being "and" should not be allowed' do
156
+ scope_validator = ForestLiana::ScopeValidator.new({
157
+ 'aggregator' => 'and',
158
+ 'conditions' => [
159
+ { 'field' => 'price', 'value' => '2500', 'operator' => 'equal' },
160
+ { 'field' => 'name', 'value' => '$currentUser.lastname', 'operator' => 'equal' }
161
+ ]
162
+ }, {
163
+ '1' => { '$currentUser.lastname' => 'john' }
164
+ })
165
+
166
+ allowed = scope_validator.is_scope_in_request?({
167
+ user_id: '1',
168
+ filters: JSON.generate({
169
+ aggregator: 'or',
170
+ conditions: [
171
+ { field: 'field', value: 'value', operator: 'equal' },
172
+ {
173
+ aggregator: 'and',
174
+ conditions: [
175
+ { field: 'price', value: '2500', operator: 'equal' },
176
+ { field: 'name', value: 'john', operator: 'equal' }
177
+ ]
178
+ }
179
+ ]
180
+ })
181
+ })
182
+ assert allowed == false
183
+ end
184
+ end
185
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.3
4
+ version: 5.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-19 00:00:00.000000000 Z
11
+ date: 2020-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -211,6 +211,7 @@ files:
211
211
  - app/helpers/forest_liana/adapter_helper.rb
212
212
  - app/helpers/forest_liana/application_helper.rb
213
213
  - app/helpers/forest_liana/decoration_helper.rb
214
+ - app/helpers/forest_liana/is_same_data_structure_helper.rb
214
215
  - app/helpers/forest_liana/query_helper.rb
215
216
  - app/helpers/forest_liana/schema_helper.rb
216
217
  - app/models/forest_liana/model/action.rb
@@ -262,6 +263,7 @@ files:
262
263
  - app/services/forest_liana/resources_getter.rb
263
264
  - app/services/forest_liana/schema_adapter.rb
264
265
  - app/services/forest_liana/schema_utils.rb
266
+ - app/services/forest_liana/scope_validator.rb
265
267
  - app/services/forest_liana/search_query_builder.rb
266
268
  - app/services/forest_liana/stat_getter.rb
267
269
  - app/services/forest_liana/stripe_base_getter.rb
@@ -335,9 +337,11 @@ files:
335
337
  - spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb
336
338
  - spec/dummy/db/migrate/20190716135241_add_type_to_user.rb
337
339
  - spec/dummy/db/schema.rb
340
+ - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
338
341
  - spec/helpers/forest_liana/query_helper_spec.rb
339
342
  - spec/helpers/forest_liana/schema_helper_spec.rb
340
343
  - spec/rails_helper.rb
344
+ - spec/requests/actions_controller_spec.rb
341
345
  - spec/requests/resources_spec.rb
342
346
  - spec/services/forest_liana/apimap_sorter_spec.rb
343
347
  - spec/services/forest_liana/filters_parser_spec.rb
@@ -432,6 +436,7 @@ files:
432
436
  - test/services/forest_liana/resource_updater_test.rb
433
437
  - test/services/forest_liana/resources_getter_test.rb
434
438
  - test/services/forest_liana/schema_adapter_test.rb
439
+ - test/services/forest_liana/scope_validator_test.rb
435
440
  - test/services/forest_liana/value_stat_getter_test.rb
436
441
  - test/test_helper.rb
437
442
  homepage: https://github.com/ForestAdmin/forest-rails
@@ -453,8 +458,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
453
458
  - !ruby/object:Gem::Version
454
459
  version: '0'
455
460
  requirements: []
456
- rubyforge_project:
457
- rubygems_version: 2.7.7
461
+ rubygems_version: 3.0.8
458
462
  signing_key:
459
463
  specification_version: 4
460
464
  summary: Official Rails Liana for Forest
@@ -469,6 +473,7 @@ test_files:
469
473
  - test/fixtures/has_many_field.yml
470
474
  - test/fixtures/string_field.yml
471
475
  - test/services/forest_liana/resources_getter_test.rb
476
+ - test/services/forest_liana/scope_validator_test.rb
472
477
  - test/services/forest_liana/schema_adapter_test.rb
473
478
  - test/services/forest_liana/has_many_getter_test.rb
474
479
  - test/services/forest_liana/value_stat_getter_test.rb
@@ -554,6 +559,7 @@ test_files:
554
559
  - spec/services/forest_liana/apimap_sorter_spec.rb
555
560
  - spec/services/forest_liana/filters_parser_spec.rb
556
561
  - spec/spec_helper.rb
562
+ - spec/requests/actions_controller_spec.rb
557
563
  - spec/requests/resources_spec.rb
558
564
  - spec/dummy/README.rdoc
559
565
  - spec/dummy/app/views/layouts/application.html.erb
@@ -596,5 +602,6 @@ test_files:
596
602
  - spec/dummy/config/initializers/backtrace_silencers.rb
597
603
  - spec/dummy/config/database.yml
598
604
  - spec/helpers/forest_liana/schema_helper_spec.rb
605
+ - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
599
606
  - spec/helpers/forest_liana/query_helper_spec.rb
600
607
  - spec/rails_helper.rb