forest_liana 6.0.5 → 6.2.2

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: 95b8a3f6be7165cf4debb95a67e676c4846cd53dd5226ef6550b0f4a9ccab656
4
- data.tar.gz: eb8b01c7bd02b0c828fd5aa7e27ee8677ff5b1711fab9dc96512d607f5562fac
3
+ metadata.gz: ef9f6868496eb0c7e53ccd20fc0ee0853693a9954d56254dffe3e428be975570
4
+ data.tar.gz: 3b929c3692e18e69d8ce423d93f3ca223dc72a482290ab37342adc649a730dfb
5
5
  SHA512:
6
- metadata.gz: fbdf538274940ded194b63d76426557241122eb3ac5e1f6704e37fa295d4a3eece921d0f0c28546e919c454f580733ab9778960d21a1d758cb19f84ef4bd0771
7
- data.tar.gz: 2540d9a5c6289e23ac9d9d383c2d99f86883c2f493e32e5df5de0c659957696d329ff8f60a9339e7136d783ce3cae04e1d4499f418699e70901e65241a0116b4
6
+ metadata.gz: ebbcc8eb29e6b4dd9e9e95c1dd85b9ccd7c8ddaa73e297abe9ed36994a139becd595f55db56a909b07c9da1c2829fea68404d85f90a99b0f8eb91a460abd377e
7
+ data.tar.gz: 44d9e27b058eb2c3127c9efb5670100f032cda8f606a8a3b96a9ca7fc28d2a6b4ac7b97ca70e3514e5d32521ceb6aaa70c5c57aaf12f0d7e00aa179087e18bf7
@@ -1,9 +1,21 @@
1
1
  module ForestLiana
2
2
  class StatsController < ForestLiana::ApplicationController
3
3
  if Rails::VERSION::MAJOR < 4
4
- before_filter :find_resource, except: [:get_with_live_query]
4
+ before_filter only: [:get] do
5
+ find_resource()
6
+ check_permission('statWithParameters')
7
+ end
8
+ before_filter only: [:get_with_live_query] do
9
+ check_permission('liveQueries')
10
+ end
5
11
  else
6
- before_action :find_resource, except: [:get_with_live_query]
12
+ before_action only: [:get] do
13
+ find_resource()
14
+ check_permission('statWithParameters')
15
+ end
16
+ before_action only: [:get_with_live_query] do
17
+ check_permission('liveQueries')
18
+ end
7
19
  end
8
20
 
9
21
  CHART_TYPE_VALUE = 'Value'
@@ -64,5 +76,38 @@ module ForestLiana
64
76
  render json: {status: 404}, status: :not_found, serializer: nil
65
77
  end
66
78
  end
79
+
80
+ def get_live_query_request_info
81
+ params['query']
82
+ end
83
+
84
+ def get_stat_parameter_request_info
85
+ parameters = Rails::VERSION::MAJOR < 5 ? params.dup : params.permit(params.keys).to_h;
86
+
87
+ # Notice: Removes useless properties
88
+ parameters.delete('timezone');
89
+ parameters.delete('controller');
90
+ parameters.delete('action');
91
+
92
+ return parameters;
93
+ end
94
+
95
+ def check_permission(permission_name)
96
+ begin
97
+ query_request = permission_name == 'liveQueries' ? get_live_query_request_info : get_stat_parameter_request_info;
98
+ checker = ForestLiana::PermissionsChecker.new(
99
+ nil,
100
+ permission_name,
101
+ @rendering_id,
102
+ user_id: forest_user['id'],
103
+ query_request_info: query_request
104
+ )
105
+
106
+ return head :forbidden unless checker.is_authorized?
107
+ rescue => error
108
+ FOREST_LOGGER.error "Stats execution error: #{error}"
109
+ render serializer: nil, json: { status: 400 }, status: :bad_request
110
+ end
111
+ end
67
112
  end
68
113
  end
@@ -4,5 +4,11 @@ module ForestLiana
4
4
  collection_name = ForestLiana.name_for(active_record_class)
