forest_liana 7.0.0.beta.4 → 7.0.1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +2 -2
  3. data/app/controllers/forest_liana/associations_controller.rb +2 -2
  4. data/app/controllers/forest_liana/resources_controller.rb +15 -6
  5. data/app/controllers/forest_liana/scopes_controller.rb +20 -0
  6. data/app/controllers/forest_liana/smart_actions_controller.rb +39 -3
  7. data/app/controllers/forest_liana/stats_controller.rb +5 -5
  8. data/app/services/forest_liana/filters_parser.rb +25 -9
  9. data/app/services/forest_liana/has_many_dissociator.rb +2 -2
  10. data/app/services/forest_liana/has_many_getter.rb +2 -2
  11. data/app/services/forest_liana/leaderboard_stat_getter.rb +20 -14
  12. data/app/services/forest_liana/line_stat_getter.rb +4 -2
  13. data/app/services/forest_liana/permissions_checker.rb +3 -4
  14. data/app/services/forest_liana/permissions_getter.rb +2 -2
  15. data/app/services/forest_liana/pie_stat_getter.rb +6 -3
  16. data/app/services/forest_liana/resource_getter.rb +6 -3
  17. data/app/services/forest_liana/resource_updater.rb +5 -2
  18. data/app/services/forest_liana/resources_getter.rb +6 -5
  19. data/app/services/forest_liana/scope_manager.rb +102 -0
  20. data/app/services/forest_liana/search_query_builder.rb +6 -3
  21. data/app/services/forest_liana/stat_getter.rb +2 -1
  22. data/app/services/forest_liana/value_stat_getter.rb +4 -2
  23. data/config/routes.rb +3 -0
  24. data/lib/forest_liana/version.rb +1 -1
  25. data/spec/dummy/app/controllers/forest/islands_controller.rb +5 -0
  26. data/spec/dummy/config/routes.rb +4 -0
  27. data/spec/dummy/lib/forest_liana/collections/island.rb +7 -0
  28. data/spec/requests/actions_controller_spec.rb +100 -12
  29. data/spec/requests/resources_spec.rb +2 -0
  30. data/spec/services/forest_liana/filters_parser_spec.rb +27 -1
  31. data/spec/services/forest_liana/has_many_getter_spec.rb +116 -0
  32. data/spec/services/forest_liana/line_stat_getter_spec.rb +14 -6
  33. data/spec/services/forest_liana/permissions_checker_acl_disabled_spec.rb +1 -3
  34. data/spec/services/forest_liana/pie_stat_getter_spec.rb +114 -0
  35. data/spec/services/forest_liana/resource_updater_spec.rb +116 -0
  36. data/spec/services/forest_liana/resources_getter_spec.rb +68 -1
  37. data/spec/services/forest_liana/scope_manager_spec.rb +232 -0
  38. data/spec/services/forest_liana/value_stat_getter_spec.rb +96 -0
  39. metadata +20 -15
  40. data/app/services/forest_liana/scope_validator.rb +0 -98
  41. data/test/services/forest_liana/has_many_getter_test.rb +0 -75
  42. data/test/services/forest_liana/pie_stat_getter_test.rb +0 -29
  43. data/test/services/forest_liana/resource_updater_test.rb +0 -86
  44. data/test/services/forest_liana/scope_validator_test.rb +0 -185
  45. data/test/services/forest_liana/value_stat_getter_test.rb +0 -71
@@ -2,16 +2,19 @@ module ForestLiana
2
2
  class ResourceGetter < BaseGetter
3
3
  attr_accessor :record
4
4
 
5
- def initialize(resource, params)
5
+ def initialize(resource, params, forest_user)
6
6
  @resource = resource
7
7
  @params = params
8
- @collection_name = ForestLiana.name_for(@resource)
8
+ @collection_name = ForestLiana.name_for(resource)
9
+ @user = forest_user
9
10
  @collection = get_collection(@collection_name)
10
11
  compute_includes()
11
12
  end
12
13
 
13
14
  def perform
14
- @record = get_resource().eager_load(@includes).find(@params[:id])
15
+ records = get_resource().eager_load(@includes)
16
+ scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(records, @user, @collection_name, @params[:timezone])
17
+ @record = scoped_records.find(@params[:id])
15
18
  end
16
19
  end
17
20
  end
@@ -3,15 +3,18 @@ module ForestLiana
3
3
  attr_accessor :record
4
4
  attr_accessor :errors
5
5
 
6
- def initialize(resource, params)
6
+ def initialize(resource, params, forest_user)
7
7
  @resource = resource
8
8
  @params = params
9
9
  @errors = nil
