forest_liana 6.0.0.pre.beta.2 → 6.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +105 -0
  3. data/app/controllers/forest_liana/authentication_controller.rb +5 -5
  4. data/app/controllers/forest_liana/resources_controller.rb +14 -17
  5. data/app/controllers/forest_liana/smart_actions_controller.rb +10 -5
  6. data/app/helpers/forest_liana/is_same_data_structure_helper.rb +44 -0
  7. data/app/helpers/forest_liana/widgets_helper.rb +59 -0
  8. data/app/models/forest_liana/model/action.rb +2 -1
  9. data/app/serializers/forest_liana/stripe_invoice_serializer.rb +5 -5
  10. data/app/services/forest_liana/apimap_sorter.rb +1 -0
  11. data/app/services/forest_liana/authentication.rb +0 -2
  12. data/app/services/forest_liana/authorization_getter.rb +23 -21
  13. data/app/services/forest_liana/oidc_client_manager.rb +9 -5
  14. data/app/services/forest_liana/permissions_checker.rb +117 -56
  15. data/app/services/forest_liana/permissions_formatter.rb +52 -0
  16. data/app/services/forest_liana/permissions_getter.rb +52 -17
  17. data/app/services/forest_liana/resource_creator.rb +1 -1
  18. data/app/services/forest_liana/resource_updater.rb +3 -3
  19. data/app/services/forest_liana/resources_getter.rb +3 -3
  20. data/app/services/forest_liana/schema_utils.rb +8 -3
  21. data/app/services/forest_liana/scope_validator.rb +8 -7
  22. data/app/services/forest_liana/stripe_invoice_getter.rb +1 -1
  23. data/app/services/forest_liana/stripe_invoices_getter.rb +1 -1
  24. data/app/services/forest_liana/stripe_source_getter.rb +1 -1
  25. data/app/services/forest_liana/stripe_sources_getter.rb +1 -1
  26. data/app/services/forest_liana/utils/beta_schema_utils.rb +13 -0
  27. data/config/initializers/error-messages.rb +3 -0
  28. data/config/initializers/errors.rb +21 -2
  29. data/config/routes.rb +2 -4
  30. data/lib/forest_liana.rb +1 -0
  31. data/lib/forest_liana/bootstrapper.rb +31 -5
  32. data/lib/forest_liana/schema_file_updater.rb +1 -0
  33. data/lib/forest_liana/version.rb +1 -1
  34. data/lib/generators/forest_liana/install_generator.rb +13 -5
  35. data/spec/dummy/app/assets/config/manifest.js +1 -0
  36. data/spec/dummy/config/application.rb +1 -1
  37. data/spec/dummy/db/migrate/20190226172951_create_user.rb +1 -1
  38. data/spec/dummy/db/migrate/20190226173051_create_isle.rb +1 -1
  39. data/spec/dummy/db/migrate/20190226174951_create_tree.rb +1 -1
  40. data/spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb +1 -1
  41. data/spec/dummy/db/migrate/20190716135241_add_type_to_user.rb +1 -1
  42. data/spec/dummy/db/schema.rb +18 -20
  43. data/spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb +87 -0
  44. data/spec/requests/actions_controller_spec.rb +222 -0
  45. data/spec/requests/authentications_spec.rb +12 -13
  46. data/spec/requests/resources_spec.rb +4 -4
  47. data/spec/services/forest_liana/apimap_sorter_spec.rb +6 -4
  48. data/spec/services/forest_liana/permissions_checker_acl_disabled_spec.rb +711 -0
  49. data/spec/services/forest_liana/permissions_checker_acl_enabled_spec.rb +831 -0
  50. data/spec/services/forest_liana/permissions_formatter_spec.rb +222 -0
  51. data/spec/services/forest_liana/permissions_getter_spec.rb +83 -0
  52. data/spec/spec_helper.rb +3 -0
  53. data/test/dummy/app/assets/config/manifest.js +1 -0
  54. data/test/dummy/config/application.rb +1 -1
  55. data/test/dummy/db/migrate/20150608130516_create_date_field.rb +1 -1
  56. data/test/dummy/db/migrate/20150608131430_create_integer_field.rb +1 -1
  57. data/test/dummy/db/migrate/20150608131603_create_decimal_field.rb +1 -1
  58. data/test/dummy/db/migrate/20150608131610_create_float_field.rb +1 -1
  59. data/test/dummy/db/migrate/20150608132159_create_boolean_field.rb +1 -1
  60. data/test/dummy/db/migrate/20150608132621_create_string_field.rb +1 -1
  61. data/test/dummy/db/migrate/20150608133038_create_belongs_to_field.rb +1 -1
  62. data/test/dummy/db/migrate/20150608133044_create_has_one_field.rb +1 -1
  63. data/test/dummy/db/migrate/20150608150016_create_has_many_field.rb +1 -1
  64. data/test/dummy/db/migrate/20150609114636_create_belongs_to_class_name_field.rb +1 -1
  65. data/test/dummy/db/migrate/20150612112520_create_has_and_belongs_to_many_field.rb +1 -1
  66. data/test/dummy/db/migrate/20150616150629_create_polymorphic_field.rb +1 -1
  67. data/test/dummy/db/migrate/20150623115554_create_has_many_class_name_field.rb +1 -1
  68. data/test/dummy/db/migrate/20150814081918_create_has_many_through_field.rb +1 -1
  69. data/test/dummy/db/migrate/20160627172810_create_owner.rb +1 -1
  70. data/test/dummy/db/migrate/20160627172951_create_tree.rb +1 -1
  71. data/test/dummy/db/migrate/20160628173505_add_timestamps.rb +1 -1
  72. data/test/dummy/db/migrate/20170614141921_create_serialize_field.rb +1 -1
  73. data/test/dummy/db/migrate/20181111162121_create_references_table.rb +1 -1
  74. data/test/routing/route_test.rb +0 -12
  75. data/test/services/forest_liana/resources_getter_test.rb +1 -1
  76. metadata +132 -147
  77. data/app/controllers/forest_liana/sessions_controller.rb +0 -95
  78. data/app/serializers/forest_liana/session_serializer.rb +0 -33
  79. data/app/services/forest_liana/login_handler.rb +0 -99
  80. data/app/services/forest_liana/two_factor_registration_confirmer.rb +0 -36
  81. data/app/services/forest_liana/user_secret_creator.rb +0 -26
  82. data/spec/requests/sessions_spec.rb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2f88ccbd15c18a78b06d08ad2a17dfa2075d2b586728033cbe03564c95dd92e