5
5
  ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
6
6
  end
7
+
8
+ def self.is_smart_field?(model, field_name)
9
+ collection = self.find_collection_from_model(model)
10
+ field_found = collection.fields.find { |collection_field| collection_field[:field].to_s == field_name } if collection
11
+ field_found && field_found[:is_virtual]
12
+ end
7
13
  end
8
14
  end
@@ -47,30 +47,56 @@ module ForestLiana
47
47
  end
48
48
 
49
49
  def parse_condition(condition)
50
- ensure_valid_condition(condition)
50
+ where = parse_condition_without_smart_field(condition)
51
51
 
52
- operator = condition['operator']
53
- value = condition['value']
54
- field = condition['field']
52
+ field_name = condition['field']
55
53
 
56
- if @operator_date_parser.is_date_operator?(operator)
57
- condition = @operator_date_parser.get_date_filter(operator, value)
58
- return "#{parse_field_name(field)} #{condition}"
54
+ if ForestLiana::SchemaHelper.is_smart_field?(@resource, field_name)
55
+ schema = ForestLiana.schema_for_resource(@resource)
56
+ field_schema = schema.fields.find do |field|
57
+ field[:field].to_s == field_name
58
+ end
59
+
60
+ unless field_schema.try(:[], :filter)
61
+ raise ForestLiana::Errors::NotImplementedMethodError.new("method filter on smart field '#{field_name}' not found")
62
+ end
63
+
64
+ return field_schema[:filter].call(condition, where)
59
65
  end
60
66
 
61
- if is_belongs_to(field)
62
- association = field.partition(':').first.to_sym
63
- association_field = field.partition(':').last
67
+ where
68
+ end
69
+
70
+ def get_association_field_and_resource(field_name)
71
+ if is_belongs_to(field_name)
72
+ association = field_name.partition(':').first.to_sym
73
+ association_field = field_name.partition(':').last
64
74
 
65
75
  unless @resource.reflect_on_association(association)
66
76
  raise ForestLiana::Errors::HTTP422Error.new("Association '#{association}' not found")
67
77
  end
68
78
 
69
79
  current_resource = @resource.reflect_on_association(association).klass
80
+
81
+ return association_field, current_resource
70
82
  else
71
- association_field = field
72
- current_resource = @resource
83
+ return field_name, @resource
73
84
  end
85
+ end
86
+
87
+ def parse_condition_without_smart_field(condition)
88
+ ensure_valid_condition(condition)
89
+
90
+ operator = condition['operator']
91
+ value = condition['value']
92
+ field_name = condition['field']
93
+
94
+ if @operator_date_parser.is_date_operator?(operator)
95
+ condition = @operator_date_parser.get_date_filter(operator, value)
96
+ return "#{parse_field_name(field_name)} #{condition}"
97
+ end
98
+
99
+ association_field, current_resource = get_association_field_and_resource(field_name)
74
100
 
75
101
  # NOTICE: Set the integer value instead of a string if "enum" type
76
102
  # NOTICE: Rails 3 do not have a defined_enums method
@@ -78,7 +104,7 @@ module ForestLiana
78
104
  value = current_resource.defined_enums[association_field][value]
79
105
  end
80
106
 
81
- parsed_field = parse_field_name(field)
107
+ parsed_field = parse_field_name(field_name)
82
108
  parsed_operator = parse_operator(operator)
83
109
  parsed_value = parse_value(operator, value)
84
110
  field_and_operator = "#{parsed_field} #{parsed_operator}"
@@ -149,16 +175,16 @@ module ForestLiana
149
175
 
150
176
  association = get_association_name_for_condition(field)
151
177
  quoted_table_name = ActiveRecord::Base.connection.quote_column_name(association)
152
- quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field.split(':')[1])
178
+ field_name = field.split(':')[1]
153
179
  else
154
180
  quoted_table_name = @resource.quoted_table_name
