forest_admin_agent 1.12.11 → 1.12.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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/forest_admin_agent/auth/auth_manager.rb +9 -4
  3. data/lib/forest_admin_agent/builder/agent_factory.rb +20 -3
  4. data/lib/forest_admin_agent/http/Exceptions/business_error.rb +101 -0
  5. data/lib/forest_admin_agent/http/Exceptions/http_exception.rb +7 -3
  6. data/lib/forest_admin_agent/http/error_translator.rb +133 -0
  7. data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +27 -9
  8. data/lib/forest_admin_agent/routes/resources/native_query.rb +2 -1
  9. data/lib/forest_admin_agent/routes/security/authentication.rb +3 -4
  10. data/lib/forest_admin_agent/services/ip_whitelist.rb +7 -4
  11. data/lib/forest_admin_agent/services/permissions.rb +9 -3
  12. data/lib/forest_admin_agent/services/smart_action_checker.rb +27 -24
  13. data/lib/forest_admin_agent/services/sse_cache_invalidation.rb +7 -2
  14. data/lib/forest_admin_agent/utils/caller_parser.rb +6 -6
  15. data/lib/forest_admin_agent/utils/condition_tree_parser.rb +2 -2
  16. data/lib/forest_admin_agent/utils/query_string_parser.rb +4 -3
  17. data/lib/forest_admin_agent/utils/query_validator.rb +17 -11
  18. data/lib/forest_admin_agent/utils/schema/generator_action_field_widget.rb +5 -2
  19. data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
  20. data/lib/forest_admin_agent/version.rb +1 -1
  21. data/lib/forest_admin_agent.rb +1 -0
  22. metadata +3 -9
  23. data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +0 -12
  24. data/lib/forest_admin_agent/http/Exceptions/conflict_error.rb +0 -13
  25. data/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb +0 -13
  26. data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +0 -11
  27. data/lib/forest_admin_agent/http/Exceptions/require_approval.rb +0 -14
  28. data/lib/forest_admin_agent/http/Exceptions/unprocessable_error.rb +0 -13
  29. data/lib/forest_admin_agent/http/Exceptions/validation_error.rb +0 -13
  30. data/lib/forest_admin_agent/http/error_handling.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c483941707c8c57b9ae658ef3790d4b9292fe090d5fc15619970483b5c58f89
4
- data.tar.gz: 4f8c9bc23a9a86f8d7850bad610f72d8dc31d4b485365a3e8d292e32f176a226
3
+ metadata.gz: dcedc2aca2e7965829e92f88f166f74264dd820084052ae74d225beb3a166aa8
4
+ data.tar.gz: 8931db8225893bcb912b3a445d88f6323d58bac33caf166051b5753df84c3676
5
5
  SHA512:
6
- metadata.gz: 24a0f155ef3afc2350c3a27ee03e9ee6c02538d8685bcbe6cf90d77aeef2b192a139bc84b374679c5d4cdea0d9268a485492c4f6735eb965eda5fae9b4b768bb
7
- data.tar.gz: b7ce5b4f9a40c6cbd79a48b60d8e1dda0bc9a21e907741837e4d2feca019eb115dfbea8f9b6f3d671bd839e819badb635a840766f969de8d1080d0cef12486b2
6
+ metadata.gz: 6faf436c5fbf5a3facf3313644f02483b990f09eaf10bd99aba7a96a999c6d45b9c9b8b426eaa19152627a1cf42f33d87b00a495417a5382876d231f6ee8b15b
7
+ data.tar.gz: e9a7c9e2e5763539a1dea190ed0c3cd46a2b5fff17b0aa447660695ba58faa5d37bedc23cbfac48a7be8383ceeeb8abaa9a95df54e14ce9aefc6399931946f95
@@ -15,7 +15,7 @@ module ForestAdminAgent
15
15
 
16
16
  def verify_code_and_generate_token(params)
17
17
  unless params['state']
18
- raise ForestAdminAgent::Error,
18
+ raise ForestAdminAgent::Http::Exceptions::BadRequestError,
19
19
  ForestAdminAgent::Utils::ErrorMessages::INVALID_STATE_MISSING
20
20
  end
21
21
 
@@ -40,14 +40,19 @@ module ForestAdminAgent
40
40
  def get_rendering_id_from_state(state)
41
41
  state = JSON.parse(state.tr("'", '"').gsub('=>', ':'))
42
42
  unless state.key? 'renderingId'
43
- raise ForestAdminAgent::Error,
44
- ForestAdminAgent::Utils::ErrorMessages::INVALID_STATE_RENDERING_ID
43
+ raise ForestAdminAgent::Http::Exceptions::BadRequestError.new(
44
+ ForestAdminAgent::Utils::ErrorMessages::INVALID_STATE_RENDERING_ID,
45
+ details: { state: state }
46
+ )
45
47
  end
46
48
 
47
49
  begin
48
50
  Integer(state['renderingId'])
49
51
  rescue ArgumentError
50
- raise ForestAdminAgent::Error, ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID
52
+ raise ForestAdminAgent::Http::Exceptions::ValidationError.new(
53
+ ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID,
54
+ details: { renderingId: state['renderingId'] }
55
+ )
51
56
  end
52
57
 
53
58
  state['renderingId'].to_i
@@ -7,6 +7,7 @@ module ForestAdminAgent
7
7
  class AgentFactory
8
8
  include Singleton