4
- data.tar.gz: e78f4892dc9ea07b8dbabb67d3183920f8ef41b6cb7ebe1edd8cb3d0c3b7ee2b
3
+ metadata.gz: 6a5fa2377990f8fa7bbbf8267b3250a6b5f29f44e1b9f37d82f152281851875e
4
+ data.tar.gz: 9115346647db03749473cf9b7ed6b9fa9c7afa0a678551db264cae781533311c
5
5
  SHA512:
6
- metadata.gz: 2a1dfc618f2723448b2fea3b39a48be64fad619c87a3b7d9257e3be8292f1ae86632c38563a7c9d938fd9699a0d78bac50e521d390bb40077ab824996b987ed2
7
- data.tar.gz: d1a6401e8e236b718f488e1a1b5641fb953a912c719a5ea1010f7f307c99e7b96fd9f153fc483ebd7dbef5861a5daf9c12e2bdc03f846b62850662ee1837209a
6
+ metadata.gz: 14a06f171447f52515131ecfbfe308caacea813dad47d67eee45af086b134ac73677580a0e1f5a65bbbfdde1d078410d8cf3e7adb48ed588aab5049ef4fa3e50
7
+ data.tar.gz: 93469f1d98a053c685f9fde26d106907d4f89e3244d7f46abc99a2a0265b4bf22f63f5b652736658880968ac9d48ef4f5aae0a90c80b9d5ce2c8c03677ec6153
@@ -1,7 +1,112 @@
1
1
  module ForestLiana
2
2
  class ActionsController < ForestLiana::BaseController
3
+
3
4
  def values
4
5
  render serializer: nil, json: {}, status: :ok
5
6
  end