155
- quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field)
156
181
  current_resource = @resource
182
+ field_name = field
157
183
  end
184
+ quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field_name)
158
185
 
159
186
  column_found = current_resource.columns.find { |column| column.name == field.split(':').last }
160
-
161
- if column_found.nil?
187
+ if column_found.nil? && !ForestLiana::SchemaHelper.is_smart_field?(current_resource, field_name)
162
188
  raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found")
163
189
  end
164
190
 
@@ -6,13 +6,16 @@ module ForestLiana
6
6
  # TODO: handle cache scopes per rendering
7
7
  @@expiration_in_seconds = (ENV['FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS'] || 3600).to_i
8
8
 
9
- def initialize(resource, permission_name, rendering_id, user_id:, smart_action_request_info: nil, collection_list_parameters: nil)
10
- @user_id = user_id
11
- @collection_name = ForestLiana.name_for(resource)
9
+ def initialize(resource, permission_name, rendering_id, user_id: nil, smart_action_request_info: nil, collection_list_parameters: nil, query_request_info: nil)
10
+
11
+ @collection_name = resource.present? ? ForestLiana.name_for(resource) : nil
12
12
  @permission_name = permission_name
13
13
  @rendering_id = rendering_id
14
+
15
+ @user_id = user_id
14
16
  @smart_action_request_info = smart_action_request_info
15
17
  @collection_list_parameters = collection_list_parameters
18
+ @query_request_info = query_request_info
16
19
  end
17
20
 
18
21
  def is_authorized?
@@ -48,6 +51,16 @@ module ForestLiana
48
51
 
49
52
  def is_allowed
50
53
  permissions = get_permissions_content
54
+
55
+ # NOTICE: check liveQueries permissions
56
+ if @permission_name === 'liveQueries'
57
+ return live_query_allowed?
58
+ elsif @permission_name === 'statWithParameters'
59
+ return stat_with_parameters_allowed?
60
+ end
61
+
62
+
63
+
51
64
  if permissions && permissions[@collection_name] &&
52
65
  permissions[@collection_name]['collection']
53
66
  if @permission_name === 'actions'
@@ -98,6 +111,16 @@ module ForestLiana
98
111
  permissions && permissions['data'] && permissions['data']['collections']
99
112
  end
100
113
 
114
+ def get_live_query_permissions_content
115
+ permissions = get_permissions
116
+ permissions && permissions['stats'] && permissions['stats']['queries']
117
+ end
118
+
119
+ def get_stat_with_parameters_content(statPermissionType)
120
+ permissions = get_permissions
121
+ permissions && permissions['stats'] && permissions['stats'][statPermissionType]
122
+ end
123
+
101
124
  def get_last_fetch
102
125
  permissions = get_permissions
103
126
  permissions && permissions['last_fetch']
@@ -138,6 +161,32 @@ module ForestLiana
138
161
  ).is_scope_in_request?(@collection_list_parameters)
139
162
  end
140
163
 
164
+ def live_query_allowed?
165
+ live_queries_permissions = get_live_query_permissions_content
166
+
167
+ return false unless live_queries_permissions
168
+
169
+ # NOTICE: @query_request_info matching an existing live query
170
+ return live_queries_permissions.include? @query_request_info
171
+ end
172
+
173
+ def stat_with_parameters_allowed?
174
+ permissionType = @query_request_info['type'].downcase + 's'
175
+ pool_permissions = get_stat_with_parameters_content(permissionType)
176
+
177
+ return false unless pool_permissions
178
+
179
+ # NOTICE: equivalent to Object.values in js & removes nil values
180
+ array_query_request_info = @query_request_info.values.filter_map{ |x| x unless x.nil? }
181
+
182
+ # NOTICE: pool_permissions contains the @query_request_info
183
+ # we use the intersection between statPermission and @query_request_info
184
+ return pool_permissions.any? {
185
+ |statPermission|
186
+ (array_query_request_info & statPermission.values) == array_query_request_info;
187
+ }
188
+ end
189
+
141
190
  def date_difference_in_seconds(date1, date2)