9
9
  include ForestAdminAgent::Utils::Schema
10
+ include ForestAdminAgent::Http::Exceptions
10
11
  include ForestAdminDatasourceToolkit::Exceptions
11
12
 
12
13
  TTL_SCHEMA = 7200
@@ -46,6 +47,10 @@ module ForestAdminAgent
46
47
 
47
48
  def build
48
49
  @container.register(:datasource, @customizer.datasource(@logger))
50
+
51
+ # Reset route cache to ensure routes are computed with all customizations
52
+ ForestAdminAgent::Http::Router.reset_cached_routes!
53
+
49
54
  send_schema
50
55
  end
51
56
 
@@ -58,6 +63,11 @@ module ForestAdminAgent
58
63
  end
59
64
 
60
65
  @container.register(:datasource, @customizer.datasource(@logger), replace: true)
66
+
67
+ # Reset route cache before sending schema to ensure routes are recomputed with all customizations
68
+ ForestAdminAgent::Http::Router.reset_cached_routes!
69
+ @logger.log('Info', 'route cache cleared due to agent reload')
70
+
61
71
  send_schema
62
72
  end
63
73
 
@@ -73,7 +83,10 @@ module ForestAdminAgent
73
83
 
74
84
  if Facades::Container.cache(:is_production)
75
85
  unless schema_path && File.exist?(schema_path)
76
- raise ForestException, "Can't load #{schema_path}. Providing a schema is mandatory in production."
86
+ raise InternalServerError.new(
87
+ 'Schema file not found in production',
88
+ details: { schema_path: schema_path }
89
+ )
77
90
  end
78
91
 
79
92
  schema = JSON.parse(File.read(schema_path), symbolize_names: true)
@@ -143,8 +156,12 @@ module ForestAdminAgent
143
156
  response = client.post('/forest/apimaps/hashcheck', { schemaFileHash: hash }.to_json)
144
157
  body = JSON.parse(response.body)
145
158
  body['sendSchema']
146
- rescue JSON::ParserError
147
- raise ForestException, "Invalid JSON response from ForestAdmin server #{response.body}"
159
+ rescue JSON::ParserError => e
160
+ raise InternalServerError.new(
161
+ 'Invalid JSON response from ForestAdmin server',
162
+ details: { body: response.body },
163
+ cause: e
164
+ )
148
165
  rescue Faraday::Error => e
149
166
  client.handle_response_error(e)
150
167
  end
@@ -0,0 +1,101 @@
1
+ module ForestAdminAgent
2
+ module Http
3
+ module Exceptions
4
+ # Parent class for all business errors
5
+ # This is the base class that all specific error types inherit from
6
+ class BusinessError < StandardError
7
+ attr_reader :details, :cause
8
+
9
+ def initialize(message = nil, details: {}, cause: nil)
10
+ super(message)
11
+ @details = details || {}
12
+ @cause = cause
13
+ end
14
+
15
+ # Returns the name of the error class
16
+ def name
17
+ self.class.name.split('::').last
18
+ end
19
+ end
20
+
21
+ # ====================
22
+ # Specific error types
23
+ # ====================
24
+
25
+ class BadRequestError < BusinessError
26
+ def initialize(message = 'Bad request', details: {})
27
+ super
28
+ end
29
+ end
30
+
31
+ class ValidationError < BadRequestError
32
+ def initialize(message = 'Validation failed', details: {})
33
+ super
34
+ end
35
+ end
36
+
37
+ class UnauthorizedError < BusinessError
38
+ def initialize(message = 'Unauthorized', details: {})
39
+ super
40
+ end
41
+ end
42
+
43
+ class AuthenticationOpenIdClient < UnauthorizedError
44
+ def initialize(message = 'Authentication failed with OpenID Client', details: {})
45
+ super
46
+ end
47
+ end
48
+
49
+ class ForbiddenError < BusinessError
50
+ def initialize(message = 'Forbidden', details: {})
51
+ super
52
+ end
53
+ end
54
+
55
+ class NotFoundError < BusinessError
56
+ def initialize(message, details: {})
57
+ super
58
+ end
59
+ end
60
+
61
+ class ConflictError < BusinessError
62
+ def initialize(message = 'Conflict', details: {})
63
+ super
64
+ end
65
+ end
66
+
67
+ class UnprocessableError < BusinessError
68
+ def initialize(message = 'Unprocessable entity', details: {})
69
+ super
70
+ end
71
+ end
72
+
73
+ class TooManyRequestsError < BusinessError
74
+ attr_reader :retry_after
75
+
76
+ def initialize(message, retry_after, details: {})
77
+ super(message, details: details)
78
+ @retry_after = retry_after
79
+ end
80
+ end
81
+
82
+ class InternalServerError < BusinessError
83
+ def initialize(message = 'Internal server error', details: {}, cause: nil)
84
+ super
85
+ end
86
+ end
87
+
88
+ class BadGatewayError < BusinessError
89
+ def initialize(message = 'Bad gateway error', details: {}, cause: nil)
90
+ super
91
+ end
92
+ end
93
+
94
+ class ServiceUnavailableError < BusinessError
95
+ def initialize(message = 'Service unavailable error', details: {}, cause: nil)
96
+ super
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -1,14 +1,18 @@
1
+ require_relative 'business_error'
2
+
1
3
  module ForestAdminAgent
2
4
  module Http
3
5
  module Exceptions