10
+ @user = forest_user
10
11
  end
11
12
 
12
13
  def perform
13
14
  begin
14
- @record = @resource.find(@params[:id])
15
+ collection_name = ForestLiana.name_for(@resource)
16
+ scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(@resource, @user, collection_name, @params[:timezone])
17
+ @record = scoped_records.find(@params[:id])
15
18
 
16
19
  if has_strong_parameter
17
20
  @record.update(resource_params)
@@ -4,7 +4,7 @@ module ForestLiana
4
4
  attr_reader :includes
5
5
  attr_reader :records_count
6
6
 
7
- def initialize(resource, params)
7
+ def initialize(resource, params, forest_user)
8
8
  @resource = resource
9
9
  @params = params
10
10
  @count_needs_includes = false
@@ -14,12 +14,13 @@ module ForestLiana
14
14
  @field_names_requested = field_names_requested
15
15
  get_segment
16
16
  compute_includes
17
- @search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection)
17
+ @user = forest_user
18
+ @search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection, forest_user)
18
19
 
19
20
  prepare_query
20
21
  end
21
22
 
22
- def self.get_ids_from_request(params)
23
+ def self.get_ids_from_request(params, user)
23
24
  attributes = params.dig('data', 'attributes')
24
25
  has_body_attributes = attributes != nil
25
26
  is_select_all_records_query = has_body_attributes && attributes[:all_records] == true
@@ -45,11 +46,11 @@ module ForestLiana
45
46
  collection: parent_collection_name,
46
47
  id: attributes[:parent_collection_id],
47
48
  association_name: attributes[:parent_association_name],
48
- }))
49
+ }), user)
49
50
  else
50
51
  collection_name = attributes[:collection_name]
51
52
  model = ForestLiana::SchemaUtils.find_model_from_collection_name(collection_name)
52
- resources_getter = ForestLiana::ResourcesGetter.new(model, attributes)
53
+ resources_getter = ForestLiana::ResourcesGetter.new(model, attributes, user)
53
54
  end
54
55
 
55
56
  # NOTICE: build IDs list.
@@ -0,0 +1,102 @@
1
+ module ForestLiana
2
+ class ScopeManager
3
+ @@scopes_cache = Hash.new
4
+ # 5 minutes exipration cache
5
+ @@scope_cache_expiration_delta = 300
6
+
7
+ def self.apply_scopes_on_records(records, forest_user, collection_name, timezone)
8
+ scope_filters = get_scope_for_user(forest_user, collection_name, as_string: true)
9
+
10
+ return records if scope_filters.blank?
11
+
12
+ FiltersParser.new(scope_filters, records, timezone).apply_filters
13
+ end
14
+
15
+ def self.append_scope_for_user(existing_filter, user, collection_name)
16
+ scope_filter = get_scope_for_user(user, collection_name, as_string: true)
17
+ filters = [existing_filter, scope_filter].compact
18
+
19
+ case filters.length
20
+ when 0
21
+ nil
22
+ when 1
23
+ filters[0]
24
+ else
25
+ "{\"aggregator\":\"and\",\"conditions\":[#{existing_filter},#{scope_filter}]}"
26
+ end
27
+ end
28
+
29
+ def self.get_scope_for_user(user, collection_name, as_string: false)
30
+ raise 'Missing required rendering_id' unless user['rendering_id']
31
+ raise 'Missing required collection_name' unless collection_name
32
+
33
+ collection_scope = get_collection_scope(user['rendering_id'], collection_name)
34
+
35
+ return nil unless collection_scope
36
+
37
+ filters = format_dynamic_values(user['id'], collection_scope)
38
+
39
+ as_string && filters ? JSON.generate(filters) : filters
40
+ end
41
+
42
+ def self.get_collection_scope(rendering_id, collection_name)
43
+ if !@@scopes_cache[rendering_id]
44
+ # when scope cache is unset wait for the refresh
45
+ refresh_scopes_cache(rendering_id)
46
+ elsif has_cache_expired?(rendering_id)
47
+ # when cache expired refresh the scopes without waiting for it
48
+ Thread.new { refresh_scopes_cache(rendering_id) }
49
+ end
50
+
51
+ @@scopes_cache[rendering_id][:scopes][collection_name]
52
+ end
53
+
54
+ def self.has_cache_expired?(rendering_id)
55
+ rendering_scopes = @@scopes_cache[rendering_id]
56
+ return true unless rendering_scopes
57
+
58
+ second_since_last_fetch = Time.now - rendering_scopes[:fetched_at]
59
+ second_since_last_fetch >= @@scope_cache_expiration_delta
60
+ end
61
+
62
+ def self.refresh_scopes_cache(rendering_id)
63
+ scopes = fetch_scopes(rendering_id)
64
+ @@scopes_cache[rendering_id] = {
65
+ :fetched_at => Time.now,
66
+ :scopes => scopes
67
+ }
68
+ end
69
+
70
+ def self.fetch_scopes(rendering_id)
71
+ query_parameters = { 'renderingId' => rendering_id }
72
+ response = ForestLiana::ForestApiRequester.get('/liana/scopes', query: query_parameters)
73
+
74
+ if response.is_a?(Net::HTTPOK)
75
+ JSON.parse(response.body)
76
+ else
77
+ raise 'Unable to fetch scopes'
78
+ end
79
+ end
80
+
81
+ def self.format_dynamic_values(user_id, collection_scope)
82
+ filter = collection_scope.dig('scope', 'filter')
83
+ return nil unless filter
84
+
85
+ dynamic_scopes_values = collection_scope.dig('scope', 'dynamicScopesValues')
86
+
87
+ # Only goes one level deep as required for now
88
+ filter['conditions'].map do |condition|
89
+ value = condition['value']
90
+ if value.is_a?(String) && value.start_with?('$currentUser')
91
+ condition['value'] = dynamic_scopes_values.dig('users', user_id, value)
92
+ end
93
+ end
94
+
95
+ filter
96
+ end
97
+
98
+ def self.invalidate_scope_cache(rendering_id)
99
+ @@scopes_cache.delete(rendering_id)
100
+ end
101
+ end
102
+ end
@@ -4,12 +4,13 @@ module ForestLiana
4
4
 