142
191
  (date1 - date2).to_i
143
192
  end
@@ -46,6 +46,12 @@ module ForestLiana
46
46
  end
47
47
  end
48
48
 
49
+ class NotImplementedMethodError < ExpectedError
50
+ def initialize(message = "Method not implemented")
51
+ super(501, :internal_server_error, message, 'MethodNotImplementedError')
52
+ end
53
+ end
54
+
49
55
  class InconsistentSecretAndRenderingError < ExpectedError
50
56
  def initialize(message=ForestLiana::MESSAGES[:SERVER_TRANSACTION][:SECRET_AND_RENDERINGID_INCONSISTENT])
51
57
  super(500, :internal_server_error, message, 'InconsistentSecretAndRenderingError')
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "6.0.5"
2
+ VERSION = "6.2.2"
3
3
  end
@@ -2,4 +2,5 @@ class Island < ActiveRecord::Base
2
2
  self.table_name = 'isle'
3
3
 
4
4
  has_many :trees
5
+ has_one :location
5
6
  end
@@ -0,0 +1,3 @@
1
+ class Location < ActiveRecord::Base
2
+ belongs_to :island
3
+ end
@@ -0,0 +1,2 @@
1
+ class Reference < ActiveRecord::Base
2
+ end
@@ -0,0 +1,8 @@
1
+ class CreateReferences < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :references do |t|
4
+
5
+ t.timestamps
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ class CreateLocations < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :locations do |t|
4
+ t.string :coordinates
5
+ t.references :island, index: true
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2019_07_16_135241) do
13
+ ActiveRecord::Schema.define(version: 2021_03_26_140855) do
14
14
 
15
15
  create_table "isle", force: :cascade do |t|
16
16
  t.string "name"
@@ -19,6 +19,19 @@ ActiveRecord::Schema.define(version: 2019_07_16_135241) do
19
19
  t.datetime "updated_at"
20
20
  end
21
21
 
22
+ create_table "locations", force: :cascade do |t|
23
+ t.string "coordinates"
24
+ t.integer "island_id"
25
+ t.datetime "created_at", precision: 6, null: false
26
+ t.datetime "updated_at", precision: 6, null: false
27
+ t.index ["island_id"], name: "index_locations_on_island_id"
28
+ end
29
+
30
+ create_table "references", force: :cascade do |t|
31
+ t.datetime "created_at", precision: 6, null: false
32
+ t.datetime "updated_at", precision: 6, null: false
33
+ end
34
+
22
35
  create_table "trees", force: :cascade do |t|
23
36
  t.string "name"
24
37
  t.integer "owner_id"
