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 +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
|