5
5
  attr_reader :fields_searched
6
6
 
7
- def initialize(params, includes, collection)
7
+ def initialize(params, includes, collection, user)
8
8
  @params = params
9
9
  @includes = includes
10
10
  @collection = collection
11
11
  @fields_searched = []
12
12
  @search = @params[:search]
13
+ @user = user
13
14
  end
14
15
 
15
16
  def perform(resource)
@@ -18,8 +19,10 @@ module ForestLiana
18
19
  ForestLiana::QueryHelper.get_tables_associated_to_relations_name(@resource)
19
20
  @records = search_param
20
21
 
21
- unless @params[:filters].blank?
22
- @records = FiltersParser.new(@params[:filters], @records, @params[:timezone]).apply_filters
22
+ filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filters], @user, @collection.name)
23
+
24
+ unless filters.blank?
25
+ @records = FiltersParser.new(filters, @records, @params[:timezone]).apply_filters
23
26
  end
24
27
 
25
28
  if @search
@@ -2,9 +2,10 @@ module ForestLiana
2
2
  class StatGetter < BaseGetter
3
3
  attr_accessor :record
4
4
 
5
- def initialize(resource, params)
5
+ def initialize(resource, params, forest_user)
6
6
  @resource = resource
7
7
  @params = params
8
+ @user = forest_user
8
9
  compute_includes()
9
10
  end
10
11
  end
@@ -6,8 +6,10 @@ module ForestLiana
6
6
  return if @params[:aggregate].blank?
7
7
  resource = get_resource().eager_load(@includes)
8
8
 
9
- unless @params[:filters].blank?
10
- filter_parser = FiltersParser.new(@params[:filters], resource, @params[:timezone])
9
+ filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filters], @user, @resource.name)
10
+
11
+ unless filters.blank?
12
+ filter_parser = FiltersParser.new(filters, resource, @params[:timezone])
11
13
  resource = filter_parser.apply_filters
12
14
  raw_previous_interval = filter_parser.get_previous_interval_condition
13
15
 
data/config/routes.rb CHANGED
@@ -20,6 +20,9 @@ ForestLiana::Engine.routes.draw do
20
20
  post '/stats/:collection' => 'stats#get'
21
21
  post '/stats' => 'stats#get_with_live_query'
22
22
 
23
+ # Scopes
24
+ post '/scope-cache-invalidation' => 'scopes#invalidate_scope_cache'
25
+
23
26
  # Stripe Integration
24
27
  get '(*collection)_stripe_payments' => 'stripe#payments'
25
28
  get ':collection/:id/stripe_payments' => 'stripe#payments'
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "7.0.0.beta.4"
2
+ VERSION = "7.0.1"
3
3
  end
@@ -0,0 +1,5 @@
1
+ class Forest::IslandsController < ForestLiana::SmartActionsController
2
+ def test
3
+ render json: { success: 'You are OK.' }
4
+ end
5
+ end
@@ -1,3 +1,7 @@
1
1
  Rails.application.routes.draw do