4
6
  class HttpException < StandardError
5
- attr_reader :status, :message, :name
7
+ attr_reader :name, :status, :data, :custom_headers
6
8
 
7
- def initialize(status, message, name = nil)
9
+ def initialize(status, name, message, data = {}, custom_headers = {})
8
10
  super(message)
11
+
9
12
  @status = status
10
- @message = message
11
13
  @name = name
14
+ @data = data
15
+ @custom_headers = custom_headers
12
16
  end
13
17
  end
14
18
  end
@@ -0,0 +1,133 @@
1
+ require_relative 'Exceptions/business_error'
2
+ require_relative 'Exceptions/http_exception'
3
+
4
+ module ForestAdminAgent
5
+ module Http
6
+ class ErrorTranslator
7
+ # Translate any exception to its appropriate HTTP error representation
8
+ # @param error [Exception] The error to translate
9
+ # @return [HttpException] The translated error with HTTP-specific properties
10
+ def self.translate(error)
11
+ return error if error.is_a?(Exceptions::HttpException)
12
+
13
+ name = error.class.name.split('::').last
14
+
15
+ status = get_error_status(error)
16
+
17
+ message = get_error_message(error)
18
+
19
+ data = get_error_data(error)
20
+
21
+ custom_headers = get_custom_headers(error)
22
+
23
+ Exceptions::HttpException.new(
24
+ status,
25
+ name,
26
+ message,
27
+ data,
28
+ custom_headers
29
+ )
30
+ end
31
+
32
+ # Get the HTTP status code for an error
33
+ # @param error [Exception] The error to get status for
34
+ # @return [Integer] The HTTP status code
35
+ def self.get_error_status(error)
36
+ if defined?(ForestAdminDatasourceToolkit::Exceptions::ValidationError) &&
37
+ of_type?(error, ForestAdminDatasourceToolkit::Exceptions::ValidationError)
38
+ return 400
39
+ end
40
+
41
+ error.status if error.respond_to?(:status) && error.status
42
+
43
+ case error
44
+ when Exceptions::ValidationError, Exceptions::BadRequestError
45
+ 400
46
+ when Exceptions::UnauthorizedError
47
+ 401
48
+ when Exceptions::ForbiddenError
49
+ 403
50
+ when Exceptions::NotFoundError
51
+ 404
52
+ when Exceptions::ConflictError
53
+ 409
54
+ when Exceptions::UnprocessableError
55
+ 422
56
+ when Exceptions::TooManyRequestsError
57
+ 429
58
+ when Exceptions::InternalServerError
59
+ 500
60
+ when Exceptions::BadGatewayError
61
+ 502
62
+ when Exceptions::ServiceUnavailableError
63
+ 503
64
+ when Exceptions::BusinessError
65
+ # default BusinessError → 422 (Unprocessable Entity)
66
+ 422
67
+ else
68
+ # Unknown errors → 500 (Internal Server Error)
69
+ 500
70
+ end
71
+ end
72
+
73
+ # Get the error message
74
+ # @param error [Exception] The error to get message from
75
+ # @return [String] The error message to send to the client
76
+ def self.get_error_message(error)
77
+ # Try custom error message customizer first
78
+ if defined?(ForestAdminAgent::Facades) &&
79
+ (customizer = ForestAdminAgent::Facades::Container.cache(:customize_error_message))
80
+ custom_message = eval(customizer).call(error)
81
+ return custom_message if custom_message
82
+ end
83
+
84
+ is_known_error = error.is_a?(Exceptions::HttpException) ||
85
+ error.is_a?(Exceptions::BusinessError) ||
86
+ (defined?(ForestAdminDatasourceToolkit::Exceptions::ForestException) &&
87
+ error.is_a?(ForestAdminDatasourceToolkit::Exceptions::ForestException))
88
+
89
+ return error.message if is_known_error && error.message
90
+
91
+ 'Unexpected error'
92
+ end
93
+
94
+ # Get error data/metadata
95
+ # @param error [Exception] The error to get data from
96
+ # @return [Hash, nil] The error metadata or nil
97
+ def self.get_error_data(error)
98
+ return error.details if error.is_a?(Exceptions::BusinessError) &&
99
+ error.respond_to?(:details) &&
100
+ !error.details.empty?
101
+
102
+ nil
103
+ end
104
+
105
+ # Get custom headers for specific error types
106
+ # @param error [Exception] The error to get headers for
107
+ # @return [Proc, nil] A proc that generates custom headers
108
+ def self.get_custom_headers(error)
109
+ case error
110
+ when Exceptions::NotFoundError
111
+ { 'x-error-type' => 'object-not-found' }
112
+ when Exceptions::TooManyRequestsError
113
+ { 'Retry-After' => error.retry_after.to_s }
114
+ end
115
+ end
116
+
117
+ # Check if an error is of a specific type (by class name)
118
+ # @param error [Exception] The error to check
119
+ # @param error_class [Class] The error class to check against
120
+ # @return [Boolean] True if the error is of the specified type
121
+ def self.of_type?(error, error_class)
122
+ # Direct instance check
123
+ return true if error.is_a?(error_class)
124
+
125
+ # Check by class name (handles cross-package version mismatches)
126
+ error.class.name.split('::').last == error_class.name.split('::').last
127
+ end
128
+
129
+ private_class_method :get_error_status, :get_error_message, :get_error_data,
130
+ :get_custom_headers, :of_type?
131
+ end
132
+ end
133
+ end
@@ -3,6 +3,7 @@ require 'faraday'
3
3
  module ForestAdminAgent
