forest_liana 5.2.3 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +95 -0
  3. data/app/controllers/forest_liana/resources_controller.rb +14 -17
  4. data/app/controllers/forest_liana/smart_actions_controller.rb +10 -5
  5. data/app/helpers/forest_liana/is_same_data_structure_helper.rb +44 -0
  6. data/app/helpers/forest_liana/widgets_helper.rb +59 -0
  7. data/app/models/forest_liana/model/action.rb +2 -1
  8. data/app/services/forest_liana/apimap_sorter.rb +1 -0
  9. data/app/services/forest_liana/permissions_checker.rb +118 -56
  10. data/app/services/forest_liana/permissions_formatter.rb +52 -0
  11. data/app/services/forest_liana/permissions_getter.rb +52 -17
  12. data/app/services/forest_liana/resources_getter.rb +3 -3
  13. data/app/services/forest_liana/scope_validator.rb +8 -7
  14. data/app/services/forest_liana/utils/beta_schema_utils.rb +13 -0
  15. data/config/routes.rb +2 -0
  16. data/lib/forest_liana/bootstrapper.rb +19 -0
  17. data/lib/forest_liana/schema_file_updater.rb +1 -0
  18. data/lib/forest_liana/version.rb +1 -1
  19. data/spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb +87 -0
  20. data/spec/requests/actions_controller_spec.rb +174 -0
  21. data/spec/services/forest_liana/apimap_sorter_spec.rb +6 -4
  22. data/spec/services/forest_liana/permissions_checker_acl_disabled_spec.rb +711 -0
  23. data/spec/services/forest_liana/permissions_checker_acl_enabled_spec.rb +831 -0
  24. data/spec/services/forest_liana/permissions_formatter_spec.rb +222 -0
  25. data/spec/services/forest_liana/permissions_getter_spec.rb +82 -0
  26. data/spec/services/forest_liana/schema_adapter_spec.rb +1 -1
  27. data/spec/spec_helper.rb +3 -0
  28. metadata +18 -2
@@ -0,0 +1,52 @@
1
+ module ForestLiana
2
+ class PermissionsFormatter
3
+ class << PermissionsFormatter
4
+ # Convert old format permissions to unify PermissionsGetter code
5
+ def convert_to_new_format(permissions, rendering_id)
6
+ permissions_new_format = Hash.new
7
+ permissions_new_format['collections'] = Hash.new
8
+ permissions_new_format['renderings'] = Hash.new
9
+ permissions_new_format['renderings'][rendering_id] = Hash.new
10
+ permissions.keys.each { |collection_name|
11
+ permissions_new_format['collections'][collection_name] = {
12
+ 'collection' => convert_collection_permissions_to_new_format(permissions[collection_name]['collection']),
13
+ 'actions' => convert_actions_permissions_to_new_format(permissions[collection_name]['actions'])
14
+ }
15
+
16
+ permissions_new_format['renderings'][rendering_id][collection_name] = { 'scope' => permissions[collection_name]['scope'] }
17
+ }
18
+
19
+ permissions_new_format
20
+ end
21
+
22
+ def convert_collection_permissions_to_new_format(collection_permissions)
23
+ {
24
+ 'browseEnabled' => collection_permissions['list'] || collection_permissions['searchToEdit'],
25
+ 'readEnabled' => collection_permissions['show'],
26
+ 'addEnabled' => collection_permissions['create'],
27
+ 'editEnabled' => collection_permissions['update'],
28
+ 'deleteEnabled' => collection_permissions['delete'],
29
+ 'exportEnabled' => collection_permissions['export']
30
+ }
31
+ end
32
+
33
+ def convert_actions_permissions_to_new_format(actions_permissions)
34
+ return nil unless actions_permissions
35
+
36
+ actions_permissions_new_format = Hash.new
37
+
38
+ actions_permissions.keys.each { |action_name|
39
+ allowed = actions_permissions[action_name]['allowed']
40
+ users = actions_permissions[action_name]['users']
41
+
42
+ actions_permissions_new_format[action_name] = Hash.new
43
+ actions_permissions_new_format[action_name] = {
44
+ 'triggerEnabled' => allowed && (users.nil? || users)
45
+ }
46
+ }
47
+
48
+ actions_permissions_new_format
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,25 +1,60 @@
1
1
  module ForestLiana
2
2
  class PermissionsGetter
3
- def initialize(rendering_id)
4
- @route = "/liana/v2/permissions"
5
- @rendering_id = rendering_id
6
- end
3
+ class << PermissionsGetter
4
+ def get_permissions_api_route
5
+ '/liana/v3/permissions'
6
+ end
7
+
8
+ # Permission format example:
9
+ # collections => {
10
+ # {model_name} => {
11
+ # collection => {
12
+ # browseEnabled => true,
13
+ # readEnabled => true,
14
+ # editEnabled => true,
15
+ # addEnabled => true,
16
+ # deleteEnabled => true,
17
+ # exportEnabled => true,
18
+ # },
19
+ # actions => {
20
+ # {action_name} => {
21
+ # triggerEnabled => true,
22
+ # },
23
+ # },
24
+ # },
25
+ # },
26
+ # rederings => {
27
+ # {rendering_id} => {
28
+ # {collection_id} => {
29
+ # scope => {
30
+ # dynamicScopesValues => {},
31
+ # filter => {}
32
+ # }
33
+ # }
34
+ # }
35
+ # }
36
+ # }
37
+ # With `rendering_specific_only` this returns only the permissions related data specific to the provided rendering
38
+ # For now this only includes scopes
39
+ def get_permissions_for_rendering(rendering_id, rendering_specific_only: false)
40
+ begin
41
+ query_parameters = { 'renderingId' => rendering_id }
42
+ query_parameters['renderingSpecificOnly'] = rendering_specific_only if rendering_specific_only
7
43
 