@@ -0,0 +1,10 @@
1
+ class Forest::Location
2
+ include ForestLiana::Collection
3
+
4
+ collection :Location
5
+
6
+ field :alter_coordinates, type: 'String' do
7
+ object.name + 'XYZ'
8
+ end
9
+
10
+ end
@@ -0,0 +1,15 @@
1
+ class Forest::User
2
+ include ForestLiana::Collection
3
+
4
+ collection :User
5
+
6
+ filter_cap_name = lambda do |condition, where|
7
+ capitalize_name = condition['value'].capitalize
8
+ "name IS '#{capitalize_name}'"
9
+ end
10
+
11
+ field :cap_name, type: 'String', filter: filter_cap_name do
12
+ object.name.upcase
13
+ end
14
+
15
+ end
@@ -0,0 +1,114 @@
1
+ require 'rails_helper'
2
+ require 'json'
3
+
4
+ describe "Stats", type: :request do
5
+
6
+ token = JWT.encode({
7
+ id: 38,
8
+ email: 'michael.kelso@that70.show',
9
+ first_name: 'Michael',
10
+ last_name: 'Kelso',
11
+ team: 'Operations',
12
+ rendering_id: 16,
13
+ exp: Time.now.to_i + 2.weeks.to_i
14
+ }, ForestLiana.auth_secret, 'HS256')
15
+
16
+ headers = {
17
+ 'Accept' => 'application/json',
18
+ 'Content-Type' => 'application/json',
19
+ 'Authorization' => "Bearer #{token}"
20
+ }
21
+
22
+ let(:schema) {
23
+ [
24
+ ForestLiana::Model::Collection.new({
25
+ name: 'Products',
26
+ fields: [],
27
+ actions: []
28
+ })
29
+ ]
30
+ }
31
+
32
+ before do
33
+ allow(ForestLiana).to receive(:apimap).and_return(schema)
34
+ end
35
+
36
+ before(:each) do
37
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
38
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
39
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
40
+
41
+ allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { true }
42
+
43
+ allow_any_instance_of(ForestLiana::ValueStatGetter).to receive(:perform) { true }
44
+ allow_any_instance_of(ForestLiana::QueryStatGetter).to receive(:perform) { true }
45
+ end
46
+
47
+
48
+
49
+ describe 'POST /stats/:collection' do
50
+ params = { type: 'Value', collection: 'User', aggregate: 'Count' }
51
+
52
+ it 'should respond 200' do
53
+ data = ForestLiana::Model::Stat.new(value: { countCurrent: 0, countPrevious: 0 })
54
+ allow_any_instance_of(ForestLiana::ValueStatGetter).to receive(:record) { data }
55
+ # NOTICE: bypass : find_resource error
56
+ allow_any_instance_of(ForestLiana::StatsController).to receive(:find_resource) { true }
57
+ allow(ForestLiana::QueryHelper).to receive(:get_one_association_names_symbol) { true }
58
+
59
+ post '/forest/stats/Products', params: JSON.dump(params), headers: headers
60
+ expect(response.status).to eq(200)
61
+ end
62
+
63
+ it 'should respond 401 with no headers' do
64
+ post '/forest/stats/Products', params: JSON.dump(params)
65
+ expect(response.status).to eq(401)
66
+ end
67
+
68
+ it 'should respond 404 with non existing collection' do
69
+ allow_any_instance_of(ForestLiana::ValueStatGetter).to receive(:record) { nil }
70
+
71
+ post '/forest/stats/NoCollection', params: {}, headers: headers
72
+ expect(response.status).to eq(404)
73
+ end
74
+
75
+ # it 'should respond 403 Forbidden' do
76
+ # allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { false }
77
+
78
+ # post '/forest/stats/Products', params: JSON.dump(params), headers: headers
79
+ # expect(response.status).to eq(403)
80
+ # end
81
+ end
82
+
83
+ describe 'POST /stats' do
84
+ params = { query: 'SELECT COUNT(*) AS value FROM products;' }
85
+
86
+ it 'should respond 200' do
87
+ data = ForestLiana::Model::Stat.new(value: { value: 0, objective: 0 })
88
+ allow_any_instance_of(ForestLiana::QueryStatGetter).to receive(:record) { data }
89
+
90
+ post '/forest/stats', params: JSON.dump(params), headers: headers
91
+ expect(response.status).to eq(200)
92
+ end
93
+
94
+ it 'should respond 401 with no headers' do
95
+ post '/forest/stats', params: JSON.dump(params)
96
+ expect(response.status).to eq(401)
97
+ end
98
+
99
+ it 'should respond 403 Forbidden' do
100
+ allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { false }
101
+
102
+ post '/forest/stats', params: JSON.dump(params), headers: headers
103
+ expect(response.status).to eq(403)
104
+ end
105
+
106
+ it 'should respond 422 with unprocessable query' do
107
+ allow_any_instance_of(ForestLiana::QueryStatGetter).to receive(:perform) { raise ForestLiana::Errors::LiveQueryError.new }
108
+
109
+ post '/forest/stats', params: JSON.dump(params), headers: headers
110
+ expect(response.status).to eq(422)
111
+ end
112
+ end
113
+
114
+ end