From 637cabf95fc1965a07fb6928a364a87dce2fbfb8 Mon Sep 17 00:00:00 2001 From: tsoganov Date: Wed, 9 Oct 2024 11:57:32 +0300 Subject: [PATCH 1/8] Handling contact verifications --- app/controllers/application_controller.rb | 1 + .../identification_requests_controller.rb | 72 ++++++++++++ .../repp/v1/contacts_controller.rb | 23 +++- app/interactions/actions/contact_verify.rb | 47 ++++++++ app/mailers/contact_mailer.rb | 8 ++ app/models/ability.rb | 1 + app/services/eeid/base.rb | 103 +++++++++++++++++ app/services/eeid/identification_service.rb | 21 ++++ .../identification_requested.html.erb | 34 ++++++ .../identification_requested.text.erb | 31 ++++++ config/application.yml.sample | 3 + config/locales/contacts.en.yml | 3 + config/routes.rb | 7 ++ ...554_add_verification_fields_to_contacts.rb | 9 ++ db/structure.sql | 13 ++- lib/serializers/repp/contact.rb | 3 +- .../identification_requests_webhook_test.rb | 63 +++++++++++ .../repp/v1/contacts/verify_test.rb | 71 ++++++++++++ test/services/identification_service_test.rb | 104 ++++++++++++++++++ 19 files changed, 612 insertions(+), 5 deletions(-) create mode 100644 app/controllers/eeid/webhooks/identification_requests_controller.rb create mode 100644 app/interactions/actions/contact_verify.rb create mode 100644 app/services/eeid/base.rb create mode 100644 app/services/eeid/identification_service.rb create mode 100644 app/views/mailers/contact_mailer/identification_requested.html.erb create mode 100644 app/views/mailers/contact_mailer/identification_requested.text.erb create mode 100644 db/migrate/20240924103554_add_verification_fields_to_contacts.rb create mode 100644 test/integration/eeid/identification_requests_webhook_test.rb create mode 100644 test/integration/repp/v1/contacts/verify_test.rb create mode 100644 test/services/identification_service_test.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5a6b45668b..f831f29f50 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,6 +25,7 @@ def info_for_paper_trail def comma_support_for(parent_key, key) return if params[parent_key].blank? return if params[parent_key][key].blank? + params[parent_key][key].sub!(/,/, '.') end diff --git a/app/controllers/eeid/webhooks/identification_requests_controller.rb b/app/controllers/eeid/webhooks/identification_requests_controller.rb new file mode 100644 index 0000000000..1c82969d66 --- /dev/null +++ b/app/controllers/eeid/webhooks/identification_requests_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Eeid + module Webhooks + # Controller for handling eeID identification requests webhook + class IdentificationRequestsController < ActionController::Base + THROTTLED_ACTIONS = %i[create].freeze + include Shunter::Integration::Throttle + + rescue_from Shunter::ThrottleError, with: :handle_throttle_error + + # POST /eeid/webhooks/identification_requests + def create + return render_unauthorized unless ip_whitelisted? + return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature']) + + verify_contact(permitted_params[:reference]) + render json: { status: 'success' }, status: :ok + rescue StandardError => e + Rails.logger.error("Error handling webhook: #{e.message}") + render json: { error: 'Internal Server Error' }, status: :internal_server_error + end + + private + + def permitted_params + params.permit(:identification_request_id, :reference, :client_id) + end + + def render_unauthorized + render json: { error: "IPAddress #{request.remote_ip} not authorized" }, status: :unauthorized + end + + def render_invalid_signature + render json: { error: 'Invalid HMAC signature' }, status: :unauthorized + end + + def valid_hmac_signature?(hmac_signature) + secret = ENV['ident_service_client_secret'] + computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, request.raw_post) + ActiveSupport::SecurityUtils.secure_compare(computed_signature, hmac_signature) + end + + def verify_contact(ref) + contact = Contact.find_by_code(ref) + + if contact&.ident_request_sent_at.present? + contact.update(verified_at: Time.zone.now) + Rails.logger.info("Contact verified: #{ref}") + else + Rails.logger.error("Valid contact not found for reference: #{ref}") + end + end + + def ip_whitelisted? + allowed_ips = ENV['webhook_allowed_ips'].to_s.split(',').map(&:strip) + + allowed_ips.include?(request.remote_ip) || Rails.env.development? + end + + # Mock throttled_user using request IP + def throttled_user + # Create a mock user-like object with the request IP + OpenStruct.new(id: request.remote_ip, class: 'WebhookRequest') + end + + def handle_throttle_error + render json: { error: Shunter.default_error_message }, status: :bad_request + end + end + end +end diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb index 83b98835b9..623722cb84 100644 --- a/app/controllers/repp/v1/contacts_controller.rb +++ b/app/controllers/repp/v1/contacts_controller.rb @@ -2,10 +2,10 @@ module Repp module V1 class ContactsController < BaseController # rubocop:disable Metrics/ClassLength - before_action :find_contact, only: %i[show update destroy] + before_action :find_contact, only: %i[show update destroy verify] skip_around_action :log_request, only: :search - THROTTLED_ACTIONS = %i[index check search create show update destroy].freeze + THROTTLED_ACTIONS = %i[index check search create show update destroy verify].freeze include Shunter::Integration::Throttle api :get, '/repp/v1/contacts' @@ -116,6 +116,22 @@ def destroy render_success end + api :POST, '/repp/v1/contacts/verify/:contact_code' + desc 'Generate and send identification request to a contact' + def verify + authorize! :verify, Epp::Contact + action = Actions::ContactVerify.new(@contact) + + unless action.call + handle_non_epp_errors(@contact) + return + end + + data = { contact: { code: params[:id] } } + + render_success(data: data) + end + private def index_params @@ -217,7 +233,8 @@ def contact_addr_params end def contact_params - params.require(:contact).permit(:id, :name, :email, :phone, :legal_document, + params.require(:contact).permit(:id, :name, :email, :phone, :legal_document, :verified, + :verification_link, legal_document: %i[body type], ident: [%i[ident ident_type ident_country_code]], addr: [%i[country_code city street zip state]]) diff --git a/app/interactions/actions/contact_verify.rb b/app/interactions/actions/contact_verify.rb new file mode 100644 index 0000000000..d0fedb3ec0 --- /dev/null +++ b/app/interactions/actions/contact_verify.rb @@ -0,0 +1,47 @@ +module Actions + class ContactVerify + attr_reader :contact + + def initialize(contact) + @contact = contact + end + + def call + if contact.verified_at.present? + contact.errors.add(:base, :verification_exists) + return + end + + create_identification_request + + return false if contact.errors.any? + + commit + end + + private + + def create_identification_request + ident_service = Eeid::IdentificationService.new + request = ident_service.create_identification_request(request_payload) + ContactMailer.identification_requested(contact: contact, link: request['link']).deliver_now + rescue Eeid::IdentError => e + Rails.logger.error e.message + contact.errors.add(:base, :verification_error) + end + + def request_payload + { + claims_required: [{ + type: 'sub', + value: "#{contact.ident_country_code}#{contact.ident}" + }], + reference: contact.code + } + end + + def commit + @contact.update(ident_request_sent_at: Time.zone.now) + end + end +end diff --git a/app/mailers/contact_mailer.rb b/app/mailers/contact_mailer.rb index 81d2153869..f9197b0f87 100644 --- a/app/mailers/contact_mailer.rb +++ b/app/mailers/contact_mailer.rb @@ -9,6 +9,14 @@ def email_changed(contact:, old_email:) mail(to: contact.email, bcc: old_email, subject: subject) end + def identification_requested(contact:, link:) + @contact = contact + @verification_link = link + + subject = default_i18n_subject(contact_code: contact.code) + mail(to: contact.email, subject: subject) + end + private def address_processing diff --git a/app/models/ability.rb b/app/models/ability.rb index 7f3a96177c..ba6971dcf0 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -74,6 +74,7 @@ def epp # Registrar/api_user dynamic role can(:delete, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } can(:renew, Epp::Contact) can(:transfer, Epp::Contact) + can(:verify, Epp::Contact) can(:view_password, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } end diff --git a/app/services/eeid/base.rb b/app/services/eeid/base.rb new file mode 100644 index 0000000000..b448722720 --- /dev/null +++ b/app/services/eeid/base.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Eeid + class IdentError < StandardError; end + + # Base class for handling EEID identification requests. + class Base + BASE_URL = ENV['eeid_base_url'] + TOKEN_ENDPOINT = '/api/auth/v1/token' + + def initialize(client_id, client_secret) + @client_id = client_id + @client_secret = client_secret + @token = nil + end + + def request_endpoint(endpoint, method: :get, body: nil) + Rails.logger.debug("Requesting endpoint: #{endpoint} with method: #{method}") + authenticate unless @token + request = build_request(endpoint, method, body) + response = send_request(request) + handle_response(response) + rescue StandardError => e + handle_error(e) + end + + def authenticate + Rails.logger.debug("Authenticating with client_id: #{@client_id}") + uri = URI.parse("#{BASE_URL}#{TOKEN_ENDPOINT}") + request = build_auth_request(uri) + + response = send_auth_request(uri, request) + handle_auth_response(response) + end + + private + + def build_auth_request(uri) + request = Net::HTTP::Post.new(uri) + request['Authorization'] = "Basic #{Base64.strict_encode64("#{@client_id}:#{@client_secret}")}" + request + end + + def send_auth_request(uri, request) + Net::HTTP.start(uri.hostname, uri.port, use_ssl: ssl_enabled?) do |http| + Rails.logger.debug("Sending authentication request to #{uri}") + http.request(request) + end + end + + def handle_auth_response(response) + raise IdentError, "Authentication failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess) + + @token = JSON.parse(response.body)['access_token'] + Rails.logger.debug('Authentication successful, token received') + end + + def build_request(endpoint, method, body) + uri = URI.parse("#{BASE_URL}/#{endpoint}") + request = create_request(uri, method) + request['Authorization'] = "Bearer #{@token}" + request.body = body.to_json if body + request.content_type = 'application/json' + + request + end + + def create_request(uri, method) + case method.to_sym + when :get + Net::HTTP::Get.new(uri) + when :post + Net::HTTP::Post.new(uri) + else + raise IdentError, "Unsupported HTTP method: #{method}" + end + end + + def send_request(request) + uri = URI.parse(request.uri.to_s) + Net::HTTP.start(uri.hostname, uri.port, use_ssl: ssl_enabled?) do |http| + Rails.logger.debug("Sending #{request.method} request to #{uri} with body: #{request.body}") + http.request(request) + end + end + + def handle_response(response) + parsed_response = JSON.parse(response.body) + raise IdentError, parsed_response['error'] unless response.is_a?(Net::HTTPSuccess) + + Rails.logger.debug("Request successful: #{response.body}") + parsed_response + end + + def handle_error(exception) + raise IdentError, exception.message + end + + def ssl_enabled? + !%w[test].include?(Rails.env) + end + end +end diff --git a/app/services/eeid/identification_service.rb b/app/services/eeid/identification_service.rb new file mode 100644 index 0000000000..a3ebc321b0 --- /dev/null +++ b/app/services/eeid/identification_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Eeid + # This class handles identification services. + class IdentificationService < Base + CLIENT_ID = ENV['ident_service_client_id'] + CLIENT_SECRET = ENV['ident_service_client_secret'] + + def initialize + super(CLIENT_ID, CLIENT_SECRET) + end + + def create_identification_request(request_params) + request_endpoint('/api/ident/v1/identification_requests', method: :post, body: request_params) + end + + def get_identification_request(id) + request_endpoint("/api/ident/v1/identification_requests/#{id}") + end + end +end diff --git a/app/views/mailers/contact_mailer/identification_requested.html.erb b/app/views/mailers/contact_mailer/identification_requested.html.erb new file mode 100644 index 0000000000..b1b03c5e0c --- /dev/null +++ b/app/views/mailers/contact_mailer/identification_requested.html.erb @@ -0,0 +1,34 @@ +<% + contact = RegistrantPresenter.new(registrant: @contact, view: self) + registrar = RegistrarPresenter.new(registrar: @contact.registrar, view: self) +%> +Tere <%= contact.name %>, +

