forest_liana 9.22.17 → 9.22.18
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/services/forest_liana/resources_getter.rb +2 -0
- data/app/services/forest_liana/scope_manager.rb +70 -16
- data/lib/forest_liana/version.rb +1 -1
- data/spec/requests/resources_spec.rb +83 -0
- data/spec/services/forest_liana/ability/permission/smart_action_checker_spec.rb +49 -0
- data/spec/services/forest_liana/resources_getter_spec.rb +31 -0
- data/spec/services/forest_liana/scope_manager_spec.rb +162 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2876bbcc3a687f4bbb73e23af6d9ca98e91ebce886c749afdcca20eb2fc0bed
|
|
4
|
+
data.tar.gz: 2c2fcda6f31a2a176caab533e30e89fbea88f3dd8fbc9e870ffe9a29019e2284
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ddfedbe5559ec9b55d869fec7e56e61f9e0560d574fd66d035180bb98e39bae8351739346f86d4faf720e07530c21d858660c0cfc8ae9a054d6ab3780d7088f
|
|
7
|
+
data.tar.gz: 12be328781a0a455c042c23668e3f3ca6946b437832f620398d97e7bc4bbd29d65733ff3f28b87dc7596e82e35539ed0eaff6f6a82efbb06015c7a7f82f39b1d
|
|
@@ -231,6 +231,8 @@ module ForestLiana
|
|
|
231
231
|
end
|
|
232
232
|
|
|
233
233
|
def apply_live_query_segment
|
|
234
|
+
ScopeManager.assert_can_execute_segment_query!(@user, @collection_name, @params[:segmentQuery])
|
|
235
|
+
|
|
234
236
|
LiveQueryChecker.new(@params[:segmentQuery], 'Live Query Segment').validate
|
|
235
237
|
|
|
236
238
|
begin
|
|
@@ -2,6 +2,7 @@ module ForestLiana
|
|
|
2
2
|
class ScopeManager
|
|
3
3
|
# 5 minutes expiration cache
|
|
4
4
|
@@scope_cache_expiration_delta = 300
|
|
5
|
+
SEGMENT_UNION_SEPARATOR = '/*MULTI-SEGMENTS-QUERIES-UNION*/ UNION '
|
|
5
6
|
|
|
6
7
|
def self.apply_scopes_on_records(records, user, collection_name, timezone)
|
|
7
8
|
scope_filters = get_scope(collection_name, user)
|
|
@@ -65,25 +66,69 @@ module ForestLiana
|
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def self.invalidate_scope_cache(rendering_id)
|
|
68
|
-
Rails.cache.delete(
|
|
69
|
-
RequestCache.delete(
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def self.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
Rails.cache.delete(scope_cache_key(rendering_id))
|
|
70
|
+
RequestCache.delete(scope_cache_key(rendering_id))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.assert_can_execute_segment_query!(user, collection_name, query)
|
|
74
|
+
return if Ability::ALLOWED_PERMISSION_LEVELS.include?(user['permission_level'])
|
|
75
|
+
raise Ability::Exceptions::AccessDenied.new unless query.is_a?(String)
|
|
76
|
+
return if segment_query_allowed?(segment_scopes(user['rendering_id']), collection_name, query)
|
|
77
|
+
return if segment_query_allowed?(segment_scopes(user['rendering_id'], force_fetch: true), collection_name, query)
|
|
78
|
+
|
|
79
|
+
raise Ability::Exceptions::AccessDenied.new
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.segment_query_allowed?(scopes, collection_name, query)
|
|
83
|
+
allowed_queries = (scopes['segments'] || {})[collection_name]
|
|
84
|
+
return false if allowed_queries.nil?
|
|
85
|
+
|
|
86
|
+
allowed_set = allowed_queries.map { |allowed| normalize_segment_query(allowed) }
|
|
87
|
+
return true if allowed_set.include?(normalize_segment_query(query))
|
|
88
|
+
|
|
89
|
+
parts = query.split(SEGMENT_UNION_SEPARATOR)
|
|
90
|
+
parts.any? && parts.all? { |part| allowed_set.include?(normalize_segment_query(part)) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.normalize_segment_query(query)
|
|
94
|
+
query.sub(/[\s;]+\z/, '').strip
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.fetch_scopes(rendering_id, force_fetch: false)
|
|
98
|
+
key = scope_cache_key(rendering_id)
|
|
99
|
+
return RequestCache.write(key, cached_scopes(key, rendering_id, force_fetch)) if force_fetch
|
|
100
|
+
|
|
101
|
+
RequestCache.fetch(key) { cached_scopes(key, rendering_id, force_fetch) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.segment_scopes(rendering_id, force_fetch: false)
|
|
105
|
+
fetch_scopes(rendering_id, force_fetch: force_fetch)
|
|
106
|
+
rescue StandardError
|
|
107
|
+
raise Errors::PermissionsUnavailableError.new
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.cached_scopes(key, rendering_id, force_fetch)
|
|
111
|
+
Rails.cache.fetch(key, expires_in: @@scope_cache_expiration_delta, force: force_fetch) do
|
|
112
|
+
load_scopes(rendering_id)
|
|
84
113
|
end
|
|
85
114
|
end
|
|
86
115
|
|
|
116
|
+
def self.load_scopes(rendering_id)
|
|
117
|
+
response = ForestLiana::ForestApiRequester.get("/liana/v4/permissions/renderings/#{rendering_id}")
|
|
118
|
+
raise 'Unable to fetch scopes' unless response.is_a?(Net::HTTPOK)
|
|
119
|
+
parsed_response = JSON.parse(response.body)
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
'scopes' => decode_scope(parsed_response['collections']),
|
|
123
|
+
'segments' => decode_segments(parsed_response['collections']),
|
|
124
|
+
'team' => parsed_response['team']
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.scope_cache_key(rendering_id)
|
|
129
|
+
"forest.scopes.#{rendering_id}"
|
|
130
|
+
end
|
|
131
|
+
|
|
87
132
|
def self.decode_scope(raw_scopes)
|
|
88
133
|
scopes = {}
|
|
89
134
|
raw_scopes.each do |collection_name, value|
|
|
@@ -92,5 +137,14 @@ module ForestLiana
|
|
|
92
137
|
|
|
93
138
|
scopes
|
|
94
139
|
end
|
|
140
|
+
|
|
141
|
+
def self.decode_segments(raw_collections)
|
|
142
|
+
raw_collections.each_with_object({}) do |(collection_name, value), segments|
|
|
143
|
+
segments[collection_name] = value['segments'] unless value['segments'].nil?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private_class_method :segment_query_allowed?, :normalize_segment_query, :segment_scopes, :cached_scopes,
|
|
148
|
+
:load_scopes, :scope_cache_key
|
|
95
149
|
end
|
|
96
150
|
end
|
data/lib/forest_liana/version.rb
CHANGED
|
@@ -635,6 +635,89 @@ describe 'Requesting Tree resources', :type => :request do
|
|
|
635
635
|
end
|
|
636
636
|
end
|
|
637
637
|
|
|
638
|
+
describe 'live query segment permission' do
|
|
639
|
+
let(:listed_segment_query) { 'SELECT id FROM trees WHERE name IS NOT NULL' }
|
|
640
|
+
let(:unlisted_segment_query) { 'SELECT id FROM trees WHERE 1 = 0' }
|
|
641
|
+
|
|
642
|
+
restricted_token = JWT.encode({
|
|
643
|
+
id: 1,
|
|
644
|
+
email: 'michael.kelso@that70.show',
|
|
645
|
+
first_name: 'Michael',
|
|
646
|
+
last_name: 'Kelso',
|
|
647
|
+
team: 'Operations',
|
|
648
|
+
rendering_id: 16,
|
|
649
|
+
exp: Time.now.to_i + 2.weeks.to_i,
|
|
650
|
+
permission_level: 'user'
|
|
651
|
+
}, ForestLiana.auth_secret, 'HS256')
|
|
652
|
+
|
|
653
|
+
restricted_headers = headers.merge('Authorization' => "Bearer #{restricted_token}")
|
|
654
|
+
|
|
655
|
+
let(:list_params) {
|
|
656
|
+
{
|
|
657
|
+
fields: { 'Tree' => 'id,name' },
|
|
658
|
+
page: { 'number' => '1', 'size' => '10' },
|
|
659
|
+
searchExtended: '0',
|
|
660
|
+
sort: '-id',
|
|
661
|
+
timezone: 'Europe/Paris'
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
it 'refuses a listing whose segment query is not one of the team segments' do
|
|
666
|
+
get '/forest/Tree', params: list_params.merge(segmentQuery: unlisted_segment_query), headers: restricted_headers
|
|
667
|
+
|
|
668
|
+
expect(response.status).to eq(403)
|
|
669
|
+
expect(JSON.parse(response.body)['errors'][0]['name']).to eq('AccessDenied')
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
it 'serves the listing when the rendering lists that exact segment query' do
|
|
673
|
+
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(
|
|
674
|
+
'scopes' => {},
|
|
675
|
+
'segments' => { 'Tree' => [listed_segment_query] },
|
|
676
|
+
'team' => { 'id' => '1', 'name' => 'Operations' }
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
get '/forest/Tree', params: list_params.merge(segmentQuery: listed_segment_query), headers: restricted_headers
|
|
680
|
+
|
|
681
|
+
expect(response.status).to eq(200)
|
|
682
|
+
expect(JSON.parse(response.body)['data'][0]['attributes']['name']).to eq('Lemon Tree')
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
it 'refuses the count the same way' do
|
|
686
|
+
get '/forest/Tree/count', params: { segmentQuery: unlisted_segment_query, timezone: 'Europe/Paris' }, headers: restricted_headers
|
|
687
|
+
|
|
688
|
+
expect(response.status).to eq(403)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
it 'refuses the csv export the same way' do
|
|
692
|
+
get '/forest/Tree.csv', params: { segmentQuery: unlisted_segment_query, header: 'id,name' }, headers: restricted_headers
|
|
693
|
+
|
|
694
|
+
expect(response.status).to eq(403)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
it 'refuses a select-all bulk delete without deleting anything' do
|
|
698
|
+
params = {
|
|
699
|
+
data: {
|
|
700
|
+
attributes: {
|
|
701
|
+
collection_name: 'Tree',
|
|
702
|
+
all_records: true,
|
|
703
|
+
all_records_subset_query: { segmentQuery: unlisted_segment_query }
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
delete '/forest/Tree', params: JSON.dump(params), headers: restricted_headers
|
|
709
|
+
|
|
710
|
+
expect(response.status).to eq(403)
|
|
711
|
+
expect(Tree.count).to eq(1)
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
it 'still serves an admin an unlisted segment query' do
|
|
715
|
+
get '/forest/Tree', params: list_params.merge(segmentQuery: unlisted_segment_query), headers: headers
|
|
716
|
+
|
|
717
|
+
expect(response.status).to eq(200)
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
|
|
638
721
|
describe 'csv' do
|
|
639
722
|
it 'should return CSV with correct headers and data' do
|
|
640
723
|
params = {
|
|
@@ -305,6 +305,55 @@ module ForestLiana
|
|
|
305
305
|
)
|
|
306
306
|
end
|
|
307
307
|
|
|
308
|
+
describe 'with a live query segment on the "select all" selection' do
|
|
309
|
+
let(:segment_query) { "SELECT id FROM isle WHERE name = 'foo'" }
|
|
310
|
+
|
|
311
|
+
before do
|
|
312
|
+
Rails.cache.write('forest.has_permission', false)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def select_all_parameters_with_segment
|
|
316
|
+
select_all_params = params.deep_dup
|
|
317
|
+
select_all_params['data']['attributes'][:all_records] = true
|
|
318
|
+
select_all_params['data']['attributes'][:ids] = []
|
|
319
|
+
select_all_params['data']['attributes'][:all_records_subset_query] =
|
|
320
|
+
select_all_params['data']['attributes'][:all_records_subset_query].merge('segmentQuery' => segment_query)
|
|
321
|
+
ActionController::Parameters.new(select_all_params).permit!
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'refuses with AccessDenied when the query is not one of the requester\'s segments' do
|
|
325
|
+
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(
|
|
326
|
+
'scopes' => {}, 'segments' => {}, 'team' => {'id' => '1', 'name' => 'Operations'}
|
|
327
|
+
)
|
|
328
|
+
action['approvalRequired'] = [1]
|
|
329
|
+
action['userApprovalEnabled'] = [7]
|
|
330
|
+
smart_action_checker = ForestLiana::Ability::Permission::SmartActionChecker.new(
|
|
331
|
+
select_all_parameters_with_segment, Island, action, user, nil, forest_user
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
expect { smart_action_checker.can_execute? }
|
|
335
|
+
.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it 'resolves the record ids when the query is one of the requester\'s segments' do
|
|
339
|
+
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(
|
|
340
|
+
'scopes' => {},
|
|
341
|
+
'segments' => { 'Island' => [segment_query] },
|
|
342
|
+
'team' => {'id' => '1', 'name' => 'Operations'}
|
|
343
|
+
)
|
|
344
|
+
action['approvalRequired'] = [1]
|
|
345
|
+
action['userApprovalEnabled'] = [7]
|
|
346
|
+
smart_action_checker = ForestLiana::Ability::Permission::SmartActionChecker.new(
|
|
347
|
+
select_all_parameters_with_segment, Island, action, user, nil, forest_user
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
expect { smart_action_checker.can_execute? }
|
|
351
|
+
.to raise_error(ForestLiana::Ability::Exceptions::RequireApproval) do |error|
|
|
352
|
+
expect(error.data[:recordIds]).to eq(['1'])
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
308
357
|
it 'should raise RequireApproval error if match approvalRequiredConditions' do
|
|
309
358
|
parameters = ActionController::Parameters.new(params).permit!
|
|
310
359
|
action['approvalRequired'] = [1]
|
|
@@ -153,6 +153,13 @@ module ForestLiana
|
|
|
153
153
|
|
|
154
154
|
describe 'when a live query segment uses a context variable' do
|
|
155
155
|
let(:user) { { 'id' => '1', 'name' => 'Michel', 'rendering_id' => rendering_id } }
|
|
156
|
+
let(:scopes) {
|
|
157
|
+
{
|
|
158
|
+
'scopes' => {},
|
|
159
|
+
'segments' => { 'User' => ["SELECT id FROM users WHERE name = '{{currentUser.name}}'"] },
|
|
160
|
+
'team' => {'id' => '1', 'name' => 'Operations'}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
156
163
|
let(:getter) do
|
|
157
164
|
described_class.new(resource, {
|
|
158
165
|
page: { size: pageSize, number: pageNumber },
|
|
@@ -209,6 +216,30 @@ module ForestLiana
|
|
|
209
216
|
end
|
|
210
217
|
end
|
|
211
218
|
|
|
219
|
+
describe 'when the live query segment is not one of the user\'s rendering segments' do
|
|
220
|
+
let(:getter) do
|
|
221
|
+
described_class.new(resource, {
|
|
222
|
+
page: { size: pageSize, number: pageNumber },
|
|
223
|
+
fields: { 'User' => 'id,name' },
|
|
224
|
+
segmentQuery: 'SELECT id FROM users WHERE name IS NOT NULL',
|
|
225
|
+
}, user)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it 'raises AccessDenied from the constructor, before any SQL runs' do
|
|
229
|
+
queries = []
|
|
230
|
+
subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*, payload|
|
|
231
|
+
queries << payload[:sql] unless payload[:name] == 'SCHEMA' || payload[:cached]
|
|
232
|
+
end
|
|
233
|
+
begin
|
|
234
|
+
expect { getter }.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
235
|
+
ensure
|
|
236
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
expect(queries).to be_empty
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
212
243
|
describe 'when there are more records than the page size' do
|
|
213
244
|
describe 'when asking for the 1st page and 15 records' do
|
|
214
245
|
let(:pageSize) { 15 }
|
|
@@ -70,6 +70,7 @@ module ForestLiana
|
|
|
70
70
|
expect(Rails.cache.read('forest.scopes.' + rendering_id.to_s)).to eq(
|
|
71
71
|
{
|
|
72
72
|
'scopes' => {'User' => {'aggregator' => 'and', 'conditions'=>[{'field' => 'id', 'operator' => 'greater_than', 'value' => '1'}]}},
|
|
73
|
+
'segments' => {'User' => []},
|
|
73
74
|
'team' => {'id' => '1', 'name' => 'Operations'},
|
|
74
75
|
})
|
|
75
76
|
end
|
|
@@ -120,6 +121,7 @@ module ForestLiana
|
|
|
120
121
|
expect(Rails.cache.read('forest.scopes.' + rendering_id.to_s)).to eq(
|
|
121
122
|
{
|
|
122
123
|
'scopes' => {'User' => {'aggregator' => 'and', 'conditions'=>[{'field' => 'email', 'operator' => 'equal', 'value' => '{{currentUser.email}}'}]}},
|
|
124
|
+
'segments' => {'User' => []},
|
|
123
125
|
'team' => {'id' => '1', 'name' => 'Operations'},
|
|
124
126
|
}
|
|
125
127
|
)
|
|
@@ -165,6 +167,7 @@ module ForestLiana
|
|
|
165
167
|
expect(Rails.cache.read('forest.scopes.' + rendering_id.to_s)).to eq(
|
|
166
168
|
{
|
|
167
169
|
'scopes' => {},
|
|
170
|
+
'segments' => {'User' => []},
|
|
168
171
|
'team' => {'id' => '1', 'name' => 'Operations'},
|
|
169
172
|
}
|
|
170
173
|
)
|
|
@@ -172,6 +175,165 @@ module ForestLiana
|
|
|
172
175
|
end
|
|
173
176
|
end
|
|
174
177
|
|
|
178
|
+
describe '.assert_can_execute_segment_query!' do
|
|
179
|
+
let(:segment_query) { 'SELECT id FROM users WHERE name IS NOT NULL' }
|
|
180
|
+
let(:other_segment_query) { 'SELECT id FROM users WHERE id > 10;' }
|
|
181
|
+
let(:scopes_body) {
|
|
182
|
+
{
|
|
183
|
+
'collections' => {
|
|
184
|
+
'User' => { 'scope' => nil, 'segments' => [segment_query, other_segment_query] }
|
|
185
|
+
},
|
|
186
|
+
'team' => {'id' => '1', 'name' => 'Operations'}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let(:rendering_path) { "/liana/v4/permissions/renderings/#{rendering_id}" }
|
|
191
|
+
|
|
192
|
+
def api_response(body)
|
|
193
|
+
body = JSON.generate(body)
|
|
194
|
+
response = Net::HTTPOK.new({}, 200, body)
|
|
195
|
+
allow(response).to receive(:body).and_return(body)
|
|
196
|
+
response
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
before do
|
|
200
|
+
allow(ForestLiana::ForestApiRequester).to receive(:get).and_return(api_response(scopes_body))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'accepts a query the rendering lists for the collection' do
|
|
204
|
+
expect {
|
|
205
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, segment_query)
|
|
206
|
+
}.not_to raise_error
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'refuses a query the rendering does not list' do
|
|
210
|
+
expect {
|
|
211
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, 'SELECT id FROM users WHERE 1 = 0')
|
|
212
|
+
}.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it 'refuses a collection the rendering payload carries no segments for' do
|
|
216
|
+
expect {
|
|
217
|
+
described_class.assert_can_execute_segment_query!(user, 'Island', segment_query)
|
|
218
|
+
}.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'accepts a UNION of configured segments, ignoring their trailing semicolons' do
|
|
222
|
+
union = "#{segment_query}/*MULTI-SEGMENTS-QUERIES-UNION*/ UNION SELECT id FROM users WHERE id > 10"
|
|
223
|
+
|
|
224
|
+
expect {
|
|
225
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, union)
|
|
226
|
+
}.not_to raise_error
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'refuses a UNION holding one unconfigured segment' do
|
|
230
|
+
union = "#{segment_query}/*MULTI-SEGMENTS-QUERIES-UNION*/ UNION SELECT id FROM users WHERE 1 = 0"
|
|
231
|
+
|
|
232
|
+
expect {
|
|
233
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, union)
|
|
234
|
+
}.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'accepts a configured query sent without its trailing semicolon' do
|
|
238
|
+
expect {
|
|
239
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, 'SELECT id FROM users WHERE id > 10')
|
|
240
|
+
}.not_to raise_error
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'accepts a configured segment that itself holds the union separator' do
|
|
244
|
+
union_segment = "#{segment_query}/*MULTI-SEGMENTS-QUERIES-UNION*/ UNION SELECT id FROM users WHERE id = 1"
|
|
245
|
+
scopes_body['collections']['User']['segments'] = [union_segment]
|
|
246
|
+
allow(ForestLiana::ForestApiRequester).to receive(:get).and_return(api_response(scopes_body))
|
|
247
|
+
|
|
248
|
+
expect {
|
|
249
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, union_segment)
|
|
250
|
+
}.not_to raise_error
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'answers 503 when the rendering cannot be fetched on a cold cache' do
|
|
254
|
+
allow(ForestLiana::ForestApiRequester).to receive(:get)
|
|
255
|
+
.and_return(Net::HTTPServiceUnavailable.new({}, 503, ''))
|
|
256
|
+
|
|
257
|
+
expect {
|
|
258
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, segment_query)
|
|
259
|
+
}.to raise_error(ForestLiana::Errors::PermissionsUnavailableError)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'refuses an empty query' do
|
|
263
|
+
expect {
|
|
264
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, '')
|
|
265
|
+
}.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'refuses a query that is not a string, without fetching the rendering' do
|
|
269
|
+
expect {
|
|
270
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, [segment_query])
|
|
271
|
+
}.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
272
|
+
expect(ForestLiana::ForestApiRequester).not_to have_received(:get)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
%w[admin editor developer].each do |permission_level|
|
|
276
|
+
it "lets a #{permission_level} run any query, without fetching the rendering" do
|
|
277
|
+
expect {
|
|
278
|
+
described_class.assert_can_execute_segment_query!(
|
|
279
|
+
user.merge('permission_level' => permission_level), collection_name, 'SELECT id FROM users WHERE 1 = 0'
|
|
280
|
+
)
|
|
281
|
+
}.not_to raise_error
|
|
282
|
+
|
|
283
|
+
expect(ForestLiana::ForestApiRequester).not_to have_received(:get)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
describe 'when the cached rendering may be stale' do
|
|
288
|
+
let(:scopes_without_the_segment) {
|
|
289
|
+
body = scopes_body.deep_dup
|
|
290
|
+
body['collections']['User']['segments'] = []
|
|
291
|
+
body
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
it 'refetches the rendering once before refusing a fresher rendering could allow' do
|
|
295
|
+
allow(ForestLiana::ForestApiRequester).to receive(:get)
|
|
296
|
+
.and_return(api_response(scopes_without_the_segment), api_response(scopes_body))
|
|
297
|
+
|
|
298
|
+
expect {
|
|
299
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, segment_query)
|
|
300
|
+
}.not_to raise_error
|
|
301
|
+
expect(ForestLiana::ForestApiRequester).to have_received(:get).with(rendering_path).exactly(2).times
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
it 'still refuses when the refetched rendering denies it too' do
|
|
305
|
+
allow(ForestLiana::ForestApiRequester).to receive(:get)
|
|
306
|
+
.and_return(api_response(scopes_without_the_segment))
|
|
307
|
+
|
|
308
|
+
expect {
|
|
309
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, segment_query)
|
|
310
|
+
}.to raise_error(ForestLiana::Ability::Exceptions::AccessDenied)
|
|
311
|
+
expect(ForestLiana::ForestApiRequester).to have_received(:get).with(rendering_path).exactly(2).times
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'keeps the cached rendering and answers 503 when the refetch fails' do
|
|
315
|
+
unavailable = Net::HTTPServiceUnavailable.new({}, 503, '')
|
|
316
|
+
allow(ForestLiana::ForestApiRequester).to receive(:get)
|
|
317
|
+
.and_return(api_response(scopes_without_the_segment), unavailable)
|
|
318
|
+
|
|
319
|
+
expect {
|
|
320
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, segment_query)
|
|
321
|
+
}.to raise_error(ForestLiana::Errors::PermissionsUnavailableError)
|
|
322
|
+
expect(Rails.cache.read("forest.scopes.#{rendering_id}")['segments']).to eq('User' => [])
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it 'refetches a cache entry from before segments were cached, before deciding' do
|
|
326
|
+
Rails.cache.write("forest.scopes.#{rendering_id}", {'scopes' => {}, 'team' => {}})
|
|
327
|
+
allow(ForestLiana::ForestApiRequester).to receive(:get).and_return(api_response(scopes_body))
|
|
328
|
+
|
|
329
|
+
expect {
|
|
330
|
+
described_class.assert_can_execute_segment_query!(user, collection_name, segment_query)
|
|
331
|
+
}.not_to raise_error
|
|
332
|
+
expect(ForestLiana::ForestApiRequester).to have_received(:get).with(rendering_path).once
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
175
337
|
describe '#append_scope_for_user' do
|
|
176
338
|
let(:filters) { described_class.append_scope_for_user(existing_filter, user, collection_name) }
|
|
177
339
|
|