Merge pull request #2602 from internetee/validate-client-cert

Validation of user certificates
This commit is contained in:
Timo Võhmar 2023-08-15 16:16:59 +03:00 committed by GitHub
commit 49e531f797
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 213 additions and 69 deletions

View file

@ -9,8 +9,8 @@ module EppRequestable
authorize! :create, Epp::Server authorize! :create, Epp::Server
result = server.request(request_params[:payload]) result = server.request(request_params[:payload])
render_success(data: { xml: result.force_encoding('UTF-8') }) render_success(data: { xml: result.force_encoding('UTF-8') })
rescue StandardError rescue StandardError => e
handle_non_epp_errors(nil, I18n.t('errors.messages.epp_conn_error')) handle_non_epp_errors(nil, e.message.presence || I18n.t('errors.messages.epp_conn_error'))
end end
private private

View file

@ -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

View file

@ -3,51 +3,18 @@ module Repp
class BaseController < ActionController::API # rubocop:disable Metrics/ClassLength class BaseController < ActionController::API # rubocop:disable Metrics/ClassLength
attr_reader :current_user attr_reader :current_user
around_action :log_request include ErrorAndLogHandler
before_action :authenticate_user before_action :authenticate_user
before_action :set_locale before_action :set_locale
before_action :validate_webclient_ca before_action :validate_webclient_ca
before_action :validate_client_certs before_action :validate_api_user_cert
before_action :check_ip_restriction before_action :check_registrar_ip_restriction
before_action :check_api_ip_restriction
before_action :set_paper_trail_whodunnit before_action :set_paper_trail_whodunnit
private 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 def set_domain
registrar = current_user.registrar registrar = current_user.registrar
@domain = Epp::Domain.find_by(registrar: registrar, name: params[:domain_id]) @domain = Epp::Domain.find_by(registrar: registrar, name: params[:domain_id])
@ -121,25 +88,23 @@ module Repp
render(json: @response, status: :unauthorized) render(json: @response, status: :unauthorized)
end end
def check_ip_restriction def check_api_ip_restriction
ip = webclient_request? ? request.headers['X-Client-IP'] : request.ip return if webclient_request?
return if registrar_ip_white?(ip) && webclient_request? return if @current_user.registrar.api_ip_white?(request.ip)
return if api_ip_white?(ip) && !webclient_request?
render_unauthorized_response(ip) render_unauthorized_ip_response(request.ip)
end end
def registrar_ip_white?(ip) def check_registrar_ip_restriction
return true unless ip 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 end
def api_ip_white?(ip) def render_unauthorized_ip_response(ip)
@current_user.registrar.api_ip_white?(ip)
end
def render_unauthorized_response(ip)
@response = { code: 2202, message: I18n.t('registrar.authorization.ip_not_allowed', ip: ip) } @response = { code: 2202, message: I18n.t('registrar.authorization.ip_not_allowed', ip: ip) }
render json: @response, status: :unauthorized render json: @response, status: :unauthorized
end end
@ -167,18 +132,37 @@ module Repp
render(json: @response, status: :unauthorized) render(json: @response, status: :unauthorized)
end end
def validate_client_certs def validate_api_user_cert
return if Rails.env.development? || Rails.env.test? return if Rails.env.development? || Rails.env.test?
return if webclient_request? 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) render(json: @response, status: :unauthorized)
end end
def logger def skip_webclient_user_cert_validation?
Rails.logger !webclient_request? || request.headers['Requester'] == 'tara' ||
Rails.env.development?
end end
def auth_values_to_data(registrar:) def auth_values_to_data(registrar:)

View file