7
+
8
+ def get_collection(collection_name)
9
+ ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
10
+ end
11
+
12
+ def get_action(collection_name)
13
+ collection = get_collection(collection_name)
14
+ begin
15
+ collection.actions.find {|action| ActiveSupport::Inflector.parameterize(action.name) == params[:action_name]}
16
+ rescue => error
17
+ FOREST_LOGGER.error "Smart Action get action retrieval error: #{error}"
18
+ nil
19
+ end
20
+ end
21
+
22
+ def get_record
23
+ model = ForestLiana::SchemaUtils.find_model_from_collection_name(params[:collectionName])
24
+ redord_getter = ForestLiana::ResourceGetter.new(model, {:id => params[:recordIds][0]})
25
+ redord_getter.perform
26
+ redord_getter.record
27
+ end
28
+
29
+ def get_smart_action_load_ctx(fields)
30
+ fields = fields.reduce({}) do |p, c|
31
+ ForestLiana::WidgetsHelper.set_field_widget(c)
32
+ p.update(c[:field] => c.merge!(value: nil))
33
+ end
34
+ {:record => get_record, :fields => fields}
35
+ end
36
+
37
+ def get_smart_action_change_ctx(fields)
38
+ fields = fields.reduce({}) do |p, c|
39
+ field = c.permit!.to_h.symbolize_keys
40
+ ForestLiana::WidgetsHelper.set_field_widget(field)
41
+ p.update(c[:field] => field)
42
+ end
43
+ {:record => get_record, :fields => fields}
44
+ end
45
+
46
+ def handle_result(result, formatted_fields, action)
47
+ if result.nil? || !result.is_a?(Hash)
48
+ return render status: 500, json: { error: 'Error in smart action load hook: hook must return an object' }
49
+ end
50
+ is_same_data_structure = ForestLiana::IsSameDataStructureHelper::Analyser.new(formatted_fields, result, 1)
51
+ unless is_same_data_structure.perform
52
+ return render status: 500, json: { error: 'Error in smart action hook: fields must be unchanged (no addition nor deletion allowed)' }
53
+ end
54
+
55
+ # Apply result on fields (transform the object back to an array), preserve order.
56
+ fields = action.fields.map do |field|
57
+ updated_field = result[field[:field]]
58
+
59
+ # Reset `value` when not present in `enums` (which means `enums` has changed).
60
+ if updated_field[:enums].is_a?(Array)
61
+ # `value` can be an array if the type of fields is `[x]`
62
+ if updated_field[:type].is_a?(Array) && updated_field[:value].is_a?(Array) && !(updated_field[:value] - updated_field[:enums]).empty?
63
+ updated_field[:value] = nil
64
+ end
65
+
66
+ # `value` can be any other value
67
+ if !updated_field[:type].is_a?(Array) && !updated_field[:enums].include?(updated_field[:value])
68
+ updated_field[:value] = nil
69
+ end
70
+ end
71
+
72
+ updated_field
73
+ end
74
+
75
+ render serializer: nil, json: { fields: fields}, status: :ok
76
+ end
77
+
78
+ def load
79
+ action = get_action(params[:collectionName])
80
+
81
+ if !action
82
+ render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
83
+ else
84
+ # Transform fields from array to an object to ease usage in hook, adds null value.
85
+ context = get_smart_action_load_ctx(action.fields)
86
+ formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
87
+
88
+ # Call the user-defined load hook.
89
+ result = action.hooks[:load].(context)
90
+
91
+ handle_result(result, formatted_fields, action)
92
+ end
93
+ end
94
+
95
+ def change
96
+ action = get_action(params[:collectionName])
97
+
98
+ if !action
99
+ render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
100
+ else
101
+ # Transform fields from array to an object to ease usage in hook.
102
+ context = get_smart_action_change_ctx(params[:fields])
103
+ formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
104
+
105
+ # Call the user-defined change hook.
106
+ result = action.hooks[:change][params[:changedField]].(context)
107
+
108
+ handle_result(result, formatted_fields, action)
109
+ end
110
+ end
6
111
  end
7
112
  end
@@ -46,7 +46,7 @@ module ForestLiana
46
46
  { 'renderingId' => rendering_id },
47
47
  )
48
48
 