4
4
  module Http
5
5
  class ForestAdminApiRequester
6
+ include ForestAdminAgent::Http::Exceptions
6
7
  include ForestAdminDatasourceToolkit::Exceptions
7
8
 
8
9
  def initialize
@@ -28,29 +29,46 @@ module ForestAdminAgent
28
29
  end
29
30
 
30
31
  def handle_response_error(error)
32
+ # Re-raise if it's already a BusinessError
33
+ raise error if error.is_a?(ForestAdminAgent::Http::Exceptions::BusinessError)
31
34
  raise error if error.is_a?(ForestException)
32
35
 
33
36
  if error.response[:message]&.include?('certificate')
34
- raise ForestException,
35
- 'ForestAdmin server TLS certificate cannot be verified. Please check that your system time is set properly.'
37
+ raise InternalServerError.new(
38
+ 'ForestAdmin server TLS certificate cannot be verified. Please check that your system time is set properly.',
39
+ details: { error: error.message },
40
+ cause: error
41
+ )
36
42
  end
37
43
 
38
44
  if error.response[:status].zero? || error.response[:status] == 502
39
- raise ForestException, 'Failed to reach ForestAdmin server. Are you online?'
45
+ raise BadGatewayError.new(
46
+ 'Failed to reach ForestAdmin server. Are you online?',
47
+ details: { status: error.response[:status] },
48
+ cause: error
49
+ )
40
50
  end
41
51
 
42
52
  if error.response[:status] == 404
43
- raise ForestException,
44
- 'ForestAdmin server failed to find the project related to the envSecret you configured. Can you check that you copied it properly in the Forest initialization?'
53
+ raise NotFoundError.new(
54
+ 'ForestAdmin server failed to find the project related to the envSecret you configured. Can you check that you copied it properly in the Forest initialization?',
55
+ details: { status: error.response[:status] }
56
+ )
45
57
  end
46
58
 
47
59
  if error.response[:status] == 503
48
- raise ForestException,
49
- 'Forest is in maintenance for a few minutes. We are upgrading your experience in the forest. We just need a few more minutes to get it right.'
60
+ raise ServiceUnavailableError.new(
61
+ 'Forest is in maintenance for a few minutes. We are upgrading your experience in the forest. We just need a few more minutes to get it right.',
62
+ details: { status: error.response[:status] },
63
+ cause: error
64
+ )
50
65
  end
51
66
 
52
- raise ForestException,
53
- 'An unexpected error occurred while contacting the ForestAdmin server. Please contact support@forestadmin.com for further investigations.'
67
+ raise InternalServerError.new(
68
+ 'An unexpected error occurred while contacting the ForestAdmin server. Please contact support@forestadmin.com for further investigations.',
69
+ details: { status: error.response[:status], message: error.message },
70
+ cause: error
71
+ )
54
72
  end
55
73
  end
56
74
  end
@@ -7,6 +7,7 @@ module ForestAdminAgent
7
7
  class NativeQuery < AbstractAuthenticatedRoute
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminAgent::Utils
10
+ include ForestAdminAgent::Http::Exceptions
10
11
  include ForestAdminDatasourceToolkit::Exceptions
11
12
  include ForestAdminDatasourceToolkit::Components::Charts
12
13
  include ForestAdminAgent::Routes::QueryHandler
@@ -61,7 +62,7 @@ module ForestAdminAgent
61
62
  end
62
63
 
63
64
  def raise_error(result, key_names)
64
- raise ForestException,
65
+ raise BadRequestError,
65
66
  "The result columns must be named #{key_names} instead of '#{result.keys.join("', '")}'"
66
67
  end
67
68
 
@@ -42,8 +42,7 @@ module ForestAdminAgent
42
42
 
43
43
  def handle_authentication_callback(args = {})
44
44
  if args[:params].key?(:error)
45
- raise AuthenticationOpenIdClient.new(args[:params][:error_description] || args[:params][:error],
46
- args[:params][:error])
45
+ raise AuthenticationOpenIdClient, args[:params][:error_description] || args[:params][:error]
47
46
  end
48
47
 
49
48
  if args.dig(:headers, 'action_dispatch.remote_ip')
@@ -80,14 +79,14 @@ module ForestAdminAgent
80
79
 
81
80
  def get_and_check_rendering_id(params)
82
81
  unless params['renderingId']
83
- raise ForestAdminAgent::Error,
82
+ raise BadRequestError,
84
83
  ForestAdminAgent::Utils::ErrorMessages::MISSING_RENDERING_ID
85
84
  end
86
85
 
87
86
  begin
88
87
  Integer(params['renderingId'])
89
88
  rescue ArgumentError
90
- raise ForestAdminAgent::Error, ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID
89
+ raise ValidationError, ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID
91
90
  end
92
91
 
93
92
  params['renderingId'].to_i
@@ -40,7 +40,7 @@ module ForestAdminAgent
40
40
  when RULE_MATCH_SUBNET
41
41
  ip_match_subnet?(ip, rule['range'])
42
42
  else
43
- raise 'Invalid rule type'
43
+ raise ForestAdminAgent::Http::Exceptions::InternalServerError, 'Invalid rule type'
44
44
  end
45
45
  end
46
46
 