8
- def perform
9
- begin
10
- query_parameters = { 'renderingId' => @rendering_id }
11
- response = ForestLiana::ForestApiRequester.get(@route, query: query_parameters)
44
+ api_route = get_permissions_api_route
45
+ response = ForestLiana::ForestApiRequester.get(api_route, query: query_parameters)
12
46
 
13
- if response.is_a?(Net::HTTPOK)
14
- JSON.parse(response.body)
15
- else
16
- raise "Forest API returned an #{ForestLiana::Errors::HTTPErrorHelper.format(response)}"
47
+ if response.is_a?(Net::HTTPOK)
48
+ JSON.parse(response.body)
49
+ else
50
+ raise "Forest API returned an #{ForestLiana::Errors::HTTPErrorHelper.format(response)}"
51
+ end
52
+ rescue => exception
53
+ FOREST_LOGGER.error 'Cannot retrieve the permissions from the Forest server.'
54
+ FOREST_LOGGER.error 'Which was caused by:'
55
+ ForestLiana::Errors::ExceptionHelper.recursively_print(exception, margin: ' ', is_error: true)
56
+ nil
17
57
  end
18
- rescue => exception
19
- FOREST_LOGGER.error 'Cannot retrieve the permissions from the Forest server.'
20
- FOREST_LOGGER.error 'Which was caused by:'
21
- ForestLiana::Errors::ExceptionHelper.recursively_print(exception, margin: ' ', is_error: true)
22
- nil
23
58
  end
24
59
  end
25
60
  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)
@@ -32,9 +32,10 @@ module ForestLiana
32
32
  def compute_condition_filters_from_scope(user_id)
33
33
  computed_condition_filters = @scope_filters.clone
34
34
  computed_condition_filters['conditions'].each do |condition|
35
- if condition.include?('value') &&
36
- !condition['value'].nil? &&
37
- condition['value'].start_with?('$') &&
35
+ if condition.include?('value') &&
36
+ !condition['value'].nil? &&
37
+ condition['value'].instance_of?(String) &&
38
+ condition['value'].start_with?('$') &&
38
39
  @users_variable_values.include?(user_id)
39
40
  condition['value'] = @users_variable_values[user_id][condition['value']]
40
41
  end
@@ -51,9 +52,9 @@ module ForestLiana
51
52
  ensure_valid_aggregation(node)
52
53
 
53
54
  return is_scope_condition?(node) unless node['aggregator']
54
-
55
+
55
56
  # NOTICE: Remove conditions that are not from the scope