2
+ namespace :forest do
3
+ post '/actions/test' => 'islands#test'
4
+ end
5
+
2
6
  mount ForestLiana::Engine => "/forest"
3
7
  end
@@ -0,0 +1,7 @@
1
+ class Forest::Island
2
+ include ForestLiana::Collection
3
+
4
+ collection :Island
5
+
6
+ action 'Test'
7
+ end
@@ -1,16 +1,42 @@
1
1
  require 'rails_helper'
2
2
 
3
3
  describe 'Requesting Actions routes', :type => :request do
4
+ let(:rendering_id) { 13 }
5
+ let(:scope_filters) { nil }
6
+
4
7
  before(:each) do
5
8
  allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
6
9
  allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
7
- Island.create(name: 'Corsica')
10
+ Island.create(id: 1, name: 'Corsica')
11
+
12
+ ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id)
13
+ allow(ForestLiana::ScopeManager).to receive(:get_scope_for_user).and_return(scope_filters)
8
14
  end
9
15
 
10
16
  after(:each) do
11
17
  Island.destroy_all
12
18
  end
13
-
19
+
20
+ let(:token) {
21
+ JWT.encode({
22
+ id: 38,
23
+ email: 'michael.kelso@that70.show',
24
+ first_name: 'Michael',
25
+ last_name: 'Kelso',
26
+ team: 'Operations',
27
+ rendering_id: rendering_id,
28
+ exp: Time.now.to_i + 2.weeks.to_i
29
+ }, ForestLiana.auth_secret, 'HS256')
30
+ }
31
+
32
+ let(:headers) {
33
+ {
34
+ 'Accept' => 'application/json',
35
+ 'Content-Type' => 'application/json',
36
+ 'Authorization' => "Bearer #{token}"
37
+ }
38
+ }
39
+
14
40
  describe 'hooks' do
15
41
  foo = {
16
42
  field: 'foo',
@@ -126,25 +152,25 @@ describe 'Requesting Actions routes', :type => :request do
126
152
  }
127
153
 
128
154
  it 'should respond 200' do
129
- post '/forest/actions/my_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
155
+ post '/forest/actions/my_action/hooks/load', params: JSON.dump(params), headers: headers
130
156
  expect(response.status).to eq(200)
131
157
  expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).stringify_keys]})
132
158
  end
133
159
 
134
160
  it 'should respond 500 with bad params' do
135
- post '/forest/actions/my_action/hooks/load', params: {}
161
+ post '/forest/actions/my_action/hooks/load', params: {}, headers: headers
136
162
  expect(response.status).to eq(500)
137
163
  expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: cannot retrieve action from collection'})
138
164
  end
139
165
 
140
166
  it 'should respond 500 with bad hook result type' do
141
- post '/forest/actions/fail_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
167
+ post '/forest/actions/fail_action/hooks/load', params: JSON.dump(params), headers: headers
142
168
  expect(response.status).to eq(500)
143
169
  expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: hook must return an array of fields'})
144
170
  end
145
171
 
146
172
  it 'should respond 500 with bad hook result data structure' do
147
- post '/forest/actions/cheat_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
173
+ post '/forest/actions/cheat_action/hooks/load', params: JSON.dump(params), headers: headers
148
174
  expect(response.status).to eq(500)
149
175
  expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: hook must return an array of fields'})
150
176
  end
@@ -164,7 +190,7 @@ describe 'Requesting Actions routes', :type => :request do
164
190
  }
165
191
 
166
192
  it 'should respond 200' do
167
- post '/forest/actions/my_action/hooks/change', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
193
+ post '/forest/actions/my_action/hooks/change', params: JSON.dump(params), headers: headers
168
194
  expect(response.status).to eq(200)
169
195
  expected = updated_foo.clone.merge({:value => 'baz'})
170
196
  expected[:widgetEdit] = nil
@@ -173,13 +199,13 @@ describe 'Requesting Actions routes', :type => :request do
173
199
  end
174
200
 
175
201
  it 'should respond 500 with bad params' do
176
- post '/forest/actions/my_action/hooks/change', params: JSON.dump({ data: { attributes: { collection_name: 'Island' }}}), headers: { 'CONTENT_TYPE' => 'application/json' }
202
+ post '/forest/actions/my_action/hooks/change', params: JSON.dump({ data: { attributes: { collection_name: 'Island' }}}), headers: headers
177
203
  expect(response.status).to eq(500)
178
204
  expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action change hook: fields params is mandatory'})
179
205
  end
180
206
 
181
207
  it 'should respond 500 with bad hook result type' do