@@ -93,7 +93,8 @@ module ForestAdminAgent
93
93
  status: response.status,
94
94
  response: response.body
95
95
  })
96
- raise ForestAdminAgent::Error, ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED
96
+ raise ForestAdminAgent::Http::Exceptions::InternalServerError,
97
+ ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED
97
98
  end
98
99
 
99
100
  begin
@@ -104,7 +105,8 @@ module ForestAdminAgent
104
105
  status: response.status,
105
106
  response: response.body
106
107
  })
107
- raise ForestAdminAgent::Error, ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED
108
+ raise ForestAdminAgent::Http::Exceptions::InternalServerError,
109
+ ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED
108
110
  end
109
111
 
110
112
  ip_whitelist_data = body['data']['attributes']
@@ -117,7 +119,8 @@ module ForestAdminAgent
117
119
  status: response&.status,
118
120
  response: response&.body
119
121
  })
120
- raise ForestAdminAgent::Error, ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED
122
+ raise ForestAdminAgent::Http::Exceptions::InternalServerError,
123
+ ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED
121
124
  end
122
125
  end
123
126
  end
@@ -263,7 +263,7 @@ module ForestAdminAgent
263
263
 
264
264
  action = actions.find { |a| a['endpoint'] == endpoint && a['httpMethod'].casecmp(http_method).zero? }
265
265
 
266
- raise ForestException, "The collection #{collection_name} does not have this smart action" if action.nil?
266
+ raise BadRequestError, "The collection #{collection_name} does not have this smart action" if action.nil?
267
267
 
268
268
  action
269
269
  end
@@ -277,7 +277,10 @@ module ForestAdminAgent
277
277
  "Available keys: #{collection.is_a?(Hash) ? collection.keys.join(", ") : "N/A (not a hash)"}. " \
278
278
  'This indicates an API contract violation or data corruption.'
279
279
  )
280
- raise ForestException, 'Invalid permission data structure received from Forest Admin API'
280
+ raise InternalServerError.new(
281
+ 'Invalid permission data structure received from Forest Admin API',
282
+ details: { received_keys: collection.is_a?(Hash) ? collection.keys : nil }
283
+ )
281
284
  end
282
285
 
283
286
  collection_data = collection[:collection]
@@ -288,7 +291,10 @@ module ForestAdminAgent
288
291
  "Invalid permissions data: :collection is not a hash (got #{collection_data.class}). " \
289
292
  'This indicates an API contract violation or data corruption.'
290
293
  )
291
- raise ForestException, 'Invalid permission data structure: :collection must be a hash'
294
+ raise InternalServerError.new(
295
+ 'Invalid permission data structure: :collection must be a hash',
296
+ details: { collection_data_class: collection_data.class }
297
+ )
292
298
  end
293
299
 
294
300
  # Use dig to safely extract roles, allowing for missing permissions
@@ -1,7 +1,26 @@
1
1
  module ForestAdminAgent
2
2
  module Services
3
+ include ForestAdminAgent::Http::Exceptions
4
+
5
+ class CustomActionTriggerForbiddenError < ForbiddenError
6
+ def initialize(message = 'Custom action trigger forbidden', details: {})
7
+ super
8
+ end
9
+ end
10
+
11
+ class InvalidActionConditionError < ConflictError
12
+ def initialize(message = 'Invalid action condition', details: {})
13
+ super
14
+ end
15
+ end
16
+
17
+ class CustomActionRequiresApprovalError < ForbiddenError
18
+ def initialize(message = 'Custom action requires approval', details: {})
19
+ super
20
+ end
21
+ end
22
+
3
23
  class SmartActionChecker
4
- include ForestAdminAgent::Http::Exceptions
5
24
  include ForestAdminAgent::Utils
6
25
  include ForestAdminDatasourceToolkit::Utils
7
26
  include ForestAdminDatasourceToolkit::Components::Query
@@ -9,12 +28,6 @@ module ForestAdminAgent
9
28
 
10
29
  attr_reader :parameters, :collection, :smart_action, :caller, :role_id, :filter, :attributes
11
30
 
12
- TRIGGER_FORBIDDEN_ERROR = 'CustomActionTriggerForbiddenError'.freeze
13
-
14
- REQUIRE_APPROVAL_ERROR = 'CustomActionRequiresApprovalError'.freeze
15
-
16
- INVALID_ACTION_CONDITION_ERROR = 'InvalidActionConditionError'.freeze
17
-
18
31
  def initialize(parameters, collection, smart_action, caller, role_id, filter)
19
32
  @parameters = parameters
20
33
  @collection = collection
@@ -43,7 +56,7 @@ module ForestAdminAgent
43
56
  return true
44
57
  end
45
58
 
46
- raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR)
59
+ raise CustomActionTriggerForbiddenError, 'You don\'t have the permission to trigger this action.'
47
60
  end
48
61
 
49
62
  def can_trigger?
@@ -53,17 +66,16 @@ module ForestAdminAgent
53
66
  end
54
67
  elsif smart_action[:approvalRequired].include?(role_id) && smart_action[:triggerEnabled].include?(role_id)
55
68
  if condition_by_role_id(smart_action[:approvalRequiredConditions]).nil? || match_conditions(:approvalRequiredConditions)
