forest_liana 5.2.3 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
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