182
- post '/forest/actions/fail_action/hooks/change', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
208
+ post '/forest/actions/fail_action/hooks/change', params: JSON.dump(params), headers: headers
183
209
  expect(response.status).to eq(500)
184
210
  expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: hook must return an array of fields'})
185
211
  end
@@ -196,7 +222,7 @@ describe 'Requesting Actions routes', :type => :request do
196
222
  }
197
223
  }
198
224
  }
199
- post '/forest/actions/enums_action/hooks/change', params: JSON.dump(p), headers: { 'CONTENT_TYPE' => 'application/json' }
225
+ post '/forest/actions/enums_action/hooks/change', params: JSON.dump(p), headers: headers
200
226
  expect(response.status).to eq(200)
201
227
 
202
228
  expected_enum = updated_enum.clone.merge({ :enums => %w[c d e], :value => nil, :widgetEdit => nil})
@@ -219,7 +245,7 @@ describe 'Requesting Actions routes', :type => :request do
219
245
  }
220
246
  }
221
247
  }
222
- post '/forest/actions/multiple_enums_action/hooks/change', params: JSON.dump(p), headers: { 'CONTENT_TYPE' => 'application/json' }
248
+ post '/forest/actions/multiple_enums_action/hooks/change', params: JSON.dump(p), headers: headers
223
249
  expect(response.status).to eq(200)
224
250
 
225
251
  expected_multiple_enum = updated_multiple_enum.clone.merge({ :enums => %w[c d z], :widgetEdit => nil, :value => %w[c]})
@@ -243,7 +269,7 @@ describe 'Requesting Actions routes', :type => :request do
243
269
  }
244
270
  }
245
271
 
246
- post '/forest/actions/multiple_enums_action/hooks/change', params: JSON.dump(p), headers: { 'CONTENT_TYPE' => 'application/json' }
272
+ post '/forest/actions/multiple_enums_action/hooks/change', params: JSON.dump(p), headers: headers
247
273
  expect(response.status).to eq(200)
248
274
 
249
275
  expected_multiple_enum = wrongly_updated_multiple_enum.clone.merge({ :enums => %w[c d z], :widgetEdit => nil, :value => nil })
@@ -255,4 +281,66 @@ describe 'Requesting Actions routes', :type => :request do
255
281
  end
256
282
  end
257
283
  end
284
+
285
+ describe 'calling the action' do
286
+ before(:each) do
287
+ allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { true }
288
+ end
289
+
290
+ let(:all_records) { false }
291
+ let(:params) {
292
+ {
293
+ data: {
294
+ attributes: {
295
+ collection_name: 'Island',
296
+ ids: ['1'],
297
+ all_records: all_records,
298
+ smart_action_id: 'Island-Test'
299
+ },
300
+ type: 'custom-action-requests'
301
+ },
302
+ timezone: 'Europe/Paris'
303
+ }
304
+ }
305
+
306
+ describe 'without scopes' do
307
+ it 'should respond 200 and perform the action' do
308
+ post '/forest/actions/test', params: JSON.dump(params), headers: headers
309
+ expect(response.status).to eq(200)
310
+ expect(JSON.parse(response.body)).to eq({'success' => 'You are OK.'})
311
+ end
312
+ end
313
+
314
+ describe 'with scopes' do
315
+ describe 'when record is in scope' do
316
+ let(:scope_filters) { JSON.generate({ field: 'name', operator: 'equal', value: 'Corsica' }) }
317
+
318
+ it 'should respond 200 and perform the action' do
319
+ post '/forest/actions/test', params: JSON.dump(params), headers: headers
320
+ expect(response.status).to eq(200)
321
+ expect(JSON.parse(response.body)).to eq({'success' => 'You are OK.'})
322
+ end
323
+ end
324
+
325
+ describe 'when record is out of scope' do
326
+ let(:scope_filters) { JSON.generate({ field: 'name', operator: 'equal', value: 'Ré' }) }
327
+
328
+ it 'should respond 400 and NOT perform the action' do
329
+ post '/forest/actions/test', params: JSON.dump(params), headers: headers
330
+ expect(response.status).to eq(400)
331
+ expect(JSON.parse(response.body)).to eq({ 'error' => 'Smart Action: target record not found' })
332
+ end
333
+
334
+ describe 'and all_records are targeted' do
335
+ let(:all_records) { true }
336
+
337
+ it 'should respond 200 and perform the action' do
338
+ post '/forest/actions/test', params: JSON.dump(params), headers: headers
339
+ expect(response.status).to eq(200)
340
+ expect(JSON.parse(response.body)).to eq({'success' => 'You are OK.'})
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
258
346
  end