@ -2,9 +2,11 @@ module Repp
module V1 module V1
module Registrar module Registrar
class AuthController < BaseController class AuthController < BaseController
before_action :validate_webclient_user_cert, only: :index
skip_before_action :authenticate_user, only: :tara_callback skip_before_action :authenticate_user, only: :tara_callback
skip_before_action :check_ip_restriction, only: :tara_callback skip_before_action :check_registrar_ip_restriction, only: :tara_callback
skip_before_action :validate_client_certs, 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 THROTTLED_ACTIONS = %i[index tara_callback].freeze
include Shunter::Integration::Throttle include Shunter::Integration::Throttle
@ -21,7 +23,10 @@ module Repp
def tara_callback def tara_callback
user = ApiUser.from_omniauth(auth_params) user = ApiUser.from_omniauth(auth_params)
response = { code: 401, message: I18n.t(:no_such_user), data: {} } 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}") token = Base64.urlsafe_encode64("#{user.username}:#{user.plain_text_password}")
render_success(data: { token: token, username: user.username }) render_success(data: { token: token, username: user.username })

View file

@ -187,6 +187,13 @@ default_response_timeout: '1'
epp_sessions_per_registrar: '4' 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. # Since the keys for staging are absent from the repo, we need to supply them separate for testing.
test: test:
payments_seb_bank_certificate: 'test/fixtures/files/seb_bank_cert.pem' payments_seb_bank_certificate: 'test/fixtures/files/seb_bank_cert.pem'

View file

@ -19,6 +19,7 @@ end
class JavaScriptApplicationSystemTestCase < ApplicationSystemTestCase class JavaScriptApplicationSystemTestCase < ApplicationSystemTestCase
self.use_transactional_tests = false self.use_transactional_tests = false
DatabaseCleaner.strategy = :truncation DatabaseCleaner.strategy = :truncation
Webdrivers::Chromedriver.required_version = '114.0.5735.90'
Capybara.register_driver(:chrome) do |app| Capybara.register_driver(:chrome) do |app|
options = ::Selenium::WebDriver::Chrome::Options.new options = ::Selenium::WebDriver::Chrome::Options.new

View file

@ -53,8 +53,8 @@ class ReppV1AccountsSwitchUserTest < ActionDispatch::IntegrationTest
end end
def test_returns_error_response_if_throttled def test_returns_error_response_if_throttled
ENV["shunter_default_threshold"] = '1' ENV['shunter_default_threshold'] = '1'
ENV["shunter_enabled"] = 'true' ENV['shunter_enabled'] = 'true'
new_user = users(:api_goodnames) new_user = users(:api_goodnames)
new_user.update(identity_code: '1234') new_user.update(identity_code: '1234')
@ -71,7 +71,7 @@ class ReppV1AccountsSwitchUserTest < ActionDispatch::IntegrationTest
assert_response :bad_request assert_response :bad_request
assert_equal json[:code], 2502 assert_equal json[:code], 2502
assert response.body.include?(Shunter.default_error_message) assert response.body.include?(Shunter.default_error_message)
ENV["shunter_default_threshold"] = '10000' ENV['shunter_default_threshold'] = '10000'
ENV["shunter_enabled"] = 'false' ENV['shunter_enabled'] = 'false'
end end
end end

View file

@ -43,7 +43,7 @@ class ReppV1BaseTest < ActionDispatch::IntegrationTest
assert_equal 'Invalid authorization information', response_json[:message] assert_equal 'Invalid authorization information', response_json[:message]
end 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.api_ip_whitelist_enabled = true
Setting.registrar_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(:webclient_request?, true) do
Repp::V1::BaseController.stub_any_instance(:validate_webclient_ca, 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
end end
@ -77,6 +77,43 @@ class ReppV1BaseTest < ActionDispatch::IntegrationTest
Setting.registrar_ip_whitelist_enabled = false Setting.registrar_ip_whitelist_enabled = false
end 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 private
def assert_unauthorized_ip def assert_unauthorized_ip
@ -86,4 +123,12 @@ class ReppV1BaseTest < ActionDispatch::IntegrationTest
assert_equal 2202, response_json[:code] assert_equal 2202, response_json[:code]
assert response_json[:message].include? 'Access denied from IP' assert response_json[:message].include? 'Access denied from IP'
end 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 end

View file

@ -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) json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok assert_response :ok
@ -43,4 +48,21 @@ class ReppV1RegistrarAuthTaraCallbackTest < ActionDispatch::IntegrationTest
assert_response :unauthorized assert_response :unauthorized
assert_equal 'No such user', json[:message] assert_equal 'No such user', json[:message]
end 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 end