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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9584149ffb7d20be9b8f4dacd36f5bf1d95c5276b1b34f35d86e750e0f696f2
4
- data.tar.gz: 2d24e798d72ec62aabec3ebe9fdb070f857c8e511b02d4bbe8e8ce1880e03032
3
+ metadata.gz: fcbf97d59c4b454c53e5f068f969e8e503e2a722c1a011e2d03b7f9e73a0c152
4
+ data.tar.gz: cd6608d3c224d6daaedd240928a34cec77d49c35bfdf52fdb0d5b5187a23cf02
5
5
  SHA512:
6
- metadata.gz: 87d06d138209e8ca03019c971e0aa24e0c55bb8f71f03df225cf8de312f2fcb7dbac04cd398308c8a2768f7226b9c36e06ccdbed6d63225a5abe5a0bc26cc448
7
- data.tar.gz: 31a54daccf9d158680b28d5747f35a467943f94a779448f812f43dfe8f84ba9cd921297dc930a958d7b6bd1660f8d3becd15abb50480db9c516d8b7a21e98508
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
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.22.12"
2
+ VERSION = "9.22.13"
3
3
  end
@@ -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.12
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-13 00:00:00.000000000 Z
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