49
- redirect_to(result['authorization_url'])
49
+ render json: { authorizationUrl: result['authorization_url']}, status: 200
50
50
  rescue => error
51
51
  render json: { errors: [{ status: 500, detail: error.message }] },
52
52
  status: :internal_server_error, serializer: nil
@@ -69,7 +69,7 @@ module ForestLiana
69
69
  httponly: true,
70
70
  secure: true,
71
71
  expires: ForestLiana::Token.expiration_in_days,
72
- samesite: 'none',
72
+ same_site: :None,
73
73
  path: '/'
74
74
  },
75
75
  )
@@ -86,8 +86,8 @@ module ForestLiana
86
86
  render json: response_body, status: 200
87
87
 
88
88
  rescue => error
89
- render json: { errors: [{ status: 500, detail: error.message }] },
90
- status: :internal_server_error, serializer: nil
89
+ render json: { errors: [{ status: error.try(:error_code) || 500, detail: error.message }] },
90
+ status: error.status || :internal_server_error, serializer: nil
91
91
  end
92
92
  end
93
93
 
@@ -104,7 +104,7 @@ module ForestLiana
104
104
  httponly: true,
105
105
  secure: true,
106
106
  expires: Time.at(0),
107
- samesite: 'none',
107
+ same_site: :None,
108
108
  path: '/'
109
109
  },
110
110
  )
@@ -16,18 +16,15 @@ module ForestLiana
16
16
  def index
17
17
  begin
18
18
  if request.format == 'csv'
19
- checker = ForestLiana::PermissionsChecker.new(@resource, 'export', @rendering_id)
20
- return head :forbidden unless checker.is_authorized?
21
- elsif params.has_key?(:searchToEdit)
22
- checker = ForestLiana::PermissionsChecker.new(@resource, 'searchToEdit', @rendering_id)
19
+ checker = ForestLiana::PermissionsChecker.new(@resource, 'exportEnabled', @rendering_id, user_id: forest_user['id'])
23
20
  return head :forbidden unless checker.is_authorized?
24
21
  else
25
22
  checker = ForestLiana::PermissionsChecker.new(
26
23
  @resource,
27
- 'list',
24
+ 'browseEnabled',
28
25
  @rendering_id,
29
- nil,
30
- get_collection_list_permission_info(forest_user, request)
26
+ user_id: forest_user['id'],
27
+ collection_list_parameters: get_collection_list_permission_info(forest_user, request)
31
28
  )
32
29
  return head :forbidden unless checker.is_authorized?
33
30
  end
@@ -59,10 +56,10 @@ module ForestLiana
59
56
  begin
60
57
  checker = ForestLiana::PermissionsChecker.new(
61
58
  @resource,
62
- 'list',
59
+ 'browseEnabled',
63
60
  @rendering_id,
64
- nil,
65
- get_collection_list_permission_info(forest_user, request)
61
+ user_id: forest_user['id'],
62
+ collection_list_parameters: get_collection_list_permission_info(forest_user, request)
66
63
  )
67
64
  return head :forbidden unless checker.is_authorized?
68
65
 
@@ -89,7 +86,7 @@ module ForestLiana
89
86
 
90
87
  def show
91
88
  begin
92
- checker = ForestLiana::PermissionsChecker.new(@resource, 'show', @rendering_id)
89
+ checker = ForestLiana::PermissionsChecker.new(@resource, 'readEnabled', @rendering_id, user_id: forest_user['id'])
93
90
  return head :forbidden unless checker.is_authorized?
94
91
 
95
92
  getter = ForestLiana::ResourceGetter.new(@resource, params)
@@ -104,7 +101,7 @@ module ForestLiana
104
101
 
105
102
  def create
106
103
  begin
107
- checker = ForestLiana::PermissionsChecker.new(@resource, 'create', @rendering_id)
104
+ checker = ForestLiana::PermissionsChecker.new(@resource, 'addEnabled', @rendering_id, user_id: forest_user['id'])
108
105
  return head :forbidden unless checker.is_authorized?
109
106
 
110
107
  creator = ForestLiana::ResourceCreator.new(@resource, params)
@@ -127,7 +124,7 @@ module ForestLiana
127
124
 
128
125
  def update
129
126
  begin
