diff --git a/app/controllers/api/v1/accreditation_center/auth_controller.rb b/app/controllers/api/v1/accreditation_center/auth_controller.rb new file mode 100644 index 000000000..9c49d81cf --- /dev/null +++ b/app/controllers/api/v1/accreditation_center/auth_controller.rb @@ -0,0 +1,67 @@ +require 'serializers/repp/domain' + +module Api + module V1 + module AccreditationCenter + class AuthController < ::Api::V1::AccreditationCenter::BaseController + before_action :authenticate_user + + def index + login = @current_user + registrar = @current_user.registrar + + # rubocop:disable Style/AndOr + render_success(data: nil) and return unless login + # rubocop:enable Style/AndOr + + data = set_values_to_data(login: login, registrar: registrar) + + render_success(data: data) + end + + private + + def authenticate_user + username, password = Base64.urlsafe_decode64(basic_token).split(':') + @current_user ||= ApiUser.find_by(username: username, plain_text_password: password) + + return if @current_user + + raise(ArgumentError) + rescue NoMethodError, ArgumentError + @response = { code: 2202, message: 'Invalid authorization information' } + render(json: @response, status: :unauthorized) + end + + def basic_token + pattern = /^Basic / + header = request.headers['Authorization'] + header = header.gsub(pattern, '') if header&.match(pattern) + header.strip + end + + def set_values_to_data(login:, registrar:) + data = login.as_json(only: %i[id + username + name + uuid + roles + accreditation_date + accreditation_expire_date]) + data[:registrar_name] = registrar.name + data[:registrar_reg_no] = registrar.reg_no + data[:registrar_email] = registrar.email + data[:code] = registrar.code + data + end + + def render_success(code: nil, message: nil, data: nil) + @response = { code: code || 1000, message: message || 'Command completed successfully', + data: data || {} } + + render(json: @response, status: :ok) + end + end + end + end +end diff --git a/app/controllers/api/v1/accreditation_center/base_controller.rb b/app/controllers/api/v1/accreditation_center/base_controller.rb new file mode 100644 index 000000000..7deb776b9 --- /dev/null +++ b/app/controllers/api/v1/accreditation_center/base_controller.rb @@ -0,0 +1,28 @@ +require 'auth_token/auth_token_decryptor' + +module Api + module V1 + module AccreditationCenter + class BaseController < ActionController::API + rescue_from ActiveRecord::RecordNotFound, with: :show_not_found_error + rescue_from ActiveRecord::RecordInvalid, with: :show_invalid_record_error + rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception| + error = {} + error[parameter_missing_exception.param] = ['parameter is required'] + response = { errors: [error] } + render json: response, status: :unprocessable_entity + end + + private + + def show_not_found_error + render json: { errors: [{ base: ['Not found'] }] }, status: :not_found + end + + def show_invalid_record_error(exception) + render json: { errors: exception.record.errors }, status: :bad_request + end + end + end + end +end diff --git a/app/controllers/api/v1/accreditation_center/contacts_controller.rb b/app/controllers/api/v1/accreditation_center/contacts_controller.rb new file mode 100644 index 000000000..c920c1ad2 --- /dev/null +++ b/app/controllers/api/v1/accreditation_center/contacts_controller.rb @@ -0,0 +1,21 @@ +require 'serializers/repp/contact' + +module Api + module V1 + module AccreditationCenter + class ContactsController < ::Api::V1::AccreditationCenter::BaseController + def show + @contact = Contact.find_by(code: params[:id]) + + if @contact + render json: { code: 1000, contact: Serializers::Repp::Contact.new(@contact, + show_address: false).to_json }, + status: :found + else + render json: { errors: 'Contact not found' }, status: :not_found + end + end + end + end + end +end diff --git a/app/controllers/api/v1/accreditation_center/domains_controller.rb b/app/controllers/api/v1/accreditation_center/domains_controller.rb new file mode 100644 index 000000000..80337bd28 --- /dev/null +++ b/app/controllers/api/v1/accreditation_center/domains_controller.rb @@ -0,0 +1,21 @@ +require 'serializers/repp/domain' + +module Api + module V1 + module AccreditationCenter + class DomainsController < ::Api::V1::AccreditationCenter::BaseController + def show + @domain = Domain.find_by(name: params[:name]) + + if @domain + render json: { code: 1000, domain: Serializers::Repp::Domain.new(@domain, + sponsored: true).to_json }, + status: :found + else + render json: { errors: 'Domain not found' }, status: :not_found + end + end + end + end + end +end diff --git a/app/controllers/api/v1/accreditation_center/invoice_status_controller.rb b/app/controllers/api/v1/accreditation_center/invoice_status_controller.rb new file mode 100644 index 000000000..62bf4c741 --- /dev/null +++ b/app/controllers/api/v1/accreditation_center/invoice_status_controller.rb @@ -0,0 +1,32 @@ +module Api + module V1 + module AccreditationCenter + class InvoiceStatusController < ::Api::V1::AccreditationCenter::BaseController + def index + username, password = Base64.urlsafe_decode64(basic_token).split(':') + @current_user ||= ApiUser.find_by(username: username, plain_text_password: password) + + return render json: { errors: 'No user found' }, status: :not_found if @current_user.nil? + + @invoices = @current_user.registrar.invoices.select { |i| i.cancelled_at != nil } + + if @invoices + render json: { code: 1000, invoices: @invoices }, + status: :found + else + render json: { errors: 'No invoices' }, status: :not_found + end + end + + private + + def basic_token + pattern = /^Basic / + header = request.headers['Authorization'] + header = header.gsub(pattern, '') if header&.match(pattern) + header.strip + end + end + end + end +end diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb index c29f2137f..f23d0a24f 100644 --- a/app/controllers/repp/v1/base_controller.rb +++ b/app/controllers/repp/v1/base_controller.rb @@ -116,6 +116,9 @@ module Repp def webclient_request? return if Rails.env.test? + header = request.headers['AccreditationToken'] + return if header == ENV['accreditation_secret'] + ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) end @@ -131,6 +134,10 @@ module Repp render(json: @response, status: :unauthorized) end + + def logger + Rails.logger + end end end end diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index ea00a5561..06d4a0330 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -33,7 +33,7 @@ module Repp param :registrant, String, required: true, desc: 'Registrant contact code' param :reserved_pw, String, required: false, desc: 'Reserved password for domain' param :transfer_code, String, required: false, desc: 'Desired transfer code for domain' - param :period, Integer, required: true, desc: 'Registration period in months or years' + # param :period, String, required: true, desc: 'Registration period in months or years' param :period_unit, String, required: true, desc: 'Period type (month m) or (year y)' param :nameservers_attributes, Array, required: false, desc: 'Domain nameservers' do param :hostname, String, required: true, desc: 'Nameserver hostname' @@ -64,7 +64,7 @@ module Repp handle_errors(@domain) and return unless action.call # rubocop:enable Style/AndOr - render_success(data: { domain: { name: @domain.name } }) + render_success(data: { domain: { name: @domain.name, transfer_code: @domain.transfer_code } }) end api :PUT, '/repp/v1/domains/:domain_name' diff --git a/app/controllers/repp/v1/registrar/accreditation_info_controller.rb b/app/controllers/repp/v1/registrar/accreditation_info_controller.rb new file mode 100644 index 000000000..cd86ce9ed --- /dev/null +++ b/app/controllers/repp/v1/registrar/accreditation_info_controller.rb @@ -0,0 +1,38 @@ +module Repp + module V1 + module Registrar + class AccreditationInfoController < BaseController + api :GET, 'repp/v1/registrar/accreditation/get_info' + desc 'check login user and return data' + + def index + login = current_user + registrar = current_user.registrar + + # rubocop:disable Style/AndOr + render_success(data: nil) and return unless login + # rubocop:enable Style/AndOr + + data = set_values_to_data(login: login, registrar: registrar) + + render_success(data: data) + end + + private + + def set_values_to_data(login:, registrar:) + data = login.as_json(only: %i[id + username + name + uuid + roles + accreditation_date + accreditation_expire_date]) + data[:registrar_name] = registrar.name + data[:registrar_reg_no] = registrar.reg_no + data + end + end + end + end +end diff --git a/app/controllers/repp/v1/registrar/accreditation_results_controller.rb b/app/controllers/repp/v1/registrar/accreditation_results_controller.rb new file mode 100644 index 000000000..b33ed58ee --- /dev/null +++ b/app/controllers/repp/v1/registrar/accreditation_results_controller.rb @@ -0,0 +1,61 @@ +module Repp + module V1 + module Registrar + class AccreditationResultsController < ActionController::API + before_action :authenticate_shared_key + + TEMPORARY_SECRET_KEY = ENV['accreditation_secret'].freeze + + api :POST, 'repp/v1/registrar/accreditation/push_results' + desc 'added datetime results' + + def create + username = params[:accreditation_result][:username] + result = params[:accreditation_result][:result] + + record_accreditation_result(username, result) if result + rescue ActiveRecord::RecordNotFound + record_not_found(username) + end + + private + + def record_accreditation_result(username, result) + user = ApiUser.find_by(username: username) + + raise ActiveRecord::RecordNotFound if user.nil? + + user.accreditation_date = DateTime.current + + return unless user.save + + render_success(data: { user: user, + result: result, + message: 'Accreditation info successfully added' }) + end + + def authenticate_shared_key + api_key = "Basic #{TEMPORARY_SECRET_KEY}" + render_failed unless api_key == request.authorization + end + + def record_not_found(username) + @response = { code: 2303, message: "Object '#{username}' does not exist" } + render(json: @response) + end + + def render_failed + @response = { code: 2202, message: 'Invalid authorization information' } + render(json: @response, status: :unauthorized) + end + + def render_success(code: nil, message: nil, data: nil) + @response = { code: code || 1000, message: message || 'Command completed successfully', + data: data || {} } + + render(json: @response, status: :ok) + end + end + end + end +end diff --git a/app/interactions/actions/domain_create.rb b/app/interactions/actions/domain_create.rb index 2e735bcce..8fd25df0f 100644 --- a/app/interactions/actions/domain_create.rb +++ b/app/interactions/actions/domain_create.rb @@ -106,7 +106,7 @@ module Actions end def assign_domain_period - domain.period = params[:period] + domain.period = params[:period].to_i domain.period_unit = params[:period_unit] end diff --git a/config/application.yml.sample b/config/application.yml.sample index aa86325f1..8133382c9 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -90,6 +90,9 @@ sk_digi_doc_service_name: 'Testimine' registrant_api_base_url: registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas +# Accreditation Center API +accr_center_api_auth_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/routes.rb b/config/routes.rb index 1a3b394d1..4772fbe4b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,8 @@ require_dependency 'epp_constraint' require 'sidekiq/web' Rails.application.routes.draw do + get 'practice/index' + get 'practice/contact' # https://github.com/internetee/epp_proxy#translation-of-epp-calls namespace :epp do constraints(EppConstraint.new(:session)) do @@ -72,6 +74,12 @@ Rails.application.routes.draw do get '/all_notifications', to: 'notifications#all_notifications' end end + resource :accreditation, only: [:index] do + collection do + get '/get_info', to: 'accreditation_info#index' + post '/push_results', to: 'accreditation_results#create' + end + end resources :nameservers do collection do put '/', to: 'nameservers#update' @@ -118,6 +126,16 @@ Rails.application.routes.draw do resources :companies, only: %i[index] end + namespace :accreditation_center do + # At the moment invoice_status endpoint returns only cancelled invoices. But in future logic of this enpoint can change. + # And it will need to return invoices of different statuses. I decided to leave the name of the endpoint "invoice_status" + resources :invoice_status, only: [ :index ] + resource :domains, only: [ :show ], param: :name + resource :contacts, only: [ :show ], param: :id + # resource :auth, only: [ :index ] + get 'auth', to: 'auth#index' + end + resources :auctions, only: %i[index show update], param: :uuid resources :contact_requests, only: %i[create update], param: :id resources :bounces, only: %i[create] diff --git a/db/migrate/20210729131100_add_field_to_user.rb b/db/migrate/20210729131100_add_field_to_user.rb new file mode 100644 index 000000000..38efcea49 --- /dev/null +++ b/db/migrate/20210729131100_add_field_to_user.rb @@ -0,0 +1,6 @@ +class AddFieldToUser < ActiveRecord::Migration[6.1] + def change + add_column :users, :accreditation_date, :datetime + add_column :users, :accreditation_expire_date, :datetime + end +end diff --git a/db/migrate/20210729134625_add_column_to_user.rb b/db/migrate/20210729134625_add_column_to_user.rb new file mode 100644 index 000000000..c2131d3fb --- /dev/null +++ b/db/migrate/20210729134625_add_column_to_user.rb @@ -0,0 +1,5 @@ +class AddColumnToUser < ActiveRecord::Migration[6.1] + def change + add_column :users, :uuid, :uuid, default: 'gen_random_uuid()' + end +end diff --git a/db/structure.sql b/db/structure.sql index fdfacff95..d718d0d33 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2582,7 +2582,10 @@ CREATE TABLE public.users ( remember_created_at timestamp without time zone, failed_attempts integer DEFAULT 0 NOT NULL, locked_at timestamp without time zone, - legacy_id integer + legacy_id integer, + accreditation_date timestamp without time zone, + accreditation_expire_date timestamp without time zone, + uuid uuid DEFAULT public.gen_random_uuid() ); @@ -5230,6 +5233,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20210616112332'), ('20210629074044'), ('20210628090353'), -('20210708131814'); - - +('20210708131814'), +('20210729131100'), +('20210729134625'); diff --git a/test/integration/api/accreditation_center/auth_test.rb b/test/integration/api/accreditation_center/auth_test.rb new file mode 100644 index 000000000..6626762e7 --- /dev/null +++ b/test/integration/api/accreditation_center/auth_test.rb @@ -0,0 +1,33 @@ +require 'test_helper' + +class AuthTest < ApplicationIntegrationTest + def setup + super + + @user = users(:api_bestnames) + @header = { 'Authorization' => "Basic #{generate_base64}" } + end + + def test_should_return_successful + get 'https://registry.test/api/v1/accreditation_center/auth', headers: @header + + json = JSON.parse(response.body, symbolize_names: true) + assert_equal json[:code], 1000 + assert_equal json[:message], 'Command completed successfully' + end + + def test_should_return_failed + get 'https://registry.test/api/v1/accreditation_center/auth', headers: { 'Authorization' => "Basic LAHSDHDSAFSF#@" } + + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal json[:code], 2202 + assert_equal json[:message], 'Invalid authorization information' + end + + private + + def generate_base64 + Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + end +end diff --git a/test/integration/api/accreditation_center/contacts_test.rb b/test/integration/api/accreditation_center/contacts_test.rb new file mode 100644 index 000000000..0770d663e --- /dev/null +++ b/test/integration/api/accreditation_center/contacts_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class ContactsTest < ApplicationIntegrationTest + def setup + super + + @contact = contacts(:john) + end + + def test_return_code_error_if_valid_domain_name + get '/api/v1/accreditation_center/contacts/?id=Alyosha' + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal json[:errors], 'Contact not found' + end + + def test_return_code_error_if_sdfsdf + get "/api/v1/accreditation_center/contacts/?id=#{@contact.code}" + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal json[:contact][:name], 'John' + end +end diff --git a/test/integration/api/accreditation_center/domains_test.rb b/test/integration/api/accreditation_center/domains_test.rb new file mode 100644 index 000000000..1f571ccdc --- /dev/null +++ b/test/integration/api/accreditation_center/domains_test.rb @@ -0,0 +1,21 @@ +require 'test_helper' + +class DomainsTest < ApplicationIntegrationTest + def setup + @domain = domains(:shop) + end + + def test_get_domain_info + get "/api/v1/accreditation_center/domains/?name=shop.test" + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal json[:domain][:name], "shop.test" + end + + def test_return_code_error_if_valid_domain_name + get "/api/v1/accreditation_center/domains/?name=some.ee" + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal json[:errors], "Domain not found" + end +end \ No newline at end of file diff --git a/test/integration/api/accreditation_center/invoice_status_test.rb b/test/integration/api/accreditation_center/invoice_status_test.rb new file mode 100644 index 000000000..d97a3b825 --- /dev/null +++ b/test/integration/api/accreditation_center/invoice_status_test.rb @@ -0,0 +1,33 @@ +require 'test_helper' + +class DomainsTest < ApplicationIntegrationTest + def setup + super + + @user = users(:api_bestnames) + @header = { 'Authorization' => "Basic #{generate_base64}" } + end + + def test_should_return_cancelled_invoices + date_now = Time.now + + get "/api/v1/accreditation_center/invoice_status", headers: @header + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal json[:invoices].count, 0 + + invoice = @user.registrar.invoices.last + invoice.update(cancelled_at: date_now) + + get "/api/v1/accreditation_center/invoice_status", headers: @header + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal json[:invoices].count, 1 + end + + private + + def generate_base64 + Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + end +end \ No newline at end of file diff --git a/test/integration/repp/v1/registrar/accreditaion_info_test.rb b/test/integration/repp/v1/registrar/accreditaion_info_test.rb new file mode 100644 index 000000000..4efba5d38 --- /dev/null +++ b/test/integration/repp/v1/registrar/accreditaion_info_test.rb @@ -0,0 +1,35 @@ +require 'test_helper' + +class ReppV1AccreditationInfoTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_valid_login + get '/repp/v1/registrar/accreditation/get_info', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal json[:data][:username], @user.username + assert json[:data][:roles].include? 'super' + assert_equal json[:data][:registrar_name], 'Best Names' + assert_equal json[:data][:registrar_reg_no], '1234' + end + + def test_invalid_login + token = Base64.encode64("#{@user.username}:0066600") + token = "Basic #{token}" + + auth_headers = { 'Authorization' => token } + + get '/repp/v1/registrar/accreditation/get_info', headers: auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal json[:message], 'Invalid authorization information' + end +end diff --git a/test/integration/repp/v1/registrar/accreditation_results_test.rb b/test/integration/repp/v1/registrar/accreditation_results_test.rb new file mode 100644 index 000000000..ac9d4fa4a --- /dev/null +++ b/test/integration/repp/v1/registrar/accreditation_results_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' + +class ReppV1AccreditationResultsTest < ActionDispatch::IntegrationTest + TEMPORARY_SECRET_KEY = ENV['accreditation_secret'].freeze + + def setup + @user = users(:api_bestnames) + + token = "Basic #{TEMPORARY_SECRET_KEY}" + + @auth_headers = { 'Authorization' => token } + end + + def test_should_return_valid_response + post '/repp/v1/registrar/accreditation/push_results', + headers: @auth_headers, + params: {accreditation_result: {username: @user.username, result: true} } + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal json[:data][:user][:username], @user.username + assert_equal json[:data][:result], "true" + assert_equal json[:data][:message], "Accreditation info successfully added" + end + + def test_should_return_valid_response_invalid_authorization + post '/repp/v1/registrar/accreditation/push_results', + headers: { 'Authorization' => 'Basic temporary-secret-ke'}, + params: {accreditation_result: {username: @user.username, result: true} } + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + + assert_equal json[:code], 2202 + assert_equal json[:message], 'Invalid authorization information' + end + + def test_should_return_valid_response_record_exception + post '/repp/v1/registrar/accreditation/push_results', + headers: @auth_headers, + params: {accreditation_result: { username: "chungachanga", result: true} } + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal json[:code], 2303 + assert_equal json[:message], "Object 'chungachanga' does not exist" + end +end