diff --git a/app/controllers/concerns/epp_requestable.rb b/app/controllers/concerns/epp_requestable.rb index 59d064644..05f39d93f 100644 --- a/app/controllers/concerns/epp_requestable.rb +++ b/app/controllers/concerns/epp_requestable.rb @@ -9,8 +9,8 @@ module EppRequestable authorize! :create, Epp::Server result = server.request(request_params[:payload]) render_success(data: { xml: result.force_encoding('UTF-8') }) - rescue StandardError - handle_non_epp_errors(nil, I18n.t('errors.messages.epp_conn_error')) + rescue StandardError => e + handle_non_epp_errors(nil, e.message.presence || I18n.t('errors.messages.epp_conn_error')) end private diff --git a/app/controllers/concerns/error_and_log_handler.rb b/app/controllers/concerns/error_and_log_handler.rb new file mode 100644 index 000000000..57f8c6b08 --- /dev/null +++ b/app/controllers/concerns/error_and_log_handler.rb @@ -0,0 +1,80 @@ +module ErrorAndLogHandler + extend ActiveSupport::Concern + + included do + around_action :log_request + end + + private + + # rubocop:disable Metrics/MethodLength + def log_request + yield + rescue ActiveRecord::RecordNotFound + handle_record_not_found + rescue ActionController::ParameterMissing, Apipie::ParamMissing => e + handle_parameter_missing(e) + rescue Apipie::ParamInvalid => e + handle_param_invalid(e) + rescue CanCan::AccessDenied => e + handle_access_denied(e) + rescue Shunter::ThrottleError => e + handle_throttle_error(e) + ensure + create_repp_log + end + # rubocop:enable Metrics/MethodLength + + def handle_record_not_found + @response = { code: 2303, message: 'Object does not exist' } + render(json: @response, status: :not_found) + end + + def handle_parameter_missing(error) + @response = { code: 2003, message: error.message.gsub(/\n/, '. ') } + render(json: @response, status: :bad_request) + end + + def handle_param_invalid(error) + @response = { code: 2005, message: error.message.gsub(/\n/, '. ') } + render(json: @response, status: :bad_request) + end + + def handle_access_denied(error) + @response = { code: 2201, message: 'Authorization error' } + logger.error error.to_s + render(json: @response, status: :unauthorized) + end + + def handle_throttle_error(error) + @response = { code: 2502, message: Shunter.default_error_message } + logger.error error.to_s unless Rails.env.test? + render(json: @response, status: :bad_request) + end + + def create_repp_log + log_attributes = build_log_attributes + ApiLog::ReppLog.create(log_attributes) + end + + def build_log_attributes + { + request_path: request.path, ip: request.ip, + request_method: request.request_method, + request_params: build_request_params_json, + uuid: request.try(:uuid), + response: @response.to_json, + response_code: response.status, + api_user_name: current_user.try(:username), + api_user_registrar: current_user.try(:registrar).try(:to_s) + } + end + + def build_request_params_json + request.params.except('route_info').to_json + end + + def logger + Rails.logger + end +end diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb index d2057599e..1faca4e68 100644 --- a/app/controllers/repp/v1/base_controller.rb +++ b/app/controllers/repp/v1/base_controller.rb @@ -3,51 +3,18 @@ module Repp class BaseController < ActionController::API # rubocop:disable Metrics/ClassLength attr_reader :current_user - around_action :log_request + include ErrorAndLogHandler + before_action :authenticate_user before_action :set_locale before_action :validate_webclient_ca - before_action :validate_client_certs - before_action :check_ip_restriction + before_action :validate_api_user_cert + before_action :check_registrar_ip_restriction + before_action :check_api_ip_restriction before_action :set_paper_trail_whodunnit private - def log_request - yield - rescue ActiveRecord::RecordNotFound - @response = { code: 2303, message: 'Object does not exist' } - render(json: @response, status: :not_found) - rescue ActionController::ParameterMissing, Apipie::ParamMissing => e - @response = { code: 2003, message: e.message.gsub(/\n/, '. ') } - render(json: @response, status: :bad_request) - rescue Apipie::ParamInvalid => e - @response = { code: 2005, message: e.message.gsub(/\n/, '. ') } - render(json: @response, status: :bad_request) - rescue CanCan::AccessDenied => e - @response = { code: 2201, message: 'Authorization error' } - logger.error e.to_s - render(json: @response, status: :unauthorized) - rescue Shunter::ThrottleError => e - @response = { code: 2502, message: Shunter.default_error_message } - logger.error e.to_s unless Rails.env.test? - render(json: @response, status: :bad_request) - ensure - create_repp_log - end - - # rubocop:disable Metrics/AbcSize - def create_repp_log - ApiLog::ReppLog.create( - request_path: request.path, request_method: request.request_method, - request_params: request.params.except('route_info').to_json, uuid: request.try(:uuid), - response: @response.to_json, response_code: response.status, ip: request.ip, - api_user_name: current_user.try(:username), - api_user_registrar: current_user.try(:registrar).try(:to_s) - ) - end - # rubocop:enable Metrics/AbcSize - def set_domain registrar = current_user.registrar @domain = Epp::Domain.find_by(registrar: registrar, name: params[:domain_id]) @@ -121,25 +88,23 @@ module Repp render(json: @response, status: :unauthorized) end - def check_ip_restriction - ip = webclient_request? ? request.headers['X-Client-IP'] : request.ip - return if registrar_ip_white?(ip) && webclient_request? - return if api_ip_white?(ip) && !webclient_request? + def check_api_ip_restriction + return if webclient_request? + return if @current_user.registrar.api_ip_white?(request.ip) - render_unauthorized_response(ip) + render_unauthorized_ip_response(request.ip) end - def registrar_ip_white?(ip) - return true unless ip + def check_registrar_ip_restriction + return unless webclient_request? - @current_user.registrar.registrar_ip_white?(ip) + ip = request.headers['Request-IP'] + return if @current_user.registrar.registrar_ip_white?(ip) + + render_unauthorized_ip_response(ip) end - def api_ip_white?(ip) - @current_user.registrar.api_ip_white?(ip) - end - - def render_unauthorized_response(ip) + def render_unauthorized_ip_response(ip) @response = { code: 2202, message: I18n.t('registrar.authorization.ip_not_allowed', ip: ip) } render json: @response, status: :unauthorized end @@ -167,18 +132,37 @@ module Repp render(json: @response, status: :unauthorized) end - def validate_client_certs + def validate_api_user_cert return if Rails.env.development? || Rails.env.test? return if webclient_request? - return if @current_user.pki_ok?(request.env['HTTP_SSL_CLIENT_CERT'], - request.env['HTTP_SSL_CLIENT_S_DN_CN']) - @response = { code: 2202, message: 'Invalid certificate' } + crt = request.env['HTTP_SSL_CLIENT_CERT'] + com = request.env['HTTP_SSL_CLIENT_S_DN_CN'] + + return if @current_user.pki_ok?(crt, com) + + render_invalid_cert_response + end + + def validate_webclient_user_cert + return if skip_webclient_user_cert_validation? + + crt = request.headers['User-Certificate'] + com = request.headers['User-Certificate-CN'] + + return if @current_user.pki_ok?(crt, com, api: false) + + render_invalid_cert_response + end + + def render_invalid_cert_response + @response = { code: 2202, message: 'Invalid user certificate' } render(json: @response, status: :unauthorized) end - def logger - Rails.logger + def skip_webclient_user_cert_validation? + !webclient_request? || request.headers['Requester'] == 'tara' || + Rails.env.development? end def auth_values_to_data(registrar:) diff --git a/app/controllers/repp/v1/registrar/auth_controller.rb b/app/controllers/repp/v1/registrar/auth_controller.rb index 5da1b3a38..676ea670e 100644 --- a/app/controllers/repp/v1/registrar/auth_controller.rb +++ b/app/controllers/repp/v1/registrar/auth_controller.rb @@ -2,9 +2,11 @@ module Repp module V1 module Registrar class AuthController < BaseController + before_action :validate_webclient_user_cert, only: :index skip_before_action :authenticate_user, only: :tara_callback - skip_before_action :check_ip_restriction, only: :tara_callback - skip_before_action :validate_client_certs, only: :tara_callback + skip_before_action :check_registrar_ip_restriction, only: :tara_callback + skip_before_action :check_api_ip_restriction, only: :tara_callback + skip_before_action :validate_api_user_cert, only: :tara_callback THROTTLED_ACTIONS = %i[index tara_callback].freeze include Shunter::Integration::Throttle @@ -21,7 +23,10 @@ module Repp def tara_callback user = ApiUser.from_omniauth(auth_params) response = { code: 401, message: I18n.t(:no_such_user), data: {} } - render(json: response, status: :unauthorized) and return unless user && user&.active + unless user&.active && webclient_request? + render(json: response, status: :unauthorized) + return + end token = Base64.urlsafe_encode64("#{user.username}:#{user.plain_text_password}") render_success(data: { token: token, username: user.username }) diff --git a/config/application.yml.sample b/config/application.yml.sample index 2de9c29cb..0d2fd399c 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -187,6 +187,13 @@ default_response_timeout: '1' epp_sessions_per_registrar: '4' +shunter_default_adapter: "Shunter::Adapters::Redis" +shunter_enabled: "false" +shunter_redis_host: "redis" +shunter_redis_port: "6379" +shunter_default_timespan: '60' +shunter_default_threshold: '100' + # Since the keys for staging are absent from the repo, we need to supply them separate for testing. test: payments_seb_bank_certificate: 'test/fixtures/files/seb_bank_cert.pem' diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 726767390..63c91fcde 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -19,6 +19,7 @@ end class JavaScriptApplicationSystemTestCase < ApplicationSystemTestCase self.use_transactional_tests = false DatabaseCleaner.strategy = :truncation + Webdrivers::Chromedriver.required_version = '114.0.5735.90' Capybara.register_driver(:chrome) do |app| options = ::Selenium::WebDriver::Chrome::Options.new diff --git a/test/integration/repp/v1/accounts/switch_user_test.rb b/test/integration/repp/v1/accounts/switch_user_test.rb index 2299f2bf5..fa26b442d 100644 --- a/test/integration/repp/v1/accounts/switch_user_test.rb +++ b/test/integration/repp/v1/accounts/switch_user_test.rb @@ -53,8 +53,8 @@ class ReppV1AccountsSwitchUserTest < ActionDispatch::IntegrationTest end def test_returns_error_response_if_throttled - ENV["shunter_default_threshold"] = '1' - ENV["shunter_enabled"] = 'true' + ENV['shunter_default_threshold'] = '1' + ENV['shunter_enabled'] = 'true' new_user = users(:api_goodnames) new_user.update(identity_code: '1234') @@ -71,7 +71,7 @@ class ReppV1AccountsSwitchUserTest < ActionDispatch::IntegrationTest assert_response :bad_request assert_equal json[:code], 2502 assert response.body.include?(Shunter.default_error_message) - ENV["shunter_default_threshold"] = '10000' - ENV["shunter_enabled"] = 'false' + ENV['shunter_default_threshold'] = '10000' + ENV['shunter_enabled'] = 'false' end end diff --git a/test/integration/repp/v1/base_test.rb b/test/integration/repp/v1/base_test.rb index ed69eba13..3fdc84c30 100644 --- a/test/integration/repp/v1/base_test.rb +++ b/test/integration/repp/v1/base_test.rb @@ -43,7 +43,7 @@ class ReppV1BaseTest < ActionDispatch::IntegrationTest assert_equal 'Invalid authorization information', response_json[:message] end - def test_takes_ip_whitelist_into_account + def test_takes_ip_whitelist_into_account_if_api_request Setting.api_ip_whitelist_enabled = true Setting.registrar_ip_whitelist_enabled = true @@ -67,7 +67,7 @@ class ReppV1BaseTest < ActionDispatch::IntegrationTest Repp::V1::BaseController.stub_any_instance(:webclient_request?, true) do Repp::V1::BaseController.stub_any_instance(:validate_webclient_ca, true) do - get repp_v1_contacts_path, headers: @auth_headers.merge!({ 'X-Client-IP' => whiteip.ipv4 }) + get repp_v1_contacts_path, headers: @auth_headers.merge!({ 'Request-IP' => whiteip.ipv4 }) end end @@ -77,6 +77,43 @@ class ReppV1BaseTest < ActionDispatch::IntegrationTest Setting.registrar_ip_whitelist_enabled = false end + def test_validates_webclient_user_certificate_ok + cert = certificates(:registrar) + @auth_headers.merge!({ 'User-Certificate' => cert.crt, 'User-Certificate-CN' => cert.common_name }) + + Repp::V1::BaseController.stub_any_instance(:webclient_request?, true) do + Repp::V1::BaseController.stub_any_instance(:validate_webclient_ca, true) do + get repp_v1_registrar_auth_index_path, headers: @auth_headers + end + end + + assert_response :ok + end + + def test_validates_webclient_user_certificate_if_missing + Repp::V1::BaseController.stub_any_instance(:webclient_request?, true) do + Repp::V1::BaseController.stub_any_instance(:validate_webclient_ca, true) do + get repp_v1_registrar_auth_index_path, headers: @auth_headers + end + end + + assert_unauthorized_user_cert + end + + def test_validates_webclient_user_certificate_if_revoked + cert = certificates(:registrar) + cert.update(revoked: true) + @auth_headers.merge!({ 'User-Certificate' => cert.crt, 'User-Certificate-CN' => cert.common_name }) + + Repp::V1::BaseController.stub_any_instance(:webclient_request?, true) do + Repp::V1::BaseController.stub_any_instance(:validate_webclient_ca, true) do + get repp_v1_registrar_auth_index_path, headers: @auth_headers + end + end + + assert_unauthorized_user_cert + end + private def assert_unauthorized_ip @@ -86,4 +123,12 @@ class ReppV1BaseTest < ActionDispatch::IntegrationTest assert_equal 2202, response_json[:code] assert response_json[:message].include? 'Access denied from IP' end + + def assert_unauthorized_user_cert + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 2202, response_json[:code] + assert response_json[:message].include? 'Invalid user certificate' + end end diff --git a/test/integration/repp/v1/registrar/auth/tara_callback_test.rb b/test/integration/repp/v1/registrar/auth/tara_callback_test.rb index 918fce9cc..0493f47f4 100644 --- a/test/integration/repp/v1/registrar/auth/tara_callback_test.rb +++ b/test/integration/repp/v1/registrar/auth/tara_callback_test.rb @@ -18,7 +18,12 @@ class ReppV1RegistrarAuthTaraCallbackTest < ActionDispatch::IntegrationTest }, } - post '/repp/v1/registrar/auth/tara_callback', headers: @auth_headers, params: request_body + Repp::V1::BaseController.stub_any_instance(:webclient_request?, true) do + Repp::V1::BaseController.stub_any_instance(:validate_webclient_ca, true) do + post '/repp/v1/registrar/auth/tara_callback', headers: @auth_headers, params: request_body + end + end + json = JSON.parse(response.body, symbolize_names: true) assert_response :ok @@ -43,4 +48,21 @@ class ReppV1RegistrarAuthTaraCallbackTest < ActionDispatch::IntegrationTest assert_response :unauthorized assert_equal 'No such user', json[:message] end + + def test_invalidates_user_if_not_webclient_request + request_body = { + auth: { + uid: 'EE1234', + }, + } + + Repp::V1::BaseController.stub_any_instance(:webclient_request?, false) do + post '/repp/v1/registrar/auth/tara_callback', headers: @auth_headers, params: request_body + end + + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 'No such user', json[:message] + end end