130
- checker = ForestLiana::PermissionsChecker.new(@resource, 'update', @rendering_id)
127
+ checker = ForestLiana::PermissionsChecker.new(@resource, 'editEnabled', @rendering_id, user_id: forest_user['id'])
131
128
  return head :forbidden unless checker.is_authorized?
132
129
 
133
130
  updater = ForestLiana::ResourceUpdater.new(@resource, params)
@@ -149,7 +146,7 @@ module ForestLiana
149
146
  end
150
147
 
151
148
  def destroy
152
- checker = ForestLiana::PermissionsChecker.new(@resource, 'delete', @rendering_id)
149
+ checker = ForestLiana::PermissionsChecker.new(@resource, 'deleteEnabled', @rendering_id, user_id: forest_user['id'])
153
150
  return head :forbidden unless checker.is_authorized?
154
151
 
155
152
  @resource.destroy(params[:id]) if @resource.exists?(params[:id])
@@ -161,7 +158,7 @@ module ForestLiana
161
158
  end
162
159
 
163
160
  def destroy_bulk
164
- checker = ForestLiana::PermissionsChecker.new(@resource, 'delete', @rendering_id)
161
+ checker = ForestLiana::PermissionsChecker.new(@resource, 'deleteEnabled', @rendering_id, user_id: forest_user['id'])
165
162
  return head :forbidden unless checker.is_authorized?
166
163
 
167
164
  ids = ForestLiana::ResourcesGetter.get_ids_from_request(params)
@@ -245,8 +242,8 @@ module ForestLiana
245
242
  @collection ||= ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
246
243
  end
247
244
 
248
- # NOTICE: Return a formatted object containing the request condition filters and
249
- # the user id used by the scope validator class to validate if scope is
245
+ # NOTICE: Return a formatted object containing the request condition filters and
246
+ # the user id used by the scope validator class to validate if scope is
250
247
  # in request
251
248
  def get_collection_list_permission_info(user, collection_list_request)
