forest_liana 9.22.12 → 9.22.13
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/actions_controller.rb +10 -0
- data/app/services/forest_liana/ability/permission.rb +19 -0
- data/app/services/forest_liana/single_record_selection.rb +11 -0
- data/lib/forest_liana/version.rb +1 -1
- data/spec/requests/actions_controller_spec.rb +41 -0
- data/spec/services/forest_liana/ability/permission_spec.rb +74 -0
- data/spec/services/forest_liana/search_query_builder_spec.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fcbf97d59c4b454c53e5f068f969e8e503e2a722c1a011e2d03b7f9e73a0c152
|
|
4
|
+
data.tar.gz: cd6608d3c224d6daaedd240928a34cec77d49c35bfdf52fdb0d5b5187a23cf02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78dd9047c6f8647076342d0a99f223ad66c7c33590b6b8be4726901d4aa6b057eb97eb4137cd6de3852fcf06d505621b77e6d6b653bb699443e753b57955a782
|
|
7
|
+
data.tar.gz: c0dc2372c158fb2f86929279b3594ae6aec5b2270ae6fd74a19871c7dc26af89b0b20d8d5f8a822ddd6147ab11d89413f80df582168ea31e41e7fac847c2e430
|
|
@@ -47,6 +47,12 @@ module ForestLiana
|
|
|
47
47
|
{:field_changed => found_field_changed, :fields => fields, :params => params, :user => forest_user}
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def ensure_single_record_selection(action, attributes)
|
|
51
|
+
return if SingleRecordSelection.allowed?(action, attributes)
|
|
52
|
+
|
|
53
|
+
raise ForestLiana::Errors::HTTP422Error.new(SingleRecordSelection::MESSAGE)
|
|
54
|
+
end
|
|
55
|
+
|
|
50
56
|
def handle_result(result, action)
|
|
51
57
|
if result.nil? || !result.is_a?(Array)
|
|
52
58
|
return render status: 500, json: { error: 'Error in smart action load hook: hook must return an array of fields' }
|
|
@@ -104,6 +110,8 @@ module ForestLiana
|
|
|
104
110
|
if !action
|
|
105
111
|
render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
|
|
106
112
|
else
|
|
113
|
+
ensure_single_record_selection(action, load_request)
|
|
114
|
+
|
|
107
115
|
# Get the smart action hook load context
|
|
108
116
|
context = get_smart_action_load_ctx(action.fields)
|
|
109
117
|
|
|
@@ -132,6 +140,8 @@ module ForestLiana
|
|
|
132
140
|
return render status: 500, json: {error: 'Error in smart action change hook: fields params must be an array'}
|
|
133
141
|
end
|
|
134
142
|
|
|
143
|
+
ensure_single_record_selection(action, change_request)
|
|
144
|
+
|
|
135
145
|
# Get the smart action hook change context
|
|
136
146
|
context = get_smart_action_change_ctx(change_request[:fields], change_request[:changed_field])
|
|
137
147
|
|
|
@@ -35,6 +35,7 @@ module ForestLiana
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def is_smart_action_authorized?(user, collection, parameters, endpoint, http_method)
|
|
38
|
+
ensure_single_record_selection(collection, parameters, endpoint, http_method)
|
|
38
39
|
return true unless has_permission_system?
|
|
39
40
|
|
|
40
41
|
user_data = get_user_data(user['id'])
|
|
@@ -348,6 +349,24 @@ module ForestLiana
|
|
|
348
349
|
actions
|
|
349
350
|
end
|
|
350
351
|
|
|
352
|
+
def ensure_single_record_selection(collection, parameters, endpoint, http_method)
|
|
353
|
+
schema_action = find_action_from_endpoint(ForestLiana.name_for(collection), endpoint, http_method)
|
|
354
|
+
return unless schema_action&.type == 'single'
|
|
355
|
+
|
|
356
|
+
selections = [parameters[:data][:attributes], signed_approval_attributes(parameters)].compact
|
|
357
|
+
return if selections.all? { |attributes| SingleRecordSelection.allowed?(schema_action, attributes) }
|
|
358
|
+
|
|
359
|
+
raise ForestLiana::Errors::HTTP422Error.new(SingleRecordSelection::MESSAGE)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def signed_approval_attributes(parameters)
|
|
363
|
+
return unless parameters[:data][:attributes][:signed_approval_request]
|
|
364
|
+
|
|
365
|
+
RequestPermission.decodeSignedApprovalRequest(parameters)[:data][:attributes]
|
|
366
|
+
rescue JWT::DecodeError
|
|
367
|
+
nil
|
|
368
|
+
end
|
|
369
|
+
|
|
351
370
|
def find_action_from_endpoint(collection_name, endpoint, http_method)
|
|
352
371
|
collection = ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
|
|
353
372
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module ForestLiana
|
|
2
|
+
module SingleRecordSelection
|
|
3
|
+
MESSAGE = 'This action can only be triggered on one record at a time.'
|
|
4
|
+
|
|
5
|
+
def self.allowed?(action, attributes)
|
|
6
|
+
return true unless action&.type == 'single'
|
|
7
|
+
|
|
8
|
+
!attributes[:all_records] && Array(attributes[:ids]).length == 1
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/forest_liana/version.rb
CHANGED
|
@@ -217,6 +217,47 @@ describe 'Requesting Actions routes', :type => :request do
|
|
|
217
217
|
end
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
+
describe 'on a single-record action' do
|
|
221
|
+
let(:single_record_error) { 'This action can only be triggered on one record at a time.' }
|
|
222
|
+
|
|
223
|
+
before do
|
|
224
|
+
action = island.actions.find { |action| action.name == 'my_action' }
|
|
225
|
+
allow(action).to receive(:type).and_return('single')
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def hook_params(attributes)
|
|
229
|
+
JSON.dump(data: { attributes: { collection_name: 'Island' }.merge(attributes) })
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
[
|
|
233
|
+
['two ids', { ids: [1, 2] }],
|
|
234
|
+
['no id', { ids: [] }],
|
|
235
|
+
['a select-all', { ids: [1], all_records: true }],
|
|
236
|
+
].each do |description, selection|
|
|
237
|
+
it "should refuse to load the form given #{description}" do
|
|
238
|
+
post '/forest/actions/my_action/hooks/load', params: hook_params(selection), headers: headers
|
|
239
|
+
|
|
240
|
+
expect(response.status).to eq(422)
|
|
241
|
+
expect(response.body).to include(single_record_error)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'should refuse a change given two ids' do
|
|
246
|
+
post '/forest/actions/my_action/hooks/change',
|
|
247
|
+
params: hook_params(ids: [1, 2], fields: [{ field: 'foo', hook: 'on_foo_changed' }], changed_field: 'foo'),
|
|
248
|
+
headers: headers
|
|
249
|
+
|
|
250
|
+
expect(response.status).to eq(422)
|
|
251
|
+
expect(response.body).to include(single_record_error)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'should load the form on exactly one id' do
|
|
255
|
+
post '/forest/actions/my_action/hooks/load', params: hook_params(ids: [1]), headers: headers
|
|
256
|
+
|
|
257
|
+
expect(response.status).to eq(200)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
220
261
|
describe 'call /change' do
|
|
221
262
|
it 'should respond 200' do
|
|
222
263
|
action = island.actions.select { |action| action.name == 'my_action' }.first
|
|
@@ -428,6 +428,80 @@ module ForestLiana
|
|
|
428
428
|
expect {dummy_class.is_smart_action_authorized?(user, String, parameters, '/forest/actions/my_action', 'POST')}
|
|
429
429
|
.to raise_error(ForestLiana::Ability::Exceptions::UnknownCollection, 'The collection String doesn\'t exist')
|
|
430
430
|
end
|
|
431
|
+
|
|
432
|
+
it 'should run a bulk action on several ids' do
|
|
433
|
+
parameters[:data][:attributes][:ids] = ['1', '2']
|
|
434
|
+
|
|
435
|
+
expect(dummy_class.is_smart_action_authorized?(user, Island, parameters, '/forest/actions/my_action', 'POST')).to equal true
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
describe 'with a single-record action' do
|
|
439
|
+
let(:single_record_error) { 'This action can only be triggered on one record at a time.' }
|
|
440
|
+
|
|
441
|
+
before do
|
|
442
|
+
island_action = ForestLiana.apimap.find { |collection| collection.name.to_s == 'Island' }
|
|
443
|
+
.actions.find { |action| action.name == 'my_action' }
|
|
444
|
+
allow(island_action).to receive(:type).and_return('single')
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it 'should return true on exactly one id' do
|
|
448
|
+
expect(dummy_class.is_smart_action_authorized?(user, Island, parameters, '/forest/actions/my_action', 'POST')).to equal true
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
[
|
|
452
|
+
['two ids', { ids: ['1', '2'] }],
|
|
453
|
+
['no id', { ids: [] }],
|
|
454
|
+
['a select-all', { ids: ['1'], all_records: true }],
|
|
455
|
+
].each do |description, selection|
|
|
456
|
+
it "should refuse #{description} before checking permissions" do
|
|
457
|
+
parameters[:data][:attributes].merge!(selection)
|
|
458
|
+
expect(SmartActionChecker).not_to receive(:new)
|
|
459
|
+
|
|
460
|
+
expect { dummy_class.is_smart_action_authorized?(user, Island, parameters, '/forest/actions/my_action', 'POST') }
|
|
461
|
+
.to raise_error(ForestLiana::Errors::HTTP422Error, single_record_error)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def sign(ids)
|
|
466
|
+
JWT.encode(
|
|
467
|
+
{ data: { attributes: parameters[:data][:attributes].to_h.merge(ids: ids, requester_id: 2) } },
|
|
468
|
+
ForestLiana.env_secret,
|
|
469
|
+
'HS256'
|
|
470
|
+
)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
it 'should refuse to approve a request naming several ids' do
|
|
474
|
+
parameters[:data][:attributes][:signed_approval_request] = sign(['1', '2'])
|
|
475
|
+
expect(SmartActionChecker).not_to receive(:new)
|
|
476
|
+
|
|
477
|
+
expect { dummy_class.is_smart_action_authorized?(user, Island, parameters, '/forest/actions/my_action', 'POST') }
|
|
478
|
+
.to raise_error(ForestLiana::Errors::HTTP422Error, single_record_error)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it 'should refuse several ids sent beside a signed request naming one' do
|
|
482
|
+
parameters[:data][:attributes][:ids] = ['1', '2']
|
|
483
|
+
parameters[:data][:attributes][:signed_approval_request] = sign(['1'])
|
|
484
|
+
expect(SmartActionChecker).not_to receive(:new)
|
|
485
|
+
|
|
486
|
+
expect { dummy_class.is_smart_action_authorized?(user, Island, parameters, '/forest/actions/my_action', 'POST') }
|
|
487
|
+
.to raise_error(ForestLiana::Errors::HTTP422Error, single_record_error)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
it 'should leave an undecodable signed request to the permission check' do
|
|
491
|
+
parameters[:data][:attributes][:signed_approval_request] = 'not-a-jwt'
|
|
492
|
+
expect(Permission::SmartActionChecker).to receive(:new).and_return(instance_double(Permission::SmartActionChecker, can_execute?: true))
|
|
493
|
+
|
|
494
|
+
expect(dummy_class.is_smart_action_authorized?(user, Island, parameters, '/forest/actions/my_action', 'POST')).to equal true
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
it 'should refuse several ids without a permission system' do
|
|
498
|
+
Rails.cache.write('forest.has_permission', false)
|
|
499
|
+
parameters[:data][:attributes][:ids] = ['1', '2']
|
|
500
|
+
|
|
501
|
+
expect { dummy_class.is_smart_action_authorized?(user, Island, parameters, '/forest/actions/my_action', 'POST') }
|
|
502
|
+
.to raise_error(ForestLiana::Errors::HTTP422Error, single_record_error)
|
|
503
|
+
end
|
|
504
|
+
end
|
|
431
505
|
end
|
|
432
506
|
|
|
433
507
|
describe 'when the server doesn\'t return an success response' do
|
|
@@ -751,6 +751,7 @@ module ForestLiana
|
|
|
751
751
|
|
|
752
752
|
before do
|
|
753
753
|
Rails.cache.write('forest.has_permission', false)
|
|
754
|
+
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return({ 'scopes' => {}, 'team' => { 'id' => '1', 'name' => 'Operations' } })
|
|
754
755
|
allow(ForestLiana::ScopeManager).to receive(:append_scope).and_call_original
|
|
755
756
|
allow(ForestLiana::ScopeManager).to receive(:get_scope).and_return(nil)
|
|
756
757
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_liana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.22.
|
|
4
|
+
version: 9.22.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -332,6 +332,7 @@ files:
|
|
|
332
332
|
- app/services/forest_liana/schema_utils.rb
|
|
333
333
|
- app/services/forest_liana/scope_manager.rb
|
|
334
334
|
- app/services/forest_liana/search_query_builder.rb
|
|
335
|
+
- app/services/forest_liana/single_record_selection.rb
|
|
335
336
|
- app/services/forest_liana/smart_action_field_validator.rb
|
|
336
337
|
- app/services/forest_liana/smart_action_form_parser.rb
|
|
337
338
|
- app/services/forest_liana/smart_field_dependencies.rb
|