56
- raise RequireApproval.new(
69
+ raise CustomActionRequiresApprovalError.new(
57
70
  'This action requires to be approved.',
58
- REQUIRE_APPROVAL_ERROR,
59
- smart_action[:userApprovalEnabled]
71
+ details: { user_approval_enabled: smart_action[:userApprovalEnabled] }
60
72
  )
61
73
  elsif condition_by_role_id(smart_action[:triggerConditions]).nil? || match_conditions(:triggerConditions)
62
74
  return true
63
75
  end
64
76
  end
65
77
 
66
- raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR)
78
+ raise CustomActionTriggerForbiddenError, 'You don\'t have the permission to trigger this action.'
67
79
  end
68
80
 
69
81
  def match_conditions(condition_name)
@@ -105,16 +117,10 @@ module ForestAdminAgent
105
117
  # Wrap other ForestExceptions (like invalid operators) in ConflictError
106
118
  raise if e.message.include?('has no primary keys')
107
119
 
108
- raise ConflictError.new(
109
- 'The conditions to trigger this action cannot be verified. Please contact an administrator.',
110
- INVALID_ACTION_CONDITION_ERROR
111
- )
120
+ raise InvalidActionConditionError, 'The conditions to trigger this action cannot be verified. Please contact an administrator.'
112
121
  rescue ArgumentError, TypeError => e
113
122
  # Catch specific errors from condition parsing/validation
114
- raise ConflictError.new(
115
- "Invalid action condition: #{e.message}. Please contact an administrator.",
116
- INVALID_ACTION_CONDITION_ERROR
117
- )
123
+ raise InvalidActionConditionError, "Invalid action condition: #{e.message}. Please contact an administrator."
118
124
  rescue StandardError => e
119
125
  # Catch unexpected errors and log for debugging
120
126
  ForestAdminAgent::Facades::Container.logger.log(
@@ -122,10 +128,7 @@ module ForestAdminAgent
122
128
  "Unexpected error in match_conditions: #{e.class} - #{e.message}"
123
129
  )
124
130
 
125
- raise ConflictError.new(
126
- 'The conditions to trigger this action cannot be verified. Please contact an administrator.',
127
- INVALID_ACTION_CONDITION_ERROR
128
- )
131
+ raise InvalidActionConditionError, 'The conditions to trigger this action cannot be verified. Please contact an administrator.'
129
132
  end
130
133
 
131
134
  def condition_by_role_id(condition)
@@ -3,6 +3,7 @@ require 'ld-eventsource'
3
3
  module ForestAdminAgent
4
4
  module Services
5
5
  class SSECacheInvalidation
6
+ include ForestAdminAgent::Http::Exceptions
6
7
  include ForestAdminDatasourceToolkit::Exceptions
7
8
 
8
9
  MESSAGE_CACHE_KEYS = {
@@ -38,7 +39,7 @@ module ForestAdminAgent
38
39
  )
39
40
  end
40
41
  end
41
- rescue StandardError
42
+ rescue StandardError => e
42
43
  ForestAdminAgent::Facades::Container.logger.log(
43
44
  'Debug',
44
45
  'SSE connection to forestadmin server'
@@ -49,7 +50,11 @@ module ForestAdminAgent
49
50
  'SSE connection to forestadmin server closed unexpectedly, retrying.'
50
51
  )
51
52
 
52
- raise ForestException, 'Failed to reach SSE data from ForestAdmin server.'
53
+ raise ServiceUnavailableError.new(
54
+ 'Failed to reach SSE data from ForestAdmin server',
55
+ details: { error: e.message },
56
+ cause: e
57
+ )
53
58
  end
54
59
  end
55
60
  end
@@ -5,6 +5,7 @@ require 'active_support/time'
5
5
  module ForestAdminAgent
6
6
  module Utils
7
7
  class CallerParser
8
+ include ForestAdminAgent::Http::Exceptions
8
9
  include ForestAdminDatasourceToolkit::Exceptions
9
10
 
10
11
  def initialize(args)
@@ -29,16 +30,15 @@ module ForestAdminAgent
29
30
  def validate_headers
30
31
  return if @args.dig(:headers, 'HTTP_AUTHORIZATION')
31
32
 
32
- raise Http::Exceptions::HttpException.new(
33
- 401,
34
- 'You must be logged in to access at this resource.'
35
- )
33
+ raise Http::Exceptions::UnauthorizedError, 'You must be logged in to access at this resource.'
36
34
  end
37
35
 
38
36
  def extract_timezone
39
37
  timezone = @args[:params]['timezone']
40
- raise ForestException, 'Missing timezone' unless timezone
41
- raise ForestException, "Invalid timezone: #{timezone}" unless Time.find_zone(timezone)
38
+
39
+ raise BadRequestError, 'Missing timezone' unless timezone
40
+
41
+ raise BadRequestError, "Invalid timezone: #{timezone}" unless Time.find_zone(timezone)
42
42
 
43
43
  timezone
44
44
  end
@@ -4,7 +4,7 @@ require 'active_support/time'
4
4
  module ForestAdminAgent
5
5
  module Utils
6
6
  class ConditionTreeParser
7
- include ForestAdminDatasourceToolkit::Exceptions
7
+ include ForestAdminAgent::Http::Exceptions
8
8
  include ForestAdminDatasourceToolkit::Utils
9
9
  include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
10
10
  include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes
@@ -26,7 +26,7 @@ module ForestAdminAgent
26
26
  return conditions.size == 1 ? conditions[0] : ConditionTreeBranch.new(aggregator, conditions)
27
27
  end
