Skip to content

Commit

Permalink
Merge pull request #2602 from internetee/validate-client-cert
Browse files Browse the repository at this point in the history
Validation of user certificates
  • Loading branch information
vohmar authored Aug 15, 2023
2 parents c5a8718 + 7863322 commit 49e531f
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 68 deletions.
4 changes: 2 additions & 2 deletions app/controllers/concerns/epp_requestable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ def create
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
Expand Down
80 changes: 80 additions & 0 deletions app/controllers/concerns/error_and_log_handler.rb
Original file line number Diff line number Diff line change
@@ -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
96 changes: 40 additions & 56 deletions app/controllers/repp/v1/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,18 @@ module V1
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])
Expand Down Expand Up @@ -121,25 +88,23 @@ def authenticate_user
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)
end
ip = request.headers['Request-IP']
return if @current_user.registrar.registrar_ip_white?(ip)

def api_ip_white?(ip)
@current_user.registrar.api_ip_white?(ip)
render_unauthorized_ip_response(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
Expand Down Expand Up @@ -167,18 +132,37 @@ def validate_webclient_ca
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:)
Expand Down
11 changes: 8 additions & 3 deletions app/controllers/repp/v1/registrar/auth_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,7 +23,10 @@ def index
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 })
Expand Down
7 changes: 7 additions & 0 deletions config/application.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions test/application_system_test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ApplicationSystemTestCase < ActionDispatch::IntegrationTest
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
Expand Down
8 changes: 4 additions & 4 deletions test/integration/repp/v1/accounts/switch_user_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def test_switches_to_unlinked_api_user
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')
Expand All @@ -71,7 +71,7 @@ def test_returns_error_response_if_throttled
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
49 changes: 47 additions & 2 deletions test/integration/repp/v1/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_processes_invalid_base64_token_format_properly
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

Expand All @@ -67,7 +67,7 @@ def test_takes_ip_whitelist_into_account_if_webclient_request

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

Expand All @@ -77,6 +77,43 @@ def test_takes_ip_whitelist_into_account_if_webclient_request
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
Expand All @@ -86,4 +123,12 @@ def assert_unauthorized_ip
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
Loading

0 comments on commit 49e531f

Please sign in to comment.