+Teie registripidaja <%= registrar.name %> on palunud Teil kinnitada oma isikut, et jätkata domeeni toimingutega. +

+Palun kinnitage oma isikut, et jätkata. +

+Jätka eeID-ga +

+Kontaktandmed:
+<%= render 'mailers/shared/registrant/registrant.et.html', registrant: contact, with_phone: true %> +

+Probleemide korral pöörduge oma registripidaja poole: +<%= render 'mailers/shared/registrar/registrar.et.html', registrar: registrar %> +<%= render 'mailers/shared/signatures/signature.et.html' %> +
+

+Hi <%= contact.name %>, +

+Your registrar <%= registrar.name %> has requested that you verify your identity in order to proceed with domain actions. +

+Please confirm your identity to continue. +

+Continue with eeID +

+Contact information:
+<%= render 'mailers/shared/registrant/registrant.en.html', registrant: contact, with_phone: true %> +

+In case of problems please turn to your registrar: +<%= render 'mailers/shared/registrar/registrar.en.html', registrar: registrar %> +<%= render 'mailers/shared/signatures/signature.en.html' %> \ No newline at end of file diff --git a/app/views/mailers/contact_mailer/identification_requested.text.erb b/app/views/mailers/contact_mailer/identification_requested.text.erb new file mode 100644 index 0000000000..a1a8e1d5f0 --- /dev/null +++ b/app/views/mailers/contact_mailer/identification_requested.text.erb @@ -0,0 +1,31 @@ +<% + contact = RegistrantPresenter.new(registrant: @contact, view: self) + registrar = RegistrarPresenter.new(registrar: @contact.registrar, view: self) +%> +Tere <%= contact.name %>, + +Teie registripidaja <%= registrar.name %> on palunud teil kinnitada oma isikut, et jätkata domeeni toimingutega. +Palun kinnitage oma isikut, klõpsates alloleval lingil: +<%= @verification_link %> + +Kontaktandmed: +<%= render 'mailers/shared/registrant/registrant.et.text', registrant: contact, with_phone: true %> + +Palun veenduge, et muudatus on korrektne ning probleemide korral pöörduge oma registripidaja poole: +<%= render 'mailers/shared/registrar/registrar.et.text', registrar: registrar %> +<%= render 'mailers/shared/signatures/signature.et.text' %> + +---------------------------------------------------------------------------------- + +Hi <%= contact.name %>, + +Your registrar <%= registrar.name %> has requested that you verify your identity in order to proceed with domain actions. +Please confirm your identity to continue by clicking the link below: +<%= @verification_link %> + +Contact information: +<%= render 'mailers/shared/registrant/registrant.en.text', registrant: contact, with_phone: true %> + +In case of problems please turn to your registrar: +<%= render 'mailers/shared/registrar/registrar.en.text', registrar: registrar %> +<%= render 'mailers/shared/signatures/signature.en.text' %> \ No newline at end of file diff --git a/config/application.yml.sample b/config/application.yml.sample index 91f6d09b03..8e35d74fee 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -88,6 +88,9 @@ registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with comma # Accreditation Center API accr_center_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas +# Webhooks +webhook_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas + # Shared key for REST-WHOIS Bounces API incl. CERT rwhois_bounces_api_shared_key: testkey diff --git a/config/locales/contacts.en.yml b/config/locales/contacts.en.yml index 361e63530c..86576b00c4 100644 --- a/config/locales/contacts.en.yml +++ b/config/locales/contacts.en.yml @@ -9,6 +9,9 @@ en: models: contact: attributes: + base: + verification_exists: Contact already verified + verification_error: Sending identification request failed code: blank: "Required parameter missing - code" too_long_contact_code: "Contact code is too long, max 100 characters" diff --git a/config/routes.rb b/config/routes.rb index 6652f013ed..2b12f0fda4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,7 @@ collection do get 'check/:id', to: 'contacts#check' get 'search(/:id)', to: 'contacts#search' + post 'verify/:id', to: 'contacts#verify' end end end @@ -372,6 +373,12 @@ end end + namespace :eeid do + namespace :webhooks do + resources :identification_requests, only: :create + end + end + # To prevent users seeing the default welcome message "Welcome aboard" from Rails root to: redirect('admin/sign_in') end diff --git a/db/migrate/20240924103554_add_verification_fields_to_contacts.rb b/db/migrate/20240924103554_add_verification_fields_to_contacts.rb new file mode 100644 index 0000000000..1d4120a7be --- /dev/null +++ b/db/migrate/20240924103554_add_verification_fields_to_contacts.rb @@ -0,0 +1,9 @@ +class AddVerificationFieldsToContacts < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_column :contacts, :ident_request_sent_at, :datetime + add_column :contacts, :verified_at, :datetime + add_index :contacts, :verified_at, algorithm: :concurrently + end +end diff --git a/db/structure.sql b/db/structure.sql index 6b189b54c1..82f10a2079 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -695,6 +695,8 @@ CREATE TABLE public.contacts ( registrant_publishable boolean DEFAULT false, checked_company_at timestamp without time zone, company_register_status character varying + ident_request_sent_at timestamp without time zone, + verified_at timestamp without time zone ); @@ -4263,6 +4265,13 @@ CREATE INDEX index_contacts_on_registrar_id ON public.contacts USING btree (regi CREATE INDEX index_contacts_on_registrar_id_and_ident_type ON public.contacts USING btree (registrar_id, ident_type); +-- +-- Name: index_contacts_on_verified_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_contacts_on_verified_at ON public.contacts USING btree (verified_at); + + -- -- Name: index_csync_records_on_domain_id; Type: INDEX; Schema: public; Owner: - -- @@ -5602,6 +5611,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230710120154'), ('20230711083811'), ('20240816091049'), -('20240816092636'); +('20240816092636'), +('20240903131540'), +('20240924103554'); diff --git a/lib/serializers/repp/contact.rb b/lib/serializers/repp/contact.rb index 5afab98f0e..5c587e4049 100644 --- a/lib/serializers/repp/contact.rb +++ b/lib/serializers/repp/contact.rb @@ -18,7 +18,8 @@ def to_json(obj = contact) json = { code: obj.code, name: obj.name, ident: ident, phone: obj.phone, created_at: obj.created_at, auth_info: obj.auth_info, email: obj.email, statuses: statuses, disclosed_attributes: obj.disclosed_attributes, - registrar: registrar } + registrar: registrar, ident_request_sent_at: obj.ident_request_sent_at, + verified_at: obj.verified_at } json[:address] = address if @show_address if @domain_params json[:domains] = domains diff --git a/test/integration/eeid/identification_requests_webhook_test.rb b/test/integration/eeid/identification_requests_webhook_test.rb new file mode 100644 index 0000000000..26bbac6cd9 --- /dev/null +++ b/test/integration/eeid/identification_requests_webhook_test.rb @@ -0,0 +1,63 @@ +require 'test_helper' + +class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest + setup do + @contact = contacts(:john) + @secret = 'valid_secret' + ENV['ident_service_client_secret'] = @secret + payload = { + identification_request_id: '123', + reference: @contact.code + } + @valid_hmac_signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json) + + adapter = ENV['shunter_default_adapter'].constantize.new + adapter&.clear! + end + + test 'should verify contact with valid signature and parameters' do + @contact.update!(ident_request_sent_at: Time.zone.now - 1.day) + post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } + + assert_response :ok + assert_equal({ 'status' => 'success' }, JSON.parse(response.body)) + assert_not_nil @contact.reload.verified_at + end + + test 'should return unauthorized for invalid HMAC signature' do + post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => 'invalid_signature' } + + assert_response :unauthorized + assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + end + + test 'should return unauthorized for missing parameters' do + post '/eeid/webhooks/identification_requests', params: { reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } + + assert_response :unauthorized + assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + end + + test 'should handle internal server error gracefully' do + # Simulate an error in the verify_contact method + Contact.stub :find_by_code, ->(_) { raise StandardError, 'Simulated error' } do + post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } + + assert_response :internal_server_error + assert_equal({ 'error' => 'Internal Server Error' }, JSON.parse(response.body)) + end + end + + test 'returns error response if throttled' do + ENV['shunter_default_threshold'] = '1' + ENV['shunter_enabled'] = 'true' + + post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } + post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } + + assert_response :bad_request + assert response.body.include?(Shunter.default_error_message) + ENV['shunter_default_threshold'] = '10000' + ENV['shunter_enabled'] = 'false' + end +end diff --git a/test/integration/repp/v1/contacts/verify_test.rb b/test/integration/repp/v1/contacts/verify_test.rb new file mode 100644 index 0000000000..3a48b45522 --- /dev/null +++ b/test/integration/repp/v1/contacts/verify_test.rb @@ -0,0 +1,71 @@ +require 'test_helper' + +class ReppV1ContactsVerifyTest < ActionDispatch::IntegrationTest + def setup + @contact = contacts(:john) + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + + adapter = ENV['shunter_default_adapter'].constantize.new + adapter&.clear! + + stub_request(:post, %r{api/auth/v1/token}).to_return(status: 200, body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {}) + stub_request(:post, %r{api/ident/v1/identification_requests}) + .with( + body: { + claims_required: [{ type: 'sub', value: "#{@contact.ident_country_code}#{@contact.ident}" }], + reference: @contact.code + } + ).to_return(status: 200, body: { id: '123' }.to_json, headers: {}) + end + + def test_returns_error_when_not_found + post '/repp/v1/contacts/verify/nonexistant:code', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + + def test_verifies_contact + post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:code]) + assert contact.present? + assert contact.ident_request_sent_at + assert_nil contact.verified_at + end + + def test_does_not_verify_already_verified_contact + @contact.update!(verified_at: Time.zone.now - 1.day) + post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 'Contact already verified', json[:message] + end + + def test_returns_error_response_if_throttled + ENV['shunter_default_threshold'] = '1' + ENV['shunter_enabled'] = 'true' + + post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers + post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + 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' + end +end diff --git a/test/services/identification_service_test.rb b/test/services/identification_service_test.rb new file mode 100644 index 0000000000..a11ef7806e --- /dev/null +++ b/test/services/identification_service_test.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'test_helper' + +class IdentificationServiceTest < ActiveSupport::TestCase + def setup + @service = Eeid::IdentificationService.new + end + + def test_create_identification_request_success + request_params = { + claims_required: [{ + type: 'sub', + value: 'EE1234567' + }], + reference: '111:111' + } + response_body = { id: '123', status: 'created' }.to_json + + stub_request(:post, %r{api/auth/v1/token}) + .to_return(status: 200, body: { access_token: 'mock_token' }.to_json) + + stub_request(:post, %r{api/ident/v1/identification_requests}) + .with( + headers: { 'Authorization' => 'Bearer mock_token' }, + body: request_params.to_json + ) + .to_return(status: 201, body: response_body) + + result = @service.create_identification_request(request_params) + assert_equal JSON.parse(response_body), result + assert_equal 'mock_token', @service.instance_variable_get(:@token) + end + + def test_create_identification_request_failure + request_params = { + claims_required: [{ + type: 'sub', + value: 'EE1234567' + }], + reference: '111:111' + } + + stub_request(:post, %r{api/auth/v1/token}) + .to_return(status: 200, body: { access_token: 'mock_token' }.to_json) + + stub_request(:post, %r{api/ident/v1/identification_requests}) + .with( + headers: { 'Authorization' => 'Bearer mock_token' }, + body: request_params.to_json + ) + .to_return(status: 400, body: { error: 'Bad Request' }.to_json) + + assert_raises(Eeid::IdentError, 'Bad Request') do + @service.create_identification_request(request_params) + end + end + + def test_get_identification_request_success + id = '123' + response_body = { id: id, status: 'completed' }.to_json + + stub_request(:post, %r{api/auth/v1/token}) + .to_return(status: 200, body: { access_token: 'mock_token' }.to_json) + + stub_request(:get, %r{api/ident/v1/identification_requests/#{id}}) + .with(headers: { 'Authorization' => 'Bearer mock_token' }) + .to_return(status: 200, body: response_body) + + result = @service.get_identification_request(id) + assert_equal JSON.parse(response_body), result + assert_equal 'mock_token', @service.instance_variable_get(:@token) + end + + def test_get_identification_request_failure + id = '123' + + stub_request(:post, %r{api/auth/v1/token}) + .to_return(status: 200, body: { access_token: 'mock_token' }.to_json) + + stub_request(:get, %r{api/ident/v1/identification_requests/#{id}}) + .with(headers: { 'Authorization' => 'Bearer mock_token' }) + .to_return(status: 404, body: { error: 'Not Found' }.to_json) + + assert_raises(Eeid::IdentError, 'Not Found') do + @service.get_identification_request(id) + end + end + + def test_authentication_needed_for_requests + stub_request(:post, %r{api/auth/v1/token}) + .to_return(status: 401, body: { error: 'Invalid credentials' }.to_json) + + assert_raises(Eeid::IdentError) do + @service.create_identification_request({ key: 'value' }) + end + + assert_raises(Eeid::IdentError) do + @service.get_identification_request('123') + end + + assert_equal nil, @service.instance_variable_get(:@token) + end +end From 285fa138e6c1ad5a3e3b5bf9d70d2c45eb4f4969 Mon Sep 17 00:00:00 2001 From: tsoganov Date: Wed, 9 Oct 2024 12:04:40 +0300 Subject: [PATCH 2/8] Fixed codeclimate --- .../eeid/webhooks/identification_requests_controller.rb | 9 +++++++-- config/application.yml.sample | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/controllers/eeid/webhooks/identification_requests_controller.rb b/app/controllers/eeid/webhooks/identification_requests_controller.rb index 1c82969d66..0eb45c4e18 100644 --- a/app/controllers/eeid/webhooks/identification_requests_controller.rb +++ b/app/controllers/eeid/webhooks/identification_requests_controller.rb @@ -10,6 +10,7 @@ class IdentificationRequestsController < ActionController::Base rescue_from Shunter::ThrottleError, with: :handle_throttle_error # POST /eeid/webhooks/identification_requests + def create return render_unauthorized unless ip_whitelisted? return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature']) @@ -17,8 +18,7 @@ def create verify_contact(permitted_params[:reference]) render json: { status: 'success' }, status: :ok rescue StandardError => e - Rails.logger.error("Error handling webhook: #{e.message}") - render json: { error: 'Internal Server Error' }, status: :internal_server_error + handle_error(e) end private @@ -64,6 +64,11 @@ def throttled_user OpenStruct.new(id: request.remote_ip, class: 'WebhookRequest') end + def handle_error(error) + Rails.logger.error("Error handling webhook: #{error.message}") + render json: { error: 'Internal Server Error' }, status: :internal_server_error + end + def handle_throttle_error render json: { error: Shunter.default_error_message }, status: :bad_request end diff --git a/config/application.yml.sample b/config/application.yml.sample index 8e35d74fee..31d6cdae7f 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -259,3 +259,7 @@ allow_accr_endspoints: 'true' whitelist_companies: - '12345678' - '87654321' + +eeid_base_url: 'http://eid.test' +ident_service_client_id: 123 +ident_service_client_secret: 321 From 8ed18ab76ec72bf42a922783bc545726a7ee0f30 Mon Sep 17 00:00:00 2001 From: tsoganov Date: Thu, 10 Oct 2024 10:37:11 +0300 Subject: [PATCH 3/8] Skipped authenticity token for webhook controller --- .../eeid/webhooks/identification_requests_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/eeid/webhooks/identification_requests_controller.rb b/app/controllers/eeid/webhooks/identification_requests_controller.rb index 0eb45c4e18..346cd03c10 100644 --- a/app/controllers/eeid/webhooks/identification_requests_controller.rb +++ b/app/controllers/eeid/webhooks/identification_requests_controller.rb @@ -4,13 +4,14 @@ module Eeid module Webhooks # Controller for handling eeID identification requests webhook class IdentificationRequestsController < ActionController::Base + skip_before_action :verify_authenticity_token + THROTTLED_ACTIONS = %i[create].freeze include Shunter::Integration::Throttle rescue_from Shunter::ThrottleError, with: :handle_throttle_error # POST /eeid/webhooks/identification_requests - def create return render_unauthorized unless ip_whitelisted? return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature']) From d04622c49f4c28f4dae4fb1b4fe0fe34dec4da0b Mon Sep 17 00:00:00 2001 From: tsoganov Date: Fri, 11 Oct 2024 10:59:08 +0300 Subject: [PATCH 4/8] Fixed request endpoint uri --- app/services/eeid/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/eeid/base.rb b/app/services/eeid/base.rb index b448722720..78b6716f8c 100644 --- a/app/services/eeid/base.rb +++ b/app/services/eeid/base.rb @@ -56,7 +56,7 @@ def handle_auth_response(response) end def build_request(endpoint, method, body) - uri = URI.parse("#{BASE_URL}/#{endpoint}") + uri = URI.parse("#{BASE_URL}#{endpoint}") request = create_request(uri, method) request['Authorization'] = "Bearer #{@token}" request.body = body.to_json if body From 0085f99e0235c11e9bfeff9a0f2a0160a90b0dd6 Mon Sep 17 00:00:00 2001 From: tsoganov Date: Tue, 15 Oct 2024 14:13:04 +0300 Subject: [PATCH 5/8] Modified identification request webhook --- .../identification_requests_controller.rb | 30 ++++++-- .../repp/v1/contacts_controller.rb | 19 ++++- app/interactions/actions/contact_verify.rb | 4 +- app/mailers/registrar_mailer.rb | 10 +++ app/services/eeid/base.rb | 11 ++- app/services/eeid/identification_service.rb | 4 ++ .../contact_verified.html.erb | 44 ++++++++++++ .../contact_verified.text.erb | 32 +++++++++ config/routes.rb | 1 + ...5071505_add_verification_id_to_contacts.rb | 5 ++ db/structure.sql | 6 +- .../identification_requests_webhook_test.rb | 42 ++++++++++- .../repp/v1/contacts/download_poi_test.rb | 72 +++++++++++++++++++ .../repp/v1/contacts/verify_test.rb | 31 +++++++- 14 files changed, 292 insertions(+), 19 deletions(-) create mode 100644 app/mailers/registrar_mailer.rb create mode 100644 app/views/mailers/registrar_mailer/contact_verified.html.erb create mode 100644 app/views/mailers/registrar_mailer/contact_verified.text.erb create mode 100644 db/migrate/20241015071505_add_verification_id_to_contacts.rb create mode 100644 test/integration/repp/v1/contacts/download_poi_test.rb diff --git a/app/controllers/eeid/webhooks/identification_requests_controller.rb b/app/controllers/eeid/webhooks/identification_requests_controller.rb index 346cd03c10..219ec13756 100644 --- a/app/controllers/eeid/webhooks/identification_requests_controller.rb +++ b/app/controllers/eeid/webhooks/identification_requests_controller.rb @@ -16,7 +16,10 @@ def create return render_unauthorized unless ip_whitelisted? return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature']) - verify_contact(permitted_params[:reference]) + contact = Contact.find_by_code(permitted_params[:reference]) + poi = catch_poi + verify_contact(contact) + inform_registrar(contact, poi) render json: { status: 'success' }, status: :ok rescue StandardError => e handle_error(e) @@ -42,17 +45,32 @@ def valid_hmac_signature?(hmac_signature) ActiveSupport::SecurityUtils.secure_compare(computed_signature, hmac_signature) end - def verify_contact(ref) - contact = Contact.find_by_code(ref) - + def verify_contact(contact) + ref = permitted_params[:reference] if contact&.ident_request_sent_at.present? - contact.update(verified_at: Time.zone.now) + contact.update(verified_at: Time.zone.now, verification_id: permitted_params[:identification_request_id]) Rails.logger.info("Contact verified: #{ref}") else Rails.logger.error("Valid contact not found for reference: #{ref}") end end + def catch_poi + ident_service = Eeid::IdentificationService.new + response = ident_service.get_proof_of_identity(permitted_params[:identification_request_id]) + raise StandardError, response[:error] if response[:error].present? + + response[:data] + end + + def inform_registrar(contact, poi) + email = contact&.registrar&.email + return unless email + + RegistrarMailer.contact_verified(email: email, contact: contact, poi: poi) + .deliver_now + end + def ip_whitelisted? allowed_ips = ENV['webhook_allowed_ips'].to_s.split(',').map(&:strip) @@ -67,7 +85,7 @@ def throttled_user def handle_error(error) Rails.logger.error("Error handling webhook: #{error.message}") - render json: { error: 'Internal Server Error' }, status: :internal_server_error + render json: { error: error.message }, status: :internal_server_error end def handle_throttle_error diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb index 623722cb84..7b60d8b235 100644 --- a/app/controllers/repp/v1/contacts_controller.rb +++ b/app/controllers/repp/v1/contacts_controller.rb @@ -2,10 +2,10 @@ module Repp module V1 class ContactsController < BaseController # rubocop:disable Metrics/ClassLength - before_action :find_contact, only: %i[show update destroy verify] - skip_around_action :log_request, only: :search + before_action :find_contact, only: %i[show update destroy verify download_poi] + skip_around_action :log_request, only: %i[search] - THROTTLED_ACTIONS = %i[index check search create show update destroy verify].freeze + THROTTLED_ACTIONS = %i[index check search create show update destroy verify download_poi].freeze include Shunter::Integration::Throttle api :get, '/repp/v1/contacts' @@ -132,6 +132,19 @@ def verify render_success(data: data) end + api :get, '/repp/v1/contacts/download_poi/:contact_code' + desc 'Get proof of identity pdf file for a contact' + def download_poi + authorize! :verify, Epp::Contact + ident_service = Eeid::IdentificationService.new + response = ident_service.get_proof_of_identity(@contact.verification_id) + + send_data response[:data], filename: "proof_of_identity_#{@contact.verification_id}.pdf", + type: 'application/pdf', disposition: 'inline' + rescue Eeid::IdentError => e + handle_non_epp_errors(@contact, e.message) + end + private def index_params diff --git a/app/interactions/actions/contact_verify.rb b/app/interactions/actions/contact_verify.rb index d0fedb3ec0..8b9fd4fd08 100644 --- a/app/interactions/actions/contact_verify.rb +++ b/app/interactions/actions/contact_verify.rb @@ -23,8 +23,8 @@ def call def create_identification_request ident_service = Eeid::IdentificationService.new - request = ident_service.create_identification_request(request_payload) - ContactMailer.identification_requested(contact: contact, link: request['link']).deliver_now + response = ident_service.create_identification_request(request_payload) + ContactMailer.identification_requested(contact: contact, link: response['link']).deliver_now rescue Eeid::IdentError => e Rails.logger.error e.message contact.errors.add(:base, :verification_error) diff --git a/app/mailers/registrar_mailer.rb b/app/mailers/registrar_mailer.rb new file mode 100644 index 0000000000..aafc1e8ff7 --- /dev/null +++ b/app/mailers/registrar_mailer.rb @@ -0,0 +1,10 @@ +class RegistrarMailer < ApplicationMailer + helper ApplicationHelper + + def contact_verified(email:, contact:, poi:) + @contact = contact + subject = 'Successful Contact Verification' + attachments['proof_of_identity.pdf'] = poi + mail(to: email, subject: subject) + end +end diff --git a/app/services/eeid/base.rb b/app/services/eeid/base.rb index 78b6716f8c..2bf552221f 100644 --- a/app/services/eeid/base.rb +++ b/app/services/eeid/base.rb @@ -85,10 +85,17 @@ def send_request(request) end def handle_response(response) - parsed_response = JSON.parse(response.body) + case response['content-type'] + when 'application/pdf', 'application/octet-stream' + parsed_response = { data: response.body, message: response['content-disposition'] } + when %r{application/json} + parsed_response = JSON.parse(response.body).with_indifferent_access + else + raise IdentError, 'Unsupported content type' + end + raise IdentError, parsed_response['error'] unless response.is_a?(Net::HTTPSuccess) - Rails.logger.debug("Request successful: #{response.body}") parsed_response end diff --git a/app/services/eeid/identification_service.rb b/app/services/eeid/identification_service.rb index a3ebc321b0..5757b307ce 100644 --- a/app/services/eeid/identification_service.rb +++ b/app/services/eeid/identification_service.rb @@ -17,5 +17,9 @@ def create_identification_request(request_params) def get_identification_request(id) request_endpoint("/api/ident/v1/identification_requests/#{id}") end + + def get_proof_of_identity(id) + request_endpoint("/api/ident/v1/identification_requests/#{id}/proof_of_identity") + end end end diff --git a/app/views/mailers/registrar_mailer/contact_verified.html.erb b/app/views/mailers/registrar_mailer/contact_verified.html.erb new file mode 100644 index 0000000000..5a22df7715 --- /dev/null +++ b/app/views/mailers/registrar_mailer/contact_verified.html.erb @@ -0,0 +1,44 @@ +Tere, +