28
28
 
29
- raise ForestException, 'Failed to instantiate condition tree'
29
+ raise BadRequestError, 'Failed to instantiate condition tree'
30
30
  end
31
31
 
32
32
  def self.parse_value(collection, leaf)
@@ -1,6 +1,7 @@
1
1
  module ForestAdminAgent
2
2
  module Utils
3
3
  class QueryStringParser
4
+ include ForestAdminAgent::Http::Exceptions
4
5
  include ForestAdminDatasourceToolkit::Exceptions
5
6
  include ForestAdminDatasourceToolkit::Components
6
7
  include ForestAdminDatasourceToolkit::Components::Query
@@ -92,7 +93,7 @@ module ForestAdminAgent
92
93
  limit_valid = !items_per_pages.to_s.match(/\A[+]?\d+\z/).nil? && items_per_pages.to_i.positive?
93
94
 
94
95
  unless page_valid && limit_valid
95
- raise ForestException, "Invalid pagination [limit: #{items_per_pages}, skip: #{page}]"
96
+ raise BadRequestError, "Invalid pagination [limit: #{items_per_pages}, skip: #{page}]"
96
97
  end
97
98
 
98
99
  offset = (page.to_i - 1) * items_per_pages.to_i
@@ -107,7 +108,7 @@ module ForestAdminAgent
107
108
  def self.parse_search(collection, args)
108
109
  search = args.dig(:params, :data, :attributes, :all_records_subset_query, :search) || args.dig(:params, :search)
109
110
 
110
- raise ForestException, 'Collection is not searchable' if search && !collection.is_searchable?
111
+ raise BadRequestError, 'Collection is not searchable' if search && !collection.is_searchable?
111
112
 
112
113
  search
113
114
  end
@@ -148,7 +149,7 @@ module ForestAdminAgent
148
149
 
149
150
  return unless segment
150
151
 
151
- raise ForestException, "Invalid segment: #{segment}" unless collection.schema[:segments].include?(segment)
152
+ raise BadRequestError, "Invalid segment: #{segment}" unless collection.schema[:segments].include?(segment)
152
153
 
153
154
  segment
154
155
  end
@@ -8,7 +8,7 @@ module ForestAdminAgent
8
8
 
9
9
  def self.valid?(query)
10
10
  query = query.strip
11
- raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Query cannot be empty.' if query.empty?
11
+ raise Http::Exceptions::BadRequestError, 'Query cannot be empty.' if query.empty?
12
12
 
13
13
  sanitized_query = remove_content_inside_strings(query)
14
14
  check_select_only(sanitized_query)
@@ -21,30 +21,31 @@ module ForestAdminAgent
21
21
  end
22
22
 
23
23
  class << self
24
- include ForestAdminDatasourceToolkit::Exceptions
25
-
26
24
  private
27
25
 
28
26
  def check_select_only(query)
29
27
  return if query.strip.upcase.start_with?('SELECT')
30
28
 
31
- raise ForestException, 'Only SELECT queries are allowed.'
29
+ raise Http::Exceptions::BadRequestError, 'Only SELECT queries are allowed.'
32
30
  end
33
31
 
34
32
  def check_semicolon_placement(query)
35
33
  semicolon_count = query.scan(';').size
36
34
 
37
- raise ForestException, 'Only one query is allowed.' if semicolon_count > 1
35
+ raise Http::Exceptions::BadRequestError, 'Only one query is allowed.' if semicolon_count > 1
38
36
  return if semicolon_count != 1 || query.strip[-1] == ';'
39
37
 
40
- raise ForestException, 'Semicolon must only appear as the last character in the query.'
38
+ raise Http::Exceptions::BadRequestError, 'Semicolon must only appear as the last character in the query.'
41
39
  end
42
40
 
43
41
  def check_forbidden_keywords(query)
44
42
  FORBIDDEN_KEYWORDS.each do |keyword|
45
- if /\b#{Regexp.escape(keyword)}\b/i.match?(query)
46
- raise ForestException, "The query contains forbidden keyword: #{keyword}."
47
- end
43
+ next unless /\b#{Regexp.escape(keyword)}\b/i.match?(query)
44
+
45
+ raise Http::Exceptions::BadRequestError.new(
46
+ "The query contains forbidden keyword: #{keyword}.",
47
+ details: { forbidden_keyword: keyword }
48
+ )
48
49
  end
49
50
  end
50
51
 
@@ -54,12 +55,17 @@ module ForestAdminAgent
54
55
 
55
56
  return if open_count == close_count
56
57
 
57
- raise ForestException, 'The query contains unbalanced parentheses.'
58
+ raise Http::Exceptions::BadRequestError.new(
59
+ 'The query contains unbalanced parentheses.',
60
+ details: { open_count: open_count, close_count: close_count }
61
+ )
58
62
  end
59
63
 
60
64
  def check_sql_injection_patterns(query)
61
65
  INJECTION_PATTERNS.each do |pattern|
62
- raise ForestException, 'The query contains a potential SQL injection pattern.' if pattern.match?(query)
66
+ next unless pattern.match?(query)
67
+
68
+ raise Http::Exceptions::BadRequestError, 'The query contains a potential SQL injection pattern.'
63
69
  end
64
70
  end
65
71
 
@@ -2,7 +2,7 @@ module ForestAdminAgent
2
2
  module Utils
3
3
  module Schema
