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.
- checksums.yaml +4 -4
- data/lib/forest_admin_agent/auth/auth_manager.rb +9 -4
- data/lib/forest_admin_agent/builder/agent_factory.rb +20 -3
- data/lib/forest_admin_agent/http/Exceptions/business_error.rb +101 -0
- data/lib/forest_admin_agent/http/Exceptions/http_exception.rb +7 -3
- data/lib/forest_admin_agent/http/error_translator.rb +133 -0
- data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +27 -9
- data/lib/forest_admin_agent/routes/resources/native_query.rb +2 -1
- data/lib/forest_admin_agent/routes/security/authentication.rb +3 -4
- data/lib/forest_admin_agent/services/ip_whitelist.rb +7 -4
- data/lib/forest_admin_agent/services/permissions.rb +9 -3
- data/lib/forest_admin_agent/services/smart_action_checker.rb +27 -24
- data/lib/forest_admin_agent/services/sse_cache_invalidation.rb +7 -2
- data/lib/forest_admin_agent/utils/caller_parser.rb +6 -6
- data/lib/forest_admin_agent/utils/condition_tree_parser.rb +2 -2
- data/lib/forest_admin_agent/utils/query_string_parser.rb +4 -3
- data/lib/forest_admin_agent/utils/query_validator.rb +17 -11
- data/lib/forest_admin_agent/utils/schema/generator_action_field_widget.rb +5 -2
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- data/lib/forest_admin_agent.rb +1 -0
- metadata +3 -9
- data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +0 -12
- data/lib/forest_admin_agent/http/Exceptions/conflict_error.rb +0 -13
- data/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb +0 -13
- data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +0 -11
- data/lib/forest_admin_agent/http/Exceptions/require_approval.rb +0 -14
- data/lib/forest_admin_agent/http/Exceptions/unprocessable_error.rb +0 -13
- data/lib/forest_admin_agent/http/Exceptions/validation_error.rb +0 -13
- 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:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: dcedc2aca2e7965829e92f88f166f74264dd820084052ae74d225beb3a166aa8
         | 
| 4 | 
            +
              data.tar.gz: 8931db8225893bcb912b3a445d88f6323d58bac33caf166051b5753df84c3676
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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:: | 
| 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:: | 
| 44 | 
            -
             | 
| 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:: | 
| 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  | 
| 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  | 
| 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, : | 
| 7 | 
            +
                    attr_reader :name, :status, :data, :custom_headers
         | 
| 6 8 |  | 
| 7 | 
            -
                    def initialize(status, message,  | 
| 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  | 
| 35 | 
            -
             | 
| 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  | 
| 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  | 
| 44 | 
            -
             | 
| 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  | 
| 49 | 
            -
             | 
| 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  | 
| 53 | 
            -
             | 
| 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  | 
| 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 | 
| 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  | 
| 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  | 
| 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:: | 
| 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:: | 
| 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:: | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 69 | 
            +
                        raise CustomActionRequiresApprovalError.new(
         | 
| 57 70 | 
             
                          'This action requires to be approved.',
         | 
| 58 | 
            -
                           | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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:: | 
| 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 | 
            -
             | 
| 41 | 
            -
                    raise  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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  | 
| 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 | 
            -
                         | 
| 46 | 
            -
             | 
| 47 | 
            -
                         | 
| 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  | 
| 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 | 
            -
                         | 
| 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  | 
| 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  | 
| 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
         | 
    
        data/lib/forest_admin_agent.rb
    CHANGED
    
    
    
        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. | 
| 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/ | 
| 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/ | 
| 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,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,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
         |