+

+Anname teada, et järgmise kontakti kinnitamisprotsess on edukalt lõpule viidud. +

+

Kontaktandmed:

+
    +
  • Kood: <%= @contact.code %>
  • +
  • Nimi: <%= @contact.name %>
  • +
  • Identifikaator: <%= ident_for(@contact) %>
  • +
+

+Täielikud tulemused leiate manuses olevast PDF-failist. +

+Kui Teil on küsimusi seoses kontakti kinnitamise või muude teenustega registripidaja portaalis, võtke meiega ühendust. +

+

+Parimate soovidega, +

+<%= render 'mailers/shared/signatures/signature.et.html' %> +
+

+ +Hi, +

+

+We are writing to inform you that the verification process for the following contact has been successfully completed. +

+

Contact Details:

+
    +
  • Code: <%= @contact.code %>
  • +
  • Name: <%= @contact.name %>
  • +
  • Ident: <%= ident_for(@contact) %>
  • +
+

+The full result can be found in the attached PDF file. +

+

+If you have any questions regarding the contact verification or other services in the Registrar Portal, please do not hesitate to reach out to us. +

+

+Best regards, +

+<%= render 'mailers/shared/signatures/signature.en.html' %> \ No newline at end of file diff --git a/app/views/mailers/registrar_mailer/contact_verified.text.erb b/app/views/mailers/registrar_mailer/contact_verified.text.erb new file mode 100644 index 0000000000..17cb6e323f --- /dev/null +++ b/app/views/mailers/registrar_mailer/contact_verified.text.erb @@ -0,0 +1,32 @@ +Tere, + +Anname teada, et järgmise kontakti kinnitamisprotsess on edukalt lõpule viidud. + +Kontaktandmed: +- Kood: <%= @contact.code %> +- Nimi: <%= @contact.name %> +- Identifikaator: <%= ident_for(@contact) %> + +Täielikke tulemusi saab vaadata kontakti lehelt registripidaja portaalis. + +Kui Teil on küsimusi seoses kontakti kinnitamise või muude teenustega registripidaja portaalis, võtke meiega ühendust. + +Parimate soovidega, +<%= render 'mailers/shared/signatures/signature.et.text' %> +--- + +Hi, + +We are writing to inform you that the verification process for the following contact has been successfully completed. + +Contact Details: +- Code: <%= @contact.code %> +- Name: <%= @contact.name %> +- Ident: <%= ident_for(@contact) %> + +The full result can be viewed on the contact page in the Registrar Portal. + +If you have any questions regarding the contact verification or other services in the Registrar Portal, please do not hesitate to reach out to us. + +Best regards, +<%= render 'mailers/shared/signatures/signature.en.text' %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 2b12f0fda4..3a1c538a8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,6 +75,7 @@ get 'check/:id', to: 'contacts#check' get 'search(/:id)', to: 'contacts#search' post 'verify/:id', to: 'contacts#verify' + get 'download_poi/:id', to: 'contacts#download_poi' end end end diff --git a/db/migrate/20241015071505_add_verification_id_to_contacts.rb b/db/migrate/20241015071505_add_verification_id_to_contacts.rb new file mode 100644 index 0000000000..7aff96abc4 --- /dev/null +++ b/db/migrate/20241015071505_add_verification_id_to_contacts.rb @@ -0,0 +1,5 @@ +class AddVerificationIdToContacts < ActiveRecord::Migration[6.1] + def change + add_column :contacts, :verification_id, :string + end +end diff --git a/db/structure.sql b/db/structure.sql index 82f10a2079..d73bf3ebe5 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -696,7 +696,8 @@ CREATE TABLE public.contacts ( checked_company_at timestamp without time zone, company_register_status character varying ident_request_sent_at timestamp without time zone, - verified_at timestamp without time zone + verified_at timestamp without time zone, + verification_id character varying ); @@ -5613,6 +5614,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240816091049'), ('20240816092636'), ('20240903131540'), -('20240924103554'); +('20240924103554'), +('20241015071505'); diff --git a/test/integration/eeid/identification_requests_webhook_test.rb b/test/integration/eeid/identification_requests_webhook_test.rb index 26bbac6cd9..625cfa3a56 100644 --- a/test/integration/eeid/identification_requests_webhook_test.rb +++ b/test/integration/eeid/identification_requests_webhook_test.rb @@ -11,6 +11,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest } @valid_hmac_signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json) + stub_request(:post, %r{api/auth/v1/token}) + .to_return( + status: 200, + body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {} + ) + pdf_content = File.read(Rails.root.join('test/fixtures/files/legaldoc.pdf')) + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return(status: 200, body: pdf_content, headers: { 'Content-Type' => 'application/pdf' }) + adapter = ENV['shunter_default_adapter'].constantize.new adapter&.clear! end @@ -22,6 +31,8 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest assert_response :ok assert_equal({ 'status' => 'success' }, JSON.parse(response.body)) assert_not_nil @contact.reload.verified_at + assert_equal @contact.verification_id, '123' + assert_notify_registrar('Successful Contact Verification') end test 'should return unauthorized for invalid HMAC signature' do @@ -29,13 +40,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest assert_response :unauthorized assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + assert_emails 0 end test 'should return unauthorized for missing parameters' do post '/eeid/webhooks/identification_requests', params: { reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } assert_response :unauthorized - assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + assert_emails 0 end test 'should handle internal server error gracefully' do @@ -44,10 +57,24 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } assert_response :internal_server_error - assert_equal({ 'error' => 'Internal Server Error' }, JSON.parse(response.body)) + assert_equal({ 'error' => 'Simulated error' }, JSON.parse(response.body)) + assert_emails 0 end end + test 'should handle error from ident response' do + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return(status: :not_found, body: { error: 'Proof of identity not found' }.to_json, headers: { 'Content-Type' => 'application/json' }) + + @contact.update!(ident_request_sent_at: Time.zone.now - 1.day) + post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } + + assert_response :internal_server_error + assert_equal({ 'error' => 'Proof of identity not found' }, JSON.parse(response.body)) + assert_emails 0 + assert_nil @contact.reload.verified_at + end + test 'returns error response if throttled' do ENV['shunter_default_threshold'] = '1' ENV['shunter_enabled'] = 'true' @@ -60,4 +87,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest ENV['shunter_default_threshold'] = '10000' ENV['shunter_enabled'] = 'false' end + + private + + def assert_notify_registrar(subject) + assert_emails 1 + email = ActionMailer::Base.deliveries.last + assert_equal [@contact.registrar.email], email.to + assert_equal subject, email.subject + assert_equal 1, email.attachments.size + assert_equal 'proof_of_identity.pdf', email.attachments.first.filename + end end diff --git a/test/integration/repp/v1/contacts/download_poi_test.rb b/test/integration/repp/v1/contacts/download_poi_test.rb new file mode 100644 index 0000000000..1d943dc5c6 --- /dev/null +++ b/test/integration/repp/v1/contacts/download_poi_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' + +class ReppV1ContactsDownloadPoiTest < ActionDispatch::IntegrationTest + def setup + @contact = contacts(:john) + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + + adapter = ENV['shunter_default_adapter'].constantize.new + adapter&.clear! + + stub_request(:post, %r{api/auth/v1/token}) + .to_return( + status: 200, + body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {} + ) + pdf_content = File.read(Rails.root.join('test/fixtures/files/legaldoc.pdf')) + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return(status: 200, body: pdf_content, headers: { 'Content-Type' => 'application/pdf' }) + end + + def test_returns_error_when_not_found + get '/repp/v1/contacts/download_poi/nonexistant:code', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + + def test_downloads_poi_for_contact + @contact.update!(verified_at: Time.zone.now - 1.day, verification_id: '123') + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + + assert_response :ok + assert_equal 'application/pdf', response.headers['Content-Type'] + assert_equal "inline; filename=\"proof_of_identity_123.pdf\"; filename*=UTF-8''proof_of_identity_123.pdf", response.headers['Content-Disposition'] + assert_not_empty response.body + end + + def test_handles_non_epp_error + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return( + status: :not_found, + body: { error: 'Proof of identity not found' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 'Proof of identity not found', json[:message] + end + + def test_returns_error_response_if_throttled + ENV['shunter_default_threshold'] = '1' + ENV['shunter_enabled'] = 'true' + + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + 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' + end +end diff --git a/test/integration/repp/v1/contacts/verify_test.rb b/test/integration/repp/v1/contacts/verify_test.rb index 3a48b45522..1d62c4b837 100644 --- a/test/integration/repp/v1/contacts/verify_test.rb +++ b/test/integration/repp/v1/contacts/verify_test.rb @@ -12,14 +12,18 @@ def setup adapter = ENV['shunter_default_adapter'].constantize.new adapter&.clear! - stub_request(:post, %r{api/auth/v1/token}).to_return(status: 200, body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {}) + stub_request(:post, %r{api/auth/v1/token}) + .to_return( + status: 200, + body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {} + ) stub_request(:post, %r{api/ident/v1/identification_requests}) .with( body: { claims_required: [{ type: 'sub', value: "#{@contact.ident_country_code}#{@contact.ident}" }], reference: @contact.code } - ).to_return(status: 200, body: { id: '123' }.to_json, headers: {}) + ).to_return(status: 200, body: { id: '123' }.to_json, headers: { 'Content-Type' => 'application/json' }) end def test_returns_error_when_not_found @@ -43,6 +47,20 @@ def test_verifies_contact assert contact.present? assert contact.ident_request_sent_at assert_nil contact.verified_at + assert_notify_contact('Identification requested') + end + + def test_handles_non_epp_error + stub_request(:post, %r{api/ident/v1/identification_requests}) + .to_return(status: :unprocessable_entity, body: { error: 'error' }.to_json, headers: { 'Content-Type' => 'application/json' }) + + post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 'Sending identification request failed', json[:message] + assert_nil @contact.ident_request_sent_at + assert_emails 0 end def test_does_not_verify_already_verified_contact @@ -68,4 +86,13 @@ def test_returns_error_response_if_throttled ENV['shunter_default_threshold'] = '10000' ENV['shunter_enabled'] = 'false' end + + private + + def assert_notify_contact(subject) + assert_emails 1 + email = ActionMailer::Base.deliveries.last + assert_equal [@contact.email], email.to + assert_equal subject, email.subject + end end From af46be8d0f258d153dfa49647ffb1d5751cadf35 Mon Sep 17 00:00:00 2001 From: tsoganov Date: Tue, 15 Oct 2024 14:24:02 +0300 Subject: [PATCH 6/8] Corrected tests --- test/services/identification_service_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/services/identification_service_test.rb b/test/services/identification_service_test.rb index a11ef7806e..ff080d2396 100644 --- a/test/services/identification_service_test.rb +++ b/test/services/identification_service_test.rb @@ -25,7 +25,7 @@ def test_create_identification_request_success headers: { 'Authorization' => 'Bearer mock_token' }, body: request_params.to_json ) - .to_return(status: 201, body: response_body) + .to_return(status: 201, body: response_body, headers: { 'Content-Type' => 'application/json' }) result = @service.create_identification_request(request_params) assert_equal JSON.parse(response_body), result @@ -49,7 +49,7 @@ def test_create_identification_request_failure headers: { 'Authorization' => 'Bearer mock_token' }, body: request_params.to_json ) - .to_return(status: 400, body: { error: 'Bad Request' }.to_json) + .to_return(status: 400, body: { error: 'Bad Request' }.to_json, headers: { 'Content-Type' => 'application/json' }) assert_raises(Eeid::IdentError, 'Bad Request') do @service.create_identification_request(request_params) @@ -65,7 +65,7 @@ def test_get_identification_request_success stub_request(:get, %r{api/ident/v1/identification_requests/#{id}}) .with(headers: { 'Authorization' => 'Bearer mock_token' }) - .to_return(status: 200, body: response_body) + .to_return(status: 200, body: response_body, headers: { 'Content-Type' => 'application/json' }) result = @service.get_identification_request(id) assert_equal JSON.parse(response_body), result From 35c5eb6ab5fffbc0e95dd6ee767d55622be5e46e Mon Sep 17 00:00:00 2001 From: tsoganov Date: Tue, 12 Nov 2024 18:43:11 +0200 Subject: [PATCH 7/8] Fixed structure.sql --- db/structure.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/structure.sql b/db/structure.sql index d73bf3ebe5..98bd17871d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -694,7 +694,7 @@ CREATE TABLE public.contacts ( email_history character varying, registrant_publishable boolean DEFAULT false, checked_company_at timestamp without time zone, - company_register_status character varying + company_register_status character varying, ident_request_sent_at timestamp without time zone, verified_at timestamp without time zone, verification_id character varying From 6bd4c25d655d01cfe8a9d7c1b13b81f33a67c4b9 Mon Sep 17 00:00:00 2001 From: tsoganov Date: Thu, 14 Nov 2024 08:38:38 +0200 Subject: [PATCH 8/8] Updated gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 523cb5a8c7..75258a711d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ /node_modules /import ettevotja_rekvisiidid__lihtandmed.csv.zip +Dockerfile.dev