56
- filtered_conditions = node['conditions'].map { |condition|
57
+ filtered_conditions = node['conditions'].map { |condition|
57
58
  search_scope_aggregation(condition)
58
59
  }.select { |condition|
59
60
  condition
@@ -61,7 +62,7 @@ module ForestLiana
61
62
 
62
63
  # NOTICE: If there is only one condition filter left and its current aggregator is
63
64
  # an "and", this condition filter is the searched scope
64
- if (filtered_conditions.length == 1 &&
65
+ if (filtered_conditions.length == 1 &&
65
66
  filtered_conditions.first.is_a?(Hash) &&
66
67
  filtered_conditions.first.include?(:aggregator) &&
67
68
  node['aggregator'] == 'and')
@@ -70,7 +71,7 @@ module ForestLiana
70
71
 
71
72
  # NOTICE: Otherwise, validate if the current node is the scope and return nil
72
73
  # if it's not
73
- return (filtered_conditions.length == @scope_filters['conditions'].length &&
74
+ return (filtered_conditions.length == @scope_filters['conditions'].length &&
74
75
  node['aggregator'] == @scope_filters['aggregator']) ?
75
76
  { aggregator: node['aggregator'], conditions: filtered_conditions } :
76
77
  nil
@@ -0,0 +1,13 @@
1
+ module ForestLiana
2
+ module Utils
3
+ class BetaSchemaUtils
4
+ def self.find_action_from_endpoint(collection_name, endpoint, http_method)
5
+ collection = ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
6
+
7
+ return nil unless collection
8
+
9
+ collection.actions.find { |action| action.endpoint == endpoint && action.http_method == http_method }
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -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.2.3"
2
+ VERSION = "5.4.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,174 @@
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
+ enum = {
34
+ field: 'enum',
35
+ type: 'Enum',
36
+ enums: %w[a b c],
37
+ }
38
+
39
+ action_definition = {
40
+ name: 'my_action',
41
+ fields: [foo],
42
+ hooks: {
43
+ :load => -> (context) {
44
+ context[:fields]
45
+ },
46
+ :change => {
47
+ 'foo' => -> (context) {
48
+ fields = context[:fields]
49
+ fields['foo'][:value] = 'baz'
50
+ return fields
51
+ }
52
+ }
53
+ }
54
+ }
55
+ fail_action_definition = {
56
+ name: 'fail_action',
57
+ fields: [foo],
58
+ hooks: {
59
+ :load => -> (context) {
60
+ 1
61
+ },
62
+ :change => {
63
+ 'foo' => -> (context) {
64
+ 1
65
+ }
66
+ }
67
+ }
68
+ }
69
+ cheat_action_definition = {
70
+ name: 'cheat_action',
71
+ fields: [foo],
72
+ hooks: {
73
+ :load => -> (context) {
74
+ context[:fields]['baz'] = foo.clone.update({field: 'baz'})
75
+ context[:fields]
76
+ },
77
+ :change => {
78
+ 'foo' => -> (context) {
79
+ context[:fields]['baz'] = foo.clone.update({field: 'baz'})
80
+ context[:fields]
81
+ }
82
+ }
83
+ }
84
+ }
85
+ enums_action_definition = {
86
+ name: 'enums_action',
87
+ fields: [foo, enum],
88
+ hooks: {
89
+ :change => {
90
+ 'foo' => -> (context) {
91
+ fields = context[:fields]
92
+ fields['enum'][:enums] = %w[c d e]
93
+ return fields
94
+ }
95
+ }
96
+ }
97
+ }
98
+ action = ForestLiana::Model::Action.new(action_definition)
99
+ fail_action = ForestLiana::Model::Action.new(fail_action_definition)
100
+ cheat_action = ForestLiana::Model::Action.new(cheat_action_definition)
101
+ enums_action = ForestLiana::Model::Action.new(enums_action_definition)
102
+ island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
103
+ island.actions = [action, fail_action, cheat_action, enums_action]
104
+
105
+ describe 'call /load' do
106
+ params = {recordIds: [1], collectionName: 'Island'}
107
+
108
+ it 'should respond 200' do
109
+ post '/forest/actions/my_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
110
+ expect(response.status).to eq(200)
111
+ expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).stringify_keys]})
112
+ end
113
+
114
+ it 'should respond 500 with bad params' do
115
+ post '/forest/actions/my_action/hooks/load', {}
116
+ expect(response.status).to eq(500)
117
+ end
118
+
119
+ it 'should respond 500 with bad hook result type' do
120
+ post '/forest/actions/fail_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
121
+ expect(response.status).to eq(500)
122
+ end
123
+
124
+ it 'should respond 500 with bad hook result data structure' do
125
+ post '/forest/actions/cheat_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
126
+ expect(response.status).to eq(500)
127
+ end
128
+ end
129
+
130
+ describe 'call /change' do
131
+ updated_foo = foo.clone.merge({:previousValue => nil, :value => 'bar'})
132
+ params = {recordIds: [1], fields: [updated_foo], collectionName: 'Island', changedField: 'foo'}
133
+
134
+ it 'should respond 200' do
135
+ post '/forest/actions/my_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
136
+ expect(response.status).to eq(200)
137
+ expected = updated_foo.clone.merge({:value => 'baz'})
138
+ expected[:widgetEdit] = nil
139
+ expected.delete(:widget)
140
+ expect(JSON.parse(response.body)).to eq({'fields' => [expected.stringify_keys]})
141
+ end
142
+
143
+ it 'should respond 500 with bad params' do
144
+ post '/forest/actions/my_action/hooks/change', {}
145
+ expect(response.status).to eq(500)
146
+ end
147
+
148
+ it 'should respond 500 with bad hook result type' do
149
+ post '/forest/actions/fail_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
150
+ expect(response.status).to eq(500)
151
+ end
152
+
153
+ it 'should respond 500 with bad hook result data structure' do
154
+ post '/forest/actions/cheat_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
155
+ expect(response.status).to eq(500)
156
+ end
157
+
158
+ it 'should reset value when enums has changed' do
159
+ updated_enum = enum.clone.merge({:previousValue => nil, :value => 'a'}) # set value to a
160
+ p = {recordIds: [1], fields: [updated_foo, updated_enum], collectionName: 'Island', changedField: 'foo'}
161
+ post '/forest/actions/enums_action/hooks/change', JSON.dump(p), 'CONTENT_TYPE' => 'application/json'
162
+ expect(response.status).to eq(200)
163
+
164
+ expected_enum = updated_enum.clone.merge({ :enums => %w[c d e], :value => nil, :widgetEdit => nil})
165
+ expected_enum.delete(:widget)
166
+ expected_foo = updated_foo.clone.merge({ :widgetEdit => nil})
167
+ expected_foo.delete(:widget)
168
+
169
+ expect(JSON.parse(response.body)).to eq({'fields' => [expected_foo.stringify_keys, expected_enum.stringify_keys]})
170
+ end
171
+
172
+ end
173
+ end
174
+ end