4
4
  class GeneratorActionFieldWidget
5
- include ForestAdminDatasourceToolkit::Exceptions
5
+ include ForestAdminAgent::Http::Exceptions
6
6
 
7
7
  def self.build_widget_options(field)
8
8
  return if !ActionFields.widget?(field) || %w[Collection Enum EnumList].include?(field.type)
@@ -43,7 +43,10 @@ module ForestAdminAgent
43
43
 
44
44
  return build_address_autocomplete_widget_edit(field) if ActionFields.address_autocomplete_field?(field)
45
45
 
46
- raise ForestException, "Unsupported widget type: #{field&.widget}"
46
+ raise InternalServerError.new(
47
+ "Unsupported widget type: #{field&.widget}",
48
+ details: { widget: field&.widget, field_type: field&.type }
49
+ )
47
50
  end
48
51
 
49
52
  class << self
@@ -6,7 +6,7 @@ module ForestAdminAgent
6
6
  module Schema
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "agent-ruby"
9
- LIANA_VERSION = "1.12.11"
9
+ LIANA_VERSION = "1.12.13"
10
10
 
11
11
  def self.generate(datasource)
12
12
  datasource.collections
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.12.11"
2
+ VERSION = "1.12.13"
3
3
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'forest_admin_agent/version'
2
+ require_relative 'forest_admin_agent/http/Exceptions/business_error'
2
3
  require 'zeitwerk'
3
4
 
4
5
  loader = Zeitwerk::Loader.for_gem
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.11
4
+ version: 1.12.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -300,15 +300,9 @@ files:
300
300
  - lib/forest_admin_agent/builder/agent_factory.rb
301
301
  - lib/forest_admin_agent/facades/container.rb
302
302
  - lib/forest_admin_agent/facades/whitelist.rb
303
- - lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb
304
- - lib/forest_admin_agent/http/Exceptions/conflict_error.rb
305
- - lib/forest_admin_agent/http/Exceptions/forbidden_error.rb
303
+ - lib/forest_admin_agent/http/Exceptions/business_error.rb
306
304
  - lib/forest_admin_agent/http/Exceptions/http_exception.rb
307
- - lib/forest_admin_agent/http/Exceptions/not_found_error.rb
308
- - lib/forest_admin_agent/http/Exceptions/require_approval.rb
309
- - lib/forest_admin_agent/http/Exceptions/unprocessable_error.rb
310
- - lib/forest_admin_agent/http/Exceptions/validation_error.rb
311
- - lib/forest_admin_agent/http/error_handling.rb
305
+ - lib/forest_admin_agent/http/error_translator.rb
312
306
  - lib/forest_admin_agent/http/forest_admin_api_requester.rb
313
307
  - lib/forest_admin_agent/http/router.rb
314
308
  - lib/forest_admin_agent/routes/abstract_authenticated_route.rb
@@ -1,12 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module Exceptions
4
- class AuthenticationOpenIdClient < HttpException
5
- def initialize(message = 'Authentication failed with OpenID Client',
6
- name = 'AuthenticationOpenIdClient')
7
- super(401, message, name)
8
- end
9
- end
10
- end
11
- end
12
- end
@@ -1,13 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module Exceptions
4
- class ConflictError < HttpException
5
- attr_reader :name
6
-
7
- def initialize(message, name = 'ConflictError')
8
- super(429, message, name)
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module Exceptions
4
- class ForbiddenError < HttpException
5
- attr_reader :name
6
-
7
- def initialize(message, name = 'ForbiddenError')
8
- super(403, message, name)
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,11 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module Exceptions
4
- class NotFoundError < HttpException
5
- def initialize(message, name = 'NotFoundError')
6
- super(404, message, name)
7
- end
8
- end
9
- end
10
- end
11
- end
@@ -1,14 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module Exceptions
4
- class RequireApproval < HttpException
5
- attr_reader :name, :data
6
-
7
- def initialize(message, name = 'RequireApproval', data = [])
8
- super(403, message, name)
9
- @data = data
10
- end
11
- end
12
- end
13
- end
14
- end
@@ -1,13 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module Exceptions
4
- class UnprocessableError < HttpException
5
- attr_reader :name
6
-
7
- def initialize(message = '', name = 'UnprocessableError')
8
- super(422, message, name)
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module Exceptions
4
- class ValidationError < HttpException
5
- attr_reader :name
6
-
7
- def initialize(message, name = 'ValidationError')
8
- super(400, message, name)
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,28 +0,0 @@
1
- module ForestAdminAgent
2
- module Http
3
- module ErrorHandling
4
- def get_error_message(error)
5
- if error.class.respond_to?(:ancestors) && error.class.ancestors.include?(ForestAdminAgent::Http::Exceptions::HttpException)
6
- return error.message
7
- end
8
-
9
- if (customizer = ForestAdminAgent::Facades::Container.cache(:customize_error_message))
10
- message = eval(customizer).call(error)
11
- return message if message
12
- end
13
-
14
- return error.message if error.is_a?(ForestAdminDatasourceToolkit::Exceptions::ValidationError)
15
-
16
- 'Unexpected error'
17
- end
18
-
19
- def get_error_status(error)
20
- return error.status if error.respond_to?(:status) && error.status
21
-
22
- return 400 if error.is_a?(ForestAdminDatasourceToolkit::Exceptions::ValidationError)
23
-
24
- 500
25
- end
26
- end
27
- end
28
- end