252
249
  {
@@ -19,14 +19,15 @@ module ForestLiana
19
19
 
20
20
  def check_permission_for_smart_route
21
21
  begin
22
-
22
+
23
23
  smart_action_request = get_smart_action_request
24
24
  if !smart_action_request.nil? && smart_action_request.has_key?(:smart_action_id)
25
25
  checker = ForestLiana::PermissionsChecker.new(
26
26
  find_resource(smart_action_request[:collection_name]),
27
27
  'actions',
28
28
  @rendering_id,
29
- get_smart_action_permission_info(forest_user, smart_action_request)
29
+ user_id: forest_user['id'],
30
+ smart_action_request_info: get_smart_action_request_info
30
31
  )
31
32
  return head :forbidden unless checker.is_authorized?
32
33
  else
@@ -54,10 +55,14 @@ module ForestLiana
54
55
  end
55
56
  end
56
57
 
57
- def get_smart_action_permission_info(user, smart_action_request)
58
+ # smart action permissions are retrieved from the action's endpoint and http_method
59
+ def get_smart_action_request_info
60
+ endpoint = request.fullpath
61
+ # Trim starting '/'
62
+ endpoint[0] = '' if endpoint[0] == '/'
58
63
  {
59
- user_id: user['id'],
60
- action_id: smart_action_request[:smart_action_id],
64
+ endpoint: endpoint,
65
+ http_method: request.request_method
61
66
  }
62
67
  end
63
68
  end
@@ -0,0 +1,44 @@
1
+ require 'set'
2
+
3
+ module ForestLiana
4
+ module IsSameDataStructureHelper
5
+ class Analyser
6
+ def initialize(object, other, deep = 0)
7
+ @object = object
8
+ @other = other
9
+ @deep = deep
10
+ end
11
+
12
+ def are_objects(object, other)
13
+ object && other && object.is_a?(Hash) && other.is_a?(Hash)
14
+ end
15
+
16
+ def check_keys(object, other, step = 0)
17
+ unless are_objects(object, other)
18
+ return false
19
+ end
20
+
21
+ object_keys = object.keys
22
+ other_keys = other.keys
23
+
24
+ if object_keys.length != other_keys.length
25
+ return false
26
+ end
27
+
28
+ object_keys_set = object_keys.to_set
29
+ other_keys.each { |key|
30
+ if !object_keys_set.member?(key) || (step + 1 <= @deep && !check_keys(object[key], other[key], step + 1))
31
+ return false
32
+ end
33
+ }
34
+
35
+ return true
36
+ end
37
+
38
+ def perform
39
+ check_keys(@object, @other)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,59 @@
1
+ require 'set'
2
+
3
+ module ForestLiana
4
+ module WidgetsHelper
5
+
6
+ @widget_edit_list = [
7
+ 'address editor',
8
+ 'belongsto typeahead',
9
+ 'belongsto dropdown',
10
+ 'boolean editor',
11
+ 'checkboxes',
12
+ 'color editor',
13
+ 'date editor',
14
+ 'dropdown',
15
+ 'embedded document editor',
16
+ 'file picker',
17
+ 'json code editor',
18
+ 'input array',
19
+ 'multiple select',
20
+ 'number input',
21
+ 'point editor',
22
+ 'price editor',
23
+ 'radio button',
24
+ 'rich text',
25
+ 'text area editor',
26
+ 'text editor',
27
+ 'time input',
28
+ ]
29
+
30
+ @v1_to_v2_edit_widgets_mapping = {
31
+ address: 'address editor',
32
+ 'belongsto select': 'belongsto dropdown',
33
+ 'color picker': 'color editor',
34
+ 'date picker': 'date editor',
35
+ price: 'price editor',
36
+ 'JSON editor': 'json code editor',
37
+ 'rich text editor': 'rich text',
38
+ 'text area': 'text area editor',
39
+ 'text input': 'text editor',
40
+ }
41
+
42
+ def self.set_field_widget(field)
43
+
44
+ if field[:widget]
45
+ if @v1_to_v2_edit_widgets_mapping[field[:widget].to_sym]
46
+ field[:widgetEdit] = {name: @v1_to_v2_edit_widgets_mapping[field[:widget].to_sym], parameters: {}}
47
+ elsif @widget_edit_list.include?(field[:widget])
48
+ field[:widgetEdit] = {name: field[:widget], parameters: {}}
49
+ end
50
+ end
51
+
52
+ if !field.key?(:widgetEdit)
53
+ field[:widgetEdit] = nil
54
+ end
55
+
56
+ field.delete(:widget)
57
+ end
58
+ end
59
+ end
@@ -5,7 +5,7 @@ class ForestLiana::Model::Action
5
5
  extend ActiveModel::Naming
6
6
 
7
7
  attr_accessor :id, :name, :base_url, :endpoint, :http_method, :fields, :redirect,
8
- :type, :download
8
+ :type, :download, :hooks
9
9
 
10
10
  def initialize(attributes = {})
11
11
  if attributes.key?(:global)
@@ -66,6 +66,7 @@ class ForestLiana::Model::Action
66
66
  @base_url ||= nil
67
67
  @type ||= "bulk"
68
68
  @download ||= false
69
+ @hooks = !@hooks.nil? ? @hooks.symbolize_keys : nil
69
70
  end
70
71
 
71
72
  def persisted?
@@ -3,20 +3,20 @@ module ForestLiana
3
3
  include JSONAPI::Serializer
4
4
 
5
5
  attribute :amount_due
6
+ attribute :amount_paid
7
+ attribute :amount_remaining
8
+ attribute :application_fee_amount
6
9
  attribute :attempt_count
7
10
  attribute :attempted
8
- attribute :closed
9
11
  attribute :currency
10
- attribute :date
11
- attribute :forgiven
12
+ attribute :due_date
12
13
  attribute :paid
13
14
  attribute :period_end
14
15
  attribute :period_start
16
+ attribute :status
15
17
  attribute :subtotal
16
18
  attribute :total
17
- attribute :application_fee
18
19
  attribute :tax
19
- attribute :tax_percent
20
20
 
21
21
  has_one :customer
22
22
 
@@ -39,6 +39,7 @@ module ForestLiana
39
39
  'redirect',
40
40
  'download',
41
41
  'fields',
42
+ 'hooks',
42
43
  ]
43
44
  KEYS_ACTION_FIELD = [
44
45
  'field',