forest_liana 6.0.5 → 6.2.2
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.
- checksums.yaml +4 -4
- data/app/controllers/forest_liana/stats_controller.rb +47 -2
- data/app/helpers/forest_liana/schema_helper.rb +6 -0
- data/app/services/forest_liana/filters_parser.rb +43 -17
- data/app/services/forest_liana/permissions_checker.rb +52 -3
- data/config/initializers/errors.rb +6 -0
- data/lib/forest_liana/version.rb +1 -1
- data/spec/dummy/app/models/island.rb +1 -0
- data/spec/dummy/app/models/location.rb +3 -0
- data/spec/dummy/app/models/reference.rb +2 -0
- data/spec/dummy/db/migrate/20210326110524_create_references.rb +8 -0
- data/spec/dummy/db/migrate/20210326140855_create_locations.rb +10 -0
- data/spec/dummy/db/schema.rb +14 -1
- data/spec/dummy/lib/forest_liana/collections/location.rb +10 -0
- data/spec/dummy/lib/forest_liana/collections/user.rb +15 -0
- data/spec/requests/stats_spec.rb +114 -0
- data/spec/services/forest_liana/permissions_checker_acl_disabled_spec.rb +3 -3
- data/spec/services/forest_liana/permissions_checker_acl_enabled_spec.rb +2 -2
- data/spec/services/forest_liana/permissions_checker_live_queries_spec.rb +131 -0
- data/spec/services/forest_liana/resources_getter_spec.rb +359 -0
- data/spec/services/forest_liana/schema_adapter_spec.rb +1 -1
- metadata +135 -119
- data/test/services/forest_liana/resources_getter_test.rb +0 -284
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef9f6868496eb0c7e53ccd20fc0ee0853693a9954d56254dffe3e428be975570
|
4
|
+
data.tar.gz: 3b929c3692e18e69d8ce423d93f3ca223dc72a482290ab37342adc649a730dfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 :
|
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 :
|
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
|
-
|
50
|
+
where = parse_condition_without_smart_field(condition)
|
51
51
|
|
52
|
-
|
53
|
-
value = condition['value']
|
54
|
-
field = condition['field']
|
52
|
+
field_name = condition['field']
|
55
53
|
|
56
|
-
if
|
57
|
-
|
58
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
10
|
-
|
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')
|
data/lib/forest_liana/version.rb
CHANGED
data/spec/dummy/db/schema.rb
CHANGED
@@ -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:
|
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,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
|