From 0ab9f6333fb53eb60f170abd7399d27448b98450 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Thu, 26 Jul 2018 14:46:03 +0300 Subject: [PATCH 1/6] Add API/Registrant/Domains route --- .../api/v1/registrant/domains_controller.rb | 17 +++++++++++++++++ config/routes.rb | 8 ++++++++ test/fixtures/domains.yml | 12 +++++++++++- .../registrant/registrant_api_domains_test.rb | 17 +++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/registrant/domains_controller.rb create mode 100644 test/system/api/registrant/registrant_api_domains_test.rb diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb new file mode 100644 index 000000000..44662c673 --- /dev/null +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -0,0 +1,17 @@ +require 'rails5_api_controller_backport' + +module Api + module V1 + module Registrant + class DomainsController < ActionController::API + def index + render json: { success: true } + end + + def show + render json: { success: true } + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 8f50d5587..aa73eef3f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,14 @@ Rails.application.routes.draw do mount Repp::API => '/' + namespace :api do + namespace :v1 do + namespace :registrant do + resources :domains, only: [:index, :show], param: :uuid + end + end + end + # REGISTRAR ROUTES namespace :registrar do resource :dashboard diff --git a/test/fixtures/domains.yml b/test/fixtures/domains.yml index 59a1b8ea5..38500e9cc 100644 --- a/test/fixtures/domains.yml +++ b/test/fixtures/domains.yml @@ -42,10 +42,20 @@ metro: period_unit: m uuid: ef97cb80-333b-4893-b9df-163f2b452798 +hospital: + name: hospital.test + registrar: goodnames + registrant: william + transfer_code: 23118v2 + valid_to: 2010-07-05 + period: 1 + period_unit: m + uuid: 5edda1a5-3548-41ee-8b65-6d60daf85a37 + invalid: name: invalid.test transfer_code: 1438d6 valid_to: <%= Time.zone.parse('2010-07-05').utc.to_s(:db) %> registrar: bestnames registrant: invalid - uuid: 3c430ead-bb17-4b5b-aaa1-caa7dde7e138 \ No newline at end of file + uuid: 3c430ead-bb17-4b5b-aaa1-caa7dde7e138 diff --git a/test/system/api/registrant/registrant_api_domains_test.rb b/test/system/api/registrant/registrant_api_domains_test.rb new file mode 100644 index 000000000..6a53f7720 --- /dev/null +++ b/test/system/api/registrant/registrant_api_domains_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' + +class RegistrantApiDomainsTest < ApplicationSystemTestCase + def setup + super + + @registrant = contacts(:william) + end + + def teardown + super + end + + + def test_can_get_domain_details_by_uuid + end +end From b33ad0d4e892bc6273f4c0b3b0632eb439b5bace Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Fri, 27 Jul 2018 11:00:35 +0300 Subject: [PATCH 2/6] Add show action --- .../api/v1/registrant/domains_controller.rb | 11 ++++++---- config/application.rb | 2 +- test/fixtures/domains.yml | 2 +- .../registrant/registrant_api_domains_test.rb | 21 +++++++++++++++++-- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb index 44662c673..be755b55c 100644 --- a/app/controllers/api/v1/registrant/domains_controller.rb +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -4,12 +4,15 @@ module Api module V1 module Registrant class DomainsController < ActionController::API - def index - render json: { success: true } - end def show - render json: { success: true } + @domain = Domain.find_by(uuid: params[:uuid]) + + if @domain + render json: @domain + else + render json: { errors: ["Domain not found"] }, status: :not_found + end end end end diff --git a/config/application.rb b/config/application.rb index 400e72124..1420d3cd3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -36,7 +36,7 @@ module DomainNameRegistry config.i18n.default_locale = :en config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') - config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] + # config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] # Autoload all model subdirs config.autoload_paths += Dir[Rails.root.join('app', 'models', '**/')] diff --git a/test/fixtures/domains.yml b/test/fixtures/domains.yml index 38500e9cc..4d6468c92 100644 --- a/test/fixtures/domains.yml +++ b/test/fixtures/domains.yml @@ -45,7 +45,7 @@ metro: hospital: name: hospital.test registrar: goodnames - registrant: william + registrant: john transfer_code: 23118v2 valid_to: 2010-07-05 period: 1 diff --git a/test/system/api/registrant/registrant_api_domains_test.rb b/test/system/api/registrant/registrant_api_domains_test.rb index 6a53f7720..36a9def35 100644 --- a/test/system/api/registrant/registrant_api_domains_test.rb +++ b/test/system/api/registrant/registrant_api_domains_test.rb @@ -4,14 +4,31 @@ class RegistrantApiDomainsTest < ApplicationSystemTestCase def setup super - @registrant = contacts(:william) + @domain = domains(:hospital) + @registrant = @domain.registrant end def teardown super end + def test_get_domain_details_by_uuid + get '/api/v1/registrant/domains/5edda1a5-3548-41ee-8b65-6d60daf85a37' + assert_equal(200, response.status) - def test_can_get_domain_details_by_uuid + domain = JSON.parse(response.body, symbolize_names: true) + assert_equal('hospital.test', domain[:name]) + end + + def test_get_non_existent_domain_details_by_uuid + get '/api/v1/registrant/domains/random-uuid' + assert_equal(404, response.status) + + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal({errors: ['Domain not found']}, response_json) + end + + def test_get_non_registrar_domain_details_by_uuid + # no op end end From 13562aeb0687040b3464718e7f70b7bfb8e36ee6 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Mon, 30 Jul 2018 13:56:54 +0300 Subject: [PATCH 3/6] Move file to another folder --- .../registrant/registrant_api_domains_test.rb | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) rename test/{system => integration}/api/registrant/registrant_api_domains_test.rb (51%) diff --git a/test/system/api/registrant/registrant_api_domains_test.rb b/test/integration/api/registrant/registrant_api_domains_test.rb similarity index 51% rename from test/system/api/registrant/registrant_api_domains_test.rb rename to test/integration/api/registrant/registrant_api_domains_test.rb index 36a9def35..bcf11cfc0 100644 --- a/test/system/api/registrant/registrant_api_domains_test.rb +++ b/test/integration/api/registrant/registrant_api_domains_test.rb @@ -1,6 +1,7 @@ require 'test_helper' +require 'auth_token/auth_token_creator' -class RegistrantApiDomainsTest < ApplicationSystemTestCase +class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest def setup super @@ -28,7 +29,24 @@ class RegistrantApiDomainsTest < ApplicationSystemTestCase assert_equal({errors: ['Domain not found']}, response_json) end - def test_get_non_registrar_domain_details_by_uuid - # no op + def test_root_returns_domain_list + get '/api/v1/registrant/domains', {}, @auth_headers + assert_equal(200, response.status) + end + + def test_root_returns_401_without_authorization + get '/api/v1/registrant/domains', {}, {} + assert_equal(401, response.status) + json_body = JSON.parse(response.body, symbolize_names: true) + + assert_equal({ errors: ['Not authorized'] }, json_body) + end + + private + + def auth_token + token_creator = AuthTokenCreator.create_with_defaults(@user) + hash = token_creator.token_in_hash + "Bearer #{hash[:access_token]}" end end From 10d42a0d74f250d019ed22fb99631196ad065f82 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Mon, 30 Jul 2018 15:09:48 +0300 Subject: [PATCH 4/6] Add domain index action (without pagination yet) --- .../api/v1/registrant/base_controller.rb | 38 ++++++++++++++++ .../api/v1/registrant/domains_controller.rb | 20 ++++++++- lib/auth_token/auth_token_creator.rb | 41 ++++++++++++++++++ lib/auth_token/auth_token_decryptor.rb | 43 +++++++++++++++++++ .../registrant/registrant_api_domains_test.rb | 17 +++++++- 5 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 app/controllers/api/v1/registrant/base_controller.rb create mode 100644 lib/auth_token/auth_token_creator.rb create mode 100644 lib/auth_token/auth_token_decryptor.rb diff --git a/app/controllers/api/v1/registrant/base_controller.rb b/app/controllers/api/v1/registrant/base_controller.rb new file mode 100644 index 000000000..bc5fa21d7 --- /dev/null +++ b/app/controllers/api/v1/registrant/base_controller.rb @@ -0,0 +1,38 @@ +require 'rails5_api_controller_backport' +require 'auth_token/auth_token_decryptor' + +module Api + module V1 + module Registrant + class BaseController < ActionController::API + before_action :authenticate + + 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 bearer_token + pattern = /^Bearer / + header = request.headers['Authorization'] + header.gsub(pattern, '') if header&.match(pattern) + end + + def authenticate + decryptor = AuthTokenDecryptor.create_with_defaults(bearer_token) + decryptor.decrypt_token + + if decryptor.valid? + sign_in decryptor.user + else + render json: { errors: ['Not authorized'] }, status: :unauthorized + end + end + end + end + end +end diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb index be755b55c..96d767d38 100644 --- a/app/controllers/api/v1/registrant/domains_controller.rb +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -3,10 +3,15 @@ require 'rails5_api_controller_backport' module Api module V1 module Registrant - class DomainsController < ActionController::API + class DomainsController < BaseController + def index + @domains = associated_domains(current_user) + render json: @domains + end def show - @domain = Domain.find_by(uuid: params[:uuid]) + domain_pool = associated_domains(current_user) + @domain = domain_pool.find_by(uuid: params[:uuid]) if @domain render json: @domain @@ -14,6 +19,17 @@ module Api render json: { errors: ["Domain not found"] }, status: :not_found end end + + private + + def associated_domains(user) + country_code, ident = user.registrant_ident.split('-') + + BusinessRegistryCache.fetch_associated_domains(ident, country_code) + rescue Soap::Arireg::NotAvailableError => error + Rails.logger.fatal("[EXCEPTION] #{error.to_s}") + user.domains + end end end end diff --git a/lib/auth_token/auth_token_creator.rb b/lib/auth_token/auth_token_creator.rb new file mode 100644 index 000000000..9fff8e5cd --- /dev/null +++ b/lib/auth_token/auth_token_creator.rb @@ -0,0 +1,41 @@ +class AuthTokenCreator + DEFAULT_VALIDITY = 2.hours + + attr_reader :user + attr_reader :key + attr_reader :expires_at + + def self.create_with_defaults(user) + new(user, Rails.application.config.secret_key_base, Time.now + DEFAULT_VALIDITY) + end + + def initialize(user, key, expires_at) + @user = user + @key = key + @expires_at = expires_at.utc.strftime('%F %T %Z') + end + + def hashable + { + user_ident: user.registrant_ident, + user_username: user.username, + expires_at: expires_at, + }.to_json + end + + def encrypted_token + encryptor = OpenSSL::Cipher::AES.new(256, :CBC) + encryptor.encrypt + encryptor.key = key + encrypted_bytes = encryptor.update(hashable) + encryptor.final + Base64.urlsafe_encode64(encrypted_bytes) + end + + def token_in_hash + { + access_token: encrypted_token, + expires_at: expires_at, + type: 'Bearer', + } + end +end diff --git a/lib/auth_token/auth_token_decryptor.rb b/lib/auth_token/auth_token_decryptor.rb new file mode 100644 index 000000000..be6bd99cd --- /dev/null +++ b/lib/auth_token/auth_token_decryptor.rb @@ -0,0 +1,43 @@ +class AuthTokenDecryptor + attr_reader :decrypted_data + attr_reader :token + attr_reader :key + attr_reader :user + + def self.create_with_defaults(token) + new(token, Rails.application.config.secret_key_base) + end + + def initialize(token, key) + @token = token + @key = key + end + + def decrypt_token + decipher = OpenSSL::Cipher::AES.new(256, :CBC) + decipher.decrypt + decipher.key = key + + base64_decoded = Base64.urlsafe_decode64(token.to_s) + plain = decipher.update(base64_decoded) + decipher.final + + @decrypted_data = JSON.parse(plain, symbolize_names: true) + rescue OpenSSL::Cipher::CipherError, ArgumentError + false + end + + def valid? + decrypted_data && valid_user? && still_valid? + end + + private + + def valid_user? + @user = RegistrantUser.find_by(registrant_ident: decrypted_data[:user_ident]) + @user&.username == decrypted_data[:user_username] + end + + def still_valid? + decrypted_data[:expires_at] > Time.now + end +end diff --git a/test/integration/api/registrant/registrant_api_domains_test.rb b/test/integration/api/registrant/registrant_api_domains_test.rb index bcf11cfc0..c892f088b 100644 --- a/test/integration/api/registrant/registrant_api_domains_test.rb +++ b/test/integration/api/registrant/registrant_api_domains_test.rb @@ -5,16 +5,25 @@ class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest def setup super + @original_registry_time = Setting.days_to_keep_business_registry_cache + Setting.days_to_keep_business_registry_cache = 1 + travel_to Time.zone.parse('2010-07-05') + @domain = domains(:hospital) @registrant = @domain.registrant + @user = users(:registrant) + @auth_headers = { 'HTTP_AUTHORIZATION' => auth_token } end def teardown super + + Setting.days_to_keep_business_registry_cache = @original_registry_time + travel_back end def test_get_domain_details_by_uuid - get '/api/v1/registrant/domains/5edda1a5-3548-41ee-8b65-6d60daf85a37' + get '/api/v1/registrant/domains/5edda1a5-3548-41ee-8b65-6d60daf85a37', {}, @auth_headers assert_equal(200, response.status) domain = JSON.parse(response.body, symbolize_names: true) @@ -22,7 +31,7 @@ class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest end def test_get_non_existent_domain_details_by_uuid - get '/api/v1/registrant/domains/random-uuid' + get '/api/v1/registrant/domains/random-uuid', {}, @auth_headers assert_equal(404, response.status) response_json = JSON.parse(response.body, symbolize_names: true) @@ -32,6 +41,10 @@ class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest def test_root_returns_domain_list get '/api/v1/registrant/domains', {}, @auth_headers assert_equal(200, response.status) + + response_json = JSON.parse(response.body, symbolize_names: true) + array_of_domain_names = response_json.map { |x| x[:name] } + assert(array_of_domain_names.include?('hospital.test')) end def test_root_returns_401_without_authorization From c476680a91fe24e534ccbd35b0f0832032e96da2 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Tue, 31 Jul 2018 15:14:21 +0300 Subject: [PATCH 5/6] Add pagination to index --- .../api/v1/registrant/domains_controller.rb | 19 ++++++++-- .../registrant/registrant_api_domains_test.rb | 37 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb index 96d767d38..1e21f6770 100644 --- a/app/controllers/api/v1/registrant/domains_controller.rb +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -5,7 +5,20 @@ module Api module Registrant class DomainsController < BaseController def index - @domains = associated_domains(current_user) + limit = params[:limit] || 200 + offset = params[:offset] || 0 + + if limit.to_i > 200 || limit.to_i < 1 + render(json: { errors: [{ limit: ['parameter is out of range'] }] }, + status: :bad_request) && return + end + + if offset.to_i.negative? + render(json: { errors: [{ offset: ['parameter is out of range'] }] }, + status: :bad_request) && return + end + + @domains = associated_domains(current_user).limit(limit).offset(offset) render json: @domains end @@ -16,7 +29,7 @@ module Api if @domain render json: @domain else - render json: { errors: ["Domain not found"] }, status: :not_found + render json: { errors: ['Domain not found'] }, status: :not_found end end @@ -27,7 +40,7 @@ module Api BusinessRegistryCache.fetch_associated_domains(ident, country_code) rescue Soap::Arireg::NotAvailableError => error - Rails.logger.fatal("[EXCEPTION] #{error.to_s}") + Rails.logger.fatal("[EXCEPTION] #{error}") user.domains end end diff --git a/test/integration/api/registrant/registrant_api_domains_test.rb b/test/integration/api/registrant/registrant_api_domains_test.rb index c892f088b..3e966b192 100644 --- a/test/integration/api/registrant/registrant_api_domains_test.rb +++ b/test/integration/api/registrant/registrant_api_domains_test.rb @@ -47,6 +47,35 @@ class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest assert(array_of_domain_names.include?('hospital.test')) end + def test_root_accepts_limit_and_offset_parameters + get '/api/v1/registrant/domains', { 'limit' => 2, 'offset' => 0 }, @auth_headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_equal(200, response.status) + assert_equal(2, response_json.count) + + get '/api/v1/registrant/domains', {}, @auth_headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_equal(5, response_json.count) + end + + def test_root_does_not_accept_limit_higher_than_200 + get '/api/v1/registrant/domains', { 'limit' => 400, 'offset' => 0 }, @auth_headers + + assert_equal(400, response.status) + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal({ errors: [{ limit: ['parameter is out of range'] }] }, response_json) + end + + def test_root_does_not_accept_offset_lower_than_0 + get '/api/v1/registrant/domains', { 'limit' => 200, 'offset' => "-10" }, @auth_headers + + assert_equal(400, response.status) + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal({ errors: [{ offset: ['parameter is out of range'] }] }, response_json) + end + def test_root_returns_401_without_authorization get '/api/v1/registrant/domains', {}, {} assert_equal(401, response.status) @@ -55,6 +84,14 @@ class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest assert_equal({ errors: ['Not authorized'] }, json_body) end + def test_details_returns_401_without_authorization + get '/api/v1/registrant/domains/5edda1a5-3548-41ee-8b65-6d60daf85a37', {}, {} + assert_equal(401, response.status) + json_body = JSON.parse(response.body, symbolize_names: true) + + assert_equal({ errors: ['Not authorized'] }, json_body) + end + private def auth_token From abb1ad60e890b4a08a0bc063a362fa204ba89a7b Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Tue, 7 Aug 2018 14:56:19 +0300 Subject: [PATCH 6/6] Update failing test --- app/controllers/api/v1/registrant/domains_controller.rb | 2 +- .../integration/api/registrant/registrant_api_domains_test.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb index 1e21f6770..27b7b6125 100644 --- a/app/controllers/api/v1/registrant/domains_controller.rb +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -29,7 +29,7 @@ module Api if @domain render json: @domain else - render json: { errors: ['Domain not found'] }, status: :not_found + render json: { errors: [{ base: ['Domain not found'] }] }, status: :not_found end end diff --git a/test/integration/api/registrant/registrant_api_domains_test.rb b/test/integration/api/registrant/registrant_api_domains_test.rb index 1c77131c4..e297c921c 100644 --- a/test/integration/api/registrant/registrant_api_domains_test.rb +++ b/test/integration/api/registrant/registrant_api_domains_test.rb @@ -35,7 +35,7 @@ class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest assert_equal(404, response.status) response_json = JSON.parse(response.body, symbolize_names: true) - assert_equal({errors: ['Domain not found']}, response_json) + assert_equal({ errors: [base: ['Domain not found']] }, response_json) end def test_root_returns_domain_list @@ -89,7 +89,7 @@ class RegistrantApiDomainsTest < ActionDispatch::IntegrationTest assert_equal(401, response.status) json_body = JSON.parse(response.body, symbolize_names: true) - assert_equal({ errors: ['Not authorized'] }, json_body) + assert_equal({ errors: [base: ['Not authorized']] }, json_body) end private