Merge remote-tracking branch 'origin/master' into repp-domains

This commit is contained in:
Karl Erik Õunapuu 2021-02-11 10:29:01 +02:00
commit 42adaa7159
No known key found for this signature in database
GPG key ID: C9DD647298A34764
41 changed files with 803 additions and 143 deletions

View file

@ -17,8 +17,10 @@ plugins:
config: config:
count_threshold: 3 count_threshold: 3
languages: languages:
- ruby ruby:
- javascript mass_threshold: 100
javascript:
mass_threshold: 100
eslint: eslint:
enabled: true enabled: true
channel: eslint-5 channel: eslint-5

View file

@ -1,3 +1,17 @@
10.02.2021
* Admin contact bulk change option for registrars [#1764](https://github.com/internetee/registry/issues/1764)
* Option to remove email addresses from AWS SES Supression list [#1839](https://github.com/internetee/registry/issues/1839)
* Added separate key for bounce API [#1842](https://github.com/internetee/registry/pull/1842)
09.02.2021
* Added new endpoint for WHOIS contact requests [#1794](https://github.com/internetee/registry/pull/1794)
05.02.2021
* Fixed IPv4 empty string issue in case of IPv6 only entries for IP whitelist [#1833](https://github.com/internetee/registry/issues/1833)
02.02.2021
* Fixed updateProhibited status not affecting bulk tech contact change operation [#1820](https://github.com/internetee/registry/pull/1820)
01.02.2021 01.02.2021
* Improved tests for admin interface [#1805](https://github.com/internetee/registry/pull/1805) * Improved tests for admin interface [#1805](https://github.com/internetee/registry/pull/1805)

View file

@ -92,3 +92,5 @@ group :test do
gem 'webdrivers' gem 'webdrivers'
gem 'webmock' gem 'webmock'
end end
gem 'aws-sdk-sesv2', '~> 1.16'

View file

@ -150,6 +150,18 @@ GEM
attr_required (1.0.1) attr_required (1.0.1)
autoprefixer-rails (10.0.0.2) autoprefixer-rails (10.0.0.2)
execjs execjs
aws-eventstream (1.1.0)
aws-partitions (1.424.0)
aws-sdk-core (3.112.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-sesv2 (1.16.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.16) bcrypt (3.1.16)
bindata (2.4.8) bindata (2.4.8)
bootsnap (1.4.8) bootsnap (1.4.8)
@ -228,6 +240,7 @@ GEM
i18n_data (0.10.0) i18n_data (0.10.0)
isikukood (0.1.2) isikukood (0.1.2)
iso8601 (0.12.1) iso8601 (0.12.1)
jmespath (1.4.0)
jquery-rails (4.4.0) jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
@ -488,6 +501,7 @@ DEPENDENCIES
activerecord-import activerecord-import
airbrake airbrake
apipie-rails (~> 0.5.18) apipie-rails (~> 0.5.18)
aws-sdk-sesv2 (~> 1.16)
bootsnap (>= 1.1.0) bootsnap (>= 1.1.0)
bootstrap-sass (~> 3.4) bootstrap-sass (~> 3.4)
cancancan cancancan

View file

@ -11,7 +11,7 @@ module Api
end end
def authenticate_shared_key def authenticate_shared_key
api_key = "Basic #{ENV['api_shared_key']}" api_key = "Basic #{ENV['rwhois_internal_api_shared_key']}"
head(:unauthorized) unless api_key == request.authorization head(:unauthorized) unless api_key == request.authorization
end end

View file

@ -1,7 +1,7 @@
module Api module Api
module V1 module V1
class BouncesController < BaseController class BouncesController < BaseController
before_action :authenticate_shared_key before_action :validate_shared_key_integrity
# POST api/v1/bounces/ # POST api/v1/bounces/
def create def create
@ -20,6 +20,13 @@ module Api
params.require(:data) params.require(:data)
end end
private
def validate_shared_key_integrity
api_key = "Basic #{ENV['rwhois_bounces_api_shared_key']}"
head(:unauthorized) unless api_key == request.authorization
end
end end
end end
end end

View file

@ -0,0 +1,37 @@
module Api
module V1
class ContactRequestsController < BaseController
before_action :authenticate_shared_key
# POST api/v1/contact_requests/
def create
return head(:bad_request) if contact_request_params[:email].blank?
contact_request = ContactRequest.save_record(contact_request_params)
render json: contact_request, status: :created
rescue StandardError
head(:bad_request)
end
def update
return head(:bad_request) if params[:id].blank?
process_id(params[:id])
end
def process_id(id)
record = ContactRequest.find_by(id: id)
return :not_found unless record
record.update_status(contact_request_params)
render json: record, status: :ok
rescue StandardError
head :bad_request
end
def contact_request_params
params.require(:contact_request).permit(:email, :whois_record_id, :name, :status, :ip)
end
end
end
end

View file

@ -0,0 +1,18 @@
class Registrar
class AdminContactsController < BulkChangeController
BASE_URL = URI.parse("#{ENV['repp_url']}domains/admin_contacts").freeze
ACTIVE_TAB = :admin_contact
def update
authorize! :manage, :repp
uri = BASE_URL
request = form_request(uri)
response = do_request(request, uri)
start_notice = t('.replaced')
process_response(response: response,
start_notice: start_notice,
active_tab: ACTIVE_TAB)
end
end
end

View file

@ -26,6 +26,84 @@ class Registrar
private private
def form_request(uri)
request = Net::HTTP::Patch.new(uri)
request.set_form_data(current_contact_id: params[:current_contact_id],
new_contact_id: params[:new_contact_id])
request.basic_auth(current_registrar_user.username,
current_registrar_user.plain_text_password)
request
end
def process_response(response:, start_notice: '', active_tab:)
parsed_response = JSON.parse(response.body, symbolize_names: true)
if response.code == '200'
notices = success_notices(parsed_response, start_notice)
flash[:notice] = notices.join(', ')
redirect_to registrar_domains_url
else
@error = response.code == '404' ? 'Contact(s) not found' : parsed_response[:message]
render file: 'registrar/bulk_change/new', locals: { active_tab: active_tab }
end
end
def success_notices(parsed_response, start_notice)
notices = [start_notice]
notices << "#{t('.affected_domains')}: " \
"#{parsed_response[:data][:affected_domains].join(', ')}"
if parsed_response[:data][:skipped_domains]
notices << "#{t('.skipped_domains')}: " \
"#{parsed_response[:data][:skipped_domains].join(', ')}"
end
notices
end
def do_request(request, uri)
response = if Rails.env.test?
do_test_request(request, uri)
elsif Rails.env.development?
do_dev_request(request, uri)
else
do_live_request(request, uri)
end
response
end
def do_live_request(request, uri)
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
end
def do_dev_request(request, uri)
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE,
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
end
def do_test_request(request, uri)
Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
http.request(request)
end
end
def ready_to_renew? def ready_to_renew?
domain_ids_for_bulk_renew.present? && params[:renew].present? domain_ids_for_bulk_renew.present? && params[:renew].present?
end end

View file

@ -25,32 +25,7 @@ class Registrar
current_registrar_user.plain_text_password) current_registrar_user.plain_text_password)
if Rails.env.test? response = do_request(request, uri)
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
http.request(request)
end
elsif Rails.env.development?
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE,
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
else
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
end
parsed_response = JSON.parse(response.body, symbolize_names: true) parsed_response = JSON.parse(response.body, symbolize_names: true)

View file

@ -18,32 +18,7 @@ class Registrar
request.basic_auth(current_registrar_user.username, request.basic_auth(current_registrar_user.username,
current_registrar_user.plain_text_password) current_registrar_user.plain_text_password)
if Rails.env.test? response = do_request(request, uri)
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
http.request(request)
end
elsif Rails.env.development?
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE,
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
else
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
end
parsed_response = JSON.parse(response.body, symbolize_names: true) parsed_response = JSON.parse(response.body, symbolize_names: true)

View file

@ -1,62 +1,19 @@
class Registrar class Registrar
class TechContactsController < BulkChangeController class TechContactsController < BulkChangeController
BASE_URL = URI.parse("#{ENV['repp_url']}domains/contacts").freeze
ACTIVE_TAB = :technical_contact
def update def update
authorize! :manage, :repp authorize! :manage, :repp
uri = URI.parse("#{ENV['repp_url']}domains/contacts") uri = BASE_URL
request = form_request(uri)
response = do_request(request, uri)
start_notice = t('.replaced')
request = Net::HTTP::Patch.new(uri) process_response(response: response,
request.set_form_data(current_contact_id: params[:current_contact_id], start_notice: start_notice,
new_contact_id: params[:new_contact_id]) active_tab: ACTIVE_TAB)
request.basic_auth(current_registrar_user.username,
current_registrar_user.plain_text_password)
if Rails.env.test?
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
http.request(request)
end
elsif Rails.env.development?
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE,
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
else
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
end
parsed_response = JSON.parse(response.body, symbolize_names: true)
if response.code == '200'
notices = [t('.replaced')]
notices << "#{t('.affected_domains')}: " \
"#{parsed_response[:data][:affected_domains].join(', ')}"
if parsed_response[:data][:skipped_domains]
notices << "#{t('.skipped_domains')}: " \
"#{parsed_response[:data][:skipped_domains].join(', ')}"
end
flash[:notice] = notices.join(', ')
redirect_to registrar_domains_url
else
@error = response.code == '404' ? 'Contact(s) not found' : parsed_response[:message]
render file: 'registrar/bulk_change/new', locals: { active_tab: :technical_contact }
end
end end
end end
end end

View file

@ -0,0 +1,21 @@
module Repp
module V1
module Domains
class AdminContactsController < BaseContactsController
def update
super
unless @new_contact.identical_to?(@current_contact)
@epp_errors << { code: 2304, msg: 'Admin contacts must be identical' }
end
return handle_errors if @epp_errors.any?
affected, skipped = AdminDomainContact.replace(@current_contact, @new_contact)
@response = { affected_domains: affected, skipped_domains: skipped }
render_success(data: @response)
end
end
end
end
end

View file

@ -0,0 +1,31 @@
module Repp
module V1
module Domains
class BaseContactsController < BaseController
before_action :set_current_contact, only: [:update]
before_action :set_new_contact, only: [:update]
def set_current_contact
@current_contact = current_user.registrar.contacts
.find_by!(code: contact_params[:current_contact_id])
end
def set_new_contact
@new_contact = current_user.registrar.contacts.find_by!(code: params[:new_contact_id])
end
def update
@epp_errors ||= []
@epp_errors << { code: 2304, msg: 'New contact must be valid' } if @new_contact.invalid?
end
private
def contact_params
params.require(%i[current_contact_id new_contact_id])
params.permit(:current_contact_id, :new_contact_id)
end
end
end
end
end

View file

@ -1,9 +1,7 @@
module Repp module Repp
module V1 module V1
module Domains module Domains
class ContactsController < BaseController class ContactsController < BaseContactsController
before_action :set_current_contact, only: [:update]
before_action :set_new_contact, only: [:update]
before_action :set_domain, only: %i[index create destroy] before_action :set_domain, only: %i[index create destroy]
def_param_group :contacts_apidoc do def_param_group :contacts_apidoc do
@ -48,19 +46,8 @@ module Repp
render_success(data: { domain: { name: @domain.name } }) render_success(data: { domain: { name: @domain.name } })
end end
def set_current_contact
@current_contact = current_user.registrar.contacts.find_by!(
code: contact_params[:current_contact_id]
)
end
def set_new_contact
@new_contact = current_user.registrar.contacts.find_by!(code: params[:new_contact_id])
end
def update def update
@epp_errors ||= [] super
@epp_errors << { code: 2304, msg: 'New contact must be valid' } if @new_contact.invalid?
if @new_contact == @current_contact if @new_contact == @current_contact
@epp_errors << { code: 2304, msg: 'New contact must be different from current' } @epp_errors << { code: 2304, msg: 'New contact must be different from current' }
@ -78,11 +65,6 @@ module Repp
def contact_create_params def contact_create_params
params.permit(:domain_id, contacts: [%i[action code type]]) params.permit(:domain_id, contacts: [%i[action code type]])
end end
def contact_params
params.require(%i[current_contact_id new_contact_id])
params.permit(:current_contact_id, :new_contact_id)
end
end end
end end
end end

View file

@ -1,2 +1,26 @@
class AdminDomainContact < DomainContact class AdminDomainContact < DomainContact
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def self.replace(current_contact, new_contact)
affected_domains = []
skipped_domains = []
admin_contacts = where(contact: current_contact)
admin_contacts.each do |admin_contact|
if admin_contact.domain.bulk_update_prohibited?
skipped_domains << admin_contact.domain.name
next
end
begin
admin_contact.contact = new_contact
admin_contact.save!
affected_domains << admin_contact.domain.name
rescue ActiveRecord::RecordNotUnique
skipped_domains << admin_contact.domain.name
end
end
[affected_domains.sort, skipped_domains.sort]
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end end

View file

@ -1,5 +1,6 @@
class BouncedMailAddress < ApplicationRecord class BouncedMailAddress < ApplicationRecord
validates :email, :message_id, :bounce_type, :bounce_subtype, :action, :status, presence: true validates :email, :message_id, :bounce_type, :bounce_subtype, :action, :status, presence: true
after_destroy :destroy_aws_suppression
def bounce_reason def bounce_reason
"#{action} (#{status} #{diagnostic})" "#{action} (#{status} #{diagnostic})"
@ -25,4 +26,20 @@ class BouncedMailAddress < ApplicationRecord
diagnostic: bounced_record['diagnosticCode'], diagnostic: bounced_record['diagnosticCode'],
} }
end end
def destroy_aws_suppression
return unless BouncedMailAddress.ses_configured?
res = Aws::SESV2::Client.new.delete_suppressed_destination(email_address: email)
res.successful?
rescue Aws::SESV2::Errors::ServiceError => e
logger.warn("Suppression not removed. #{e}")
end
def self.ses_configured?
ses ||= Aws::SESV2::Client.new
ses.config.credentials.access_key_id.present?
rescue Aws::Errors::MissingRegionError
false
end
end end

View file

@ -11,6 +11,13 @@ module Concerns::Contact::Identical
ident_country_code ident_country_code
org_name org_name
] ]
IDENTICAL_ATTRIBUTES = %w[
ident
ident_type
ident_country_code
].freeze
private_constant :IDENTIFIABLE_ATTRIBUTES private_constant :IDENTIFIABLE_ATTRIBUTES
def identical(registrar) def identical(registrar)
@ -20,6 +27,12 @@ module Concerns::Contact::Identical
.where.not(id: id).take .where.not(id: id).take
end end
def identical_to?(contact)
IDENTICAL_ATTRIBUTES.all? do |attribute|
attributes[attribute] == contact.attributes[attribute]
end
end
private private
def identifiable_hash def identifiable_hash

View file

@ -0,0 +1,17 @@
module Concerns
module Domain
module BulkUpdatable
extend ActiveSupport::Concern
def bulk_update_prohibited?
discarded? || statuses_blocks_update?
end
def statuses_blocks_update?
prohibited_array = [DomainStatus::SERVER_UPDATE_PROHIBITED,
DomainStatus::CLIENT_UPDATE_PROHIBITED]
prohibited_array.any? { |block_status| statuses.include?(block_status) }
end
end
end
end

View file

@ -0,0 +1,40 @@
class ContactRequest < ApplicationRecord
establish_connection :"whois_#{Rails.env}"
self.table_name = 'contact_requests'
STATUS_NEW = 'new'.freeze
STATUS_CONFIRMED = 'confirmed'.freeze
STATUS_SENT = 'sent'.freeze
STATUSES = [STATUS_NEW, STATUS_CONFIRMED, STATUS_SENT].freeze
validates :whois_record_id, presence: true
validates :email, presence: true
validates :name, presence: true
validates :status, inclusion: { in: STATUSES }
attr_readonly :secret,
:valid_to
def self.save_record(params)
contact_request = new(params)
contact_request.secret = create_random_secret
contact_request.valid_to = set_valid_to_24_hours_from_now
contact_request.status = STATUS_NEW
contact_request.save!
contact_request
end
def update_status(params)
self.status = params['status']
self.ip_address = params['ip']
save!
end
def self.create_random_secret
SecureRandom.hex(64)
end
def self.set_valid_to_24_hours_from_now
(Time.zone.now + 24.hours)
end
end

View file

@ -10,6 +10,7 @@ class Domain < ApplicationRecord
include Concerns::Domain::RegistryLockable include Concerns::Domain::RegistryLockable
include Concerns::Domain::Releasable include Concerns::Domain::Releasable
include Concerns::Domain::Disputable include Concerns::Domain::Disputable
include Concerns::Domain::BulkUpdatable
attr_accessor :roles attr_accessor :roles

View file

@ -6,7 +6,7 @@ class TechDomainContact < DomainContact
tech_contacts = where(contact: current_contact) tech_contacts = where(contact: current_contact)
tech_contacts.each do |tech_contact| tech_contacts.each do |tech_contact|
if tech_contact.domain.discarded? if tech_contact.domain.bulk_update_prohibited?
skipped_domains << tech_contact.domain.name skipped_domains << tech_contact.domain.name
next next
end end
@ -18,7 +18,6 @@ class TechDomainContact < DomainContact
skipped_domains << tech_contact.domain.name skipped_domains << tech_contact.domain.name
end end
end end
[affected_domains.sort, skipped_domains.sort] [affected_domains.sort, skipped_domains.sort]
end end
end end

View file

@ -4,8 +4,13 @@ class WhiteIp < ApplicationRecord
validate :valid_ipv4? validate :valid_ipv4?
validate :valid_ipv6? validate :valid_ipv6?
validate :validate_ipv4_and_ipv6 validate :validate_ipv4_and_ipv6
before_save :normalize_blank_values
def normalize_blank_values
%i[ipv4 ipv6].each { |c| self[c].present? || self[c] = nil }
end
def validate_ipv4_and_ipv6 def validate_ipv4_and_ipv6
return if ipv4.present? || ipv6.present? return if ipv4.present? || ipv6.present?
errors.add(:base, I18n.t(:ipv4_or_ipv6_must_be_present)) errors.add(:base, I18n.t(:ipv4_or_ipv6_must_be_present))
@ -50,10 +55,10 @@ class WhiteIp < ApplicationRecord
def ids_including(ip) def ids_including(ip)
ipv4 = ipv6 = [] ipv4 = ipv6 = []
if check_ip4(ip).present? if check_ip4(ip).present?
ipv4 = select { |white_ip| IPAddr.new(white_ip.ipv4, Socket::AF_INET) === check_ip4(ip) } ipv4 = select { |white_ip| check_ip4(white_ip.ipv4) === check_ip4(ip) }
end end
if check_ip6(ip).present? if check_ip6(ip).present?
ipv6 = select { |white_ip| IPAddr.new(white_ip.ipv6, Socket::AF_INET6) === check_ip6(ip) } ipv6 = select { |white_ip| check_ip6(white_ip.ipv6) === check_ip6(ip) }
end end
(ipv4 + ipv6).pluck(:id).flatten.uniq (ipv4 + ipv6).pluck(:id).flatten.uniq
end end

View file

@ -0,0 +1,65 @@
<%= form_tag registrar_admin_contacts_path, method: :patch, class: 'form-horizontal' do %>
<% if @error %>
<div class="alert alert-danger">
<%= @error %>
</div>
<% end %>
<div class="form-group">
<div class="row">
<div class="col-md-6 control-label">
<p><%= t '.comment' %></p>
</div>
</div>
<div class="col-md-2 control-label">
<%= label_tag :current_contact_id, t('.current_contact_id') %>
</div>
<div class="col-md-4 current_admin_contact">
<%= text_field_tag :current_contact_id, params[:current_contact_id],
list: :contacts,
required: true,
autofocus: true,
class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= label_tag :new_contact_id, t('.new_contact_id') %>
</div>
<div class="col-md-4 new_admin_contact">
<%= text_field_tag :new_contact_id, params[:new_contact_id],
list: :contacts,
required: true,
class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-4 col-md-offset-2 text-right">
<button class="btn btn-warning">
<%= t '.submit_btn' %>
</button>
</div>
</div>
<div class="form-group">
<div class="col-md-6">
<a class="btn btn-default btn-xs" role="button" data-toggle="collapse"
href="#bulk_change_tech_contact_help"><%= t '.help_btn' %></a>
<div class="collapse" id="bulk_change_tech_contact_help">
<div class="well">
<%= t '.help' %>
</div>
</div>
</div>
</div>
<% end %>
<datalist id="contacts">
<% available_contacts.each do |data| %>
<option value="<%= data.second %>"><%= data.first %></option>
<% end %>
</datalist>

View file

@ -10,7 +10,7 @@
<%= label_tag :current_contact_id, t('.current_contact_id') %> <%= label_tag :current_contact_id, t('.current_contact_id') %>
</div> </div>
<div class="col-md-4"> <div class="col-md-4 current_tech_contact">
<%= text_field_tag :current_contact_id, params[:current_contact_id], <%= text_field_tag :current_contact_id, params[:current_contact_id],
list: :contacts, list: :contacts,
required: true, required: true,
@ -24,7 +24,7 @@
<%= label_tag :new_contact_id, t('.new_contact_id') %> <%= label_tag :new_contact_id, t('.new_contact_id') %>
</div> </div>
<div class="col-md-4"> <div class="col-md-4 new_tech_contact">
<%= text_field_tag :new_contact_id, params[:new_contact_id], <%= text_field_tag :new_contact_id, params[:new_contact_id],
list: :contacts, list: :contacts,
required: true, required: true,

View file

@ -12,6 +12,10 @@
<a href="#technical_contact" data-toggle="tab"><%= t '.technical_contact' %></a> <a href="#technical_contact" data-toggle="tab"><%= t '.technical_contact' %></a>
</li> </li>
<li class="<%= 'active' if active_tab == :admin_contact %>">
<a href="#admin_contact" data-toggle="tab"><%= t '.admin_contact' %></a>
</li>
<li class="<%= 'active' if active_tab == :nameserver %>"> <li class="<%= 'active' if active_tab == :nameserver %>">
<a href="#nameserver" data-toggle="tab"><%= t '.nameserver' %></a> <a href="#nameserver" data-toggle="tab"><%= t '.nameserver' %></a>
</li> </li>
@ -31,6 +35,11 @@
<%= render 'tech_contact_form', available_contacts: available_contacts %> <%= render 'tech_contact_form', available_contacts: available_contacts %>
</div> </div>
<div class="tab-pane<%= ' active' if active_tab == :admin_contact %>"
id="admin_contact">
<%= render 'admin_contact_form', available_contacts: available_contacts %>
</div>
<div class="tab-pane<%= ' active' if active_tab == :nameserver %>" id="nameserver"> <div class="tab-pane<%= ' active' if active_tab == :nameserver %>" id="nameserver">
<%= render 'nameserver_form' %> <%= render 'nameserver_form' %>
</div> </div>

View file

@ -87,8 +87,11 @@ sk_digi_doc_service_name: 'Testimine'
registrant_api_base_url: registrant_api_base_url:
registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas
# Bounces API # Shared key for REST-WHOIS Bounces API incl. CERT
api_shared_key: testkey rwhois_bounces_api_shared_key: testkey
# Link to REST-WHOIS API
rwhois_internal_api_shared_key: testkey
# Base URL (inc. https://) of REST registrant portal # Base URL (inc. https://) of REST registrant portal
# Leave blank to use internal registrant portal # Leave blank to use internal registrant portal

View file

@ -0,0 +1,4 @@
Aws.config.update(
region: ENV['aws_default_region'],
credentials: Aws::Credentials.new(ENV['aws_access_key_id'], ENV['aws_secret_access_key'])
)

View file

@ -0,0 +1,6 @@
en:
registrar:
admin_contacts:
update:
replaced: Admin contacts have been successfully replaced.
replaced: Technical contacts have been successfully replaced.

View file

@ -0,0 +1,11 @@
en:
registrar:
admin_contacts:
update:
replaced: Admin contacts have been successfully replaced.
affected_domains: Affected domains
skipped_domains: Skipped domains
process_request:
affected_domains: Affected domains
skipped_domains: Skipped domains
replaced: Admin contacts have been successfully replaced.

View file

@ -4,6 +4,7 @@ en:
new: new:
header: Bulk change header: Bulk change
technical_contact: Technical contact technical_contact: Technical contact
admin_contact: Admin contact
nameserver: Nameserver nameserver: Nameserver
bulk_transfer: Bulk transfer bulk_transfer: Bulk transfer
bulk_renew: Bulk renew bulk_renew: Bulk renew
@ -17,6 +18,19 @@ en:
Replace technical contact specified in "current contact ID" with the one in "new Replace technical contact specified in "current contact ID" with the one in "new
contact ID" on any domain registered under this registrar contact ID" on any domain registered under this registrar
admin_contact_form:
current_contact_id: Current admin contact ID
new_contact_id: New admin contact ID
submit_btn: Replace admin contacts
help_btn: Toggle help
help: >-
Replace admin contact specified in "current contact ID" with the one in "new
contact ID" on any domain registered under this registrar. Contact idents must
be the same
comment: >-
Bulk admin change is only allowed in case of old and new contact are sharing identical
ident data ie for updating contact information.
nameserver_form: nameserver_form:
ip_hint: One IP per line ip_hint: One IP per line
replace_btn: Replace nameserver replace_btn: Replace nameserver
@ -38,3 +52,5 @@ en:
domain_ids: Domains for bulk renewal domain_ids: Domains for bulk renewal
current_balance: Current balance current_balance: Current balance
period: Period period: Period
affected_domains: Affected domains
skipped_domains: Skipped domains

View file

@ -5,3 +5,7 @@ en:
replaced: Technical contacts have been successfully replaced. replaced: Technical contacts have been successfully replaced.
affected_domains: Affected domains affected_domains: Affected domains
skipped_domains: Skipped domains skipped_domains: Skipped domains
process_request:
affected_domains: Affected domains
skipped_domains: Skipped domains
replaced: Technical contacts have been successfully replaced.

View file

@ -73,6 +73,7 @@ Rails.application.routes.draw do
get ':id/transfer_info', to: 'domains#transfer_info', constraints: { id: /.*/ } get ':id/transfer_info', to: 'domains#transfer_info', constraints: { id: /.*/ }
post 'transfer', to: 'domains#transfer' post 'transfer', to: 'domains#transfer'
patch 'contacts', to: 'domains/contacts#update' patch 'contacts', to: 'domains/contacts#update'
patch 'admin_contacts', to: 'domains/admin_contacts#update'
post 'renew/bulk', to: 'domains/renews#bulk_renew' post 'renew/bulk', to: 'domains/renews#bulk_renew'
end end
end end
@ -100,6 +101,7 @@ Rails.application.routes.draw do
end end
resources :auctions, only: %i[index show update], param: :uuid resources :auctions, only: %i[index show update], param: :uuid
resources :contact_requests, only: %i[create update], param: :id
resources :bounces, only: %i[create] resources :bounces, only: %i[create]
end end
@ -145,6 +147,7 @@ Rails.application.routes.draw do
resource :bulk_change, controller: :bulk_change, only: :new resource :bulk_change, controller: :bulk_change, only: :new
post '/bulk_renew/new', to: 'bulk_change#bulk_renew', as: :bulk_renew post '/bulk_renew/new', to: 'bulk_change#bulk_renew', as: :bulk_renew
resource :tech_contacts, only: :update resource :tech_contacts, only: :update
resource :admin_contacts, only: :update
resource :nameservers, only: :update resource :nameservers, only: :update
resources :contacts, constraints: {:id => /[^\/]+(?=#{ ActionController::Renderers::RENDERERS.map{|e| "\\.#{e}\\z"}.join("|") })|[^\/]+/} do resources :contacts, constraints: {:id => /[^\/]+(?=#{ ActionController::Renderers::RENDERERS.map{|e| "\\.#{e}\\z"}.join("|") })|[^\/]+/} do
member do member do

8
test/fixtures/contact_requests.yml vendored Normal file
View file

@ -0,0 +1,8 @@
new:
whois_record_id: 1
email: aaa@bbb.com
name: Testname
status: new
secret: somesecret
valid_to: 2010-07-05

View file

@ -0,0 +1,153 @@
require 'test_helper'
class APIDomainAdminContactsTest < ApplicationIntegrationTest
setup do
@admin_current = domains(:shop).admin_contacts.find_by(code: 'jane-001')
domain = domains(:airport)
domain.admin_contacts << @admin_current
@admin_new = contacts(:william)
@admin_new.update(ident: @admin_current.ident,
ident_type: @admin_current.ident_type,
ident_country_code: @admin_current.ident_country_code)
end
def test_replace_all_admin_contacts_when_ident_data_doesnt_match
@admin_new.update(ident: '777' ,
ident_type: 'priv',
ident_country_code: 'LV')
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
assert_equal ({ code: 2304, message: 'Admin contacts must be identical', data: {} }),
JSON.parse(response.body, symbolize_names: true)
end
def test_replace_all_admin_contacts_of_the_current_registrar
assert @admin_new.identical_to?(@admin_current)
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_nil domains(:shop).admin_contacts.find_by(code: @admin_current.code)
assert domains(:shop).admin_contacts.find_by(code: @admin_new.code)
assert domains(:airport).admin_contacts.find_by(code: @admin_new.code)
end
def test_skip_discarded_domains
domains(:airport).update!(statuses: [DomainStatus::DELETE_CANDIDATE])
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert domains(:airport).admin_contacts.find_by(code: @admin_current.code)
end
def test_return_affected_domains_in_alphabetical_order
domain = domains(:airport)
domain.admin_contacts = [@admin_current]
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :ok
assert_equal ({ code: 1000, message: 'Command completed successfully', data: { affected_domains: %w[airport.test shop.test],
skipped_domains: [] }}),
JSON.parse(response.body, symbolize_names: true)
end
def test_return_skipped_domains_in_alphabetical_order
domains(:shop).update!(statuses: [DomainStatus::DELETE_CANDIDATE])
domains(:airport).update!(statuses: [DomainStatus::DELETE_CANDIDATE])
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :ok
assert_equal %w[airport.test shop.test], JSON.parse(response.body,
symbolize_names: true)[:data][:skipped_domains]
end
def test_keep_other_admin_contacts_intact
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert domains(:airport).admin_contacts.find_by(code: 'john-001')
end
def test_keep_tech_contacts_intact
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert domains(:airport).tech_contacts.find_by(code: 'william-001')
end
def test_restrict_contacts_to_the_current_registrar
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: 'william-002' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :not_found
assert_equal ({ code: 2303, message: 'Object does not exist' }),
JSON.parse(response.body, symbolize_names: true)
end
def test_non_existent_current_contact
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: 'non-existent',
new_contact_id: @admin_new.code},
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :not_found
assert_equal ({ code: 2303, message: 'Object does not exist' }),
JSON.parse(response.body, symbolize_names: true)
end
def test_non_existent_new_contact
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: 'non-existent' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :not_found
assert_equal ({code: 2303, message: 'Object does not exist'}),
JSON.parse(response.body, symbolize_names: true)
end
def test_disallow_invalid_new_contact
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: 'invalid' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
assert_equal ({ code: 2304, message: 'New contact must be valid', data: {} }),
JSON.parse(response.body, symbolize_names: true)
end
def test_admin_bulk_changed_when_domain_update_prohibited
domains(:shop).update!(statuses: [DomainStatus::SERVER_UPDATE_PROHIBITED])
domains(:airport).admin_contacts = [@admin_current]
shop_admin_contact = Contact.find_by(code: 'jane-001')
assert domains(:shop).admin_contacts.include?(shop_admin_contact)
patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code,
new_contact_id: @admin_new.code },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :ok
assert_equal ({ code: 1000,
message: 'Command completed successfully',
data: { affected_domains: ["airport.test"],
skipped_domains: ["shop.test"] }}),
JSON.parse(response.body, symbolize_names: true)
end
private
def http_auth_key
ActionController::HttpAuthentication::Basic.encode_credentials('test_bestnames', 'testtest')
end
end

View file

@ -107,6 +107,24 @@ class APIDomainContactsTest < ApplicationIntegrationTest
JSON.parse(response.body, symbolize_names: true) JSON.parse(response.body, symbolize_names: true)
end end
def test_tech_bulk_changed_when_domain_update_prohibited
domains(:shop).update!(statuses: [DomainStatus::SERVER_UPDATE_PROHIBITED])
shop_tech_contact = Contact.find_by(code: 'william-001')
assert domains(:shop).tech_contacts.include?(shop_tech_contact)
patch '/repp/v1/domains/contacts', params: { current_contact_id: 'william-001',
new_contact_id: 'john-001' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :ok
assert_equal ({ code: 1000,
message: 'Command completed successfully',
data: { affected_domains: ["airport.test"],
skipped_domains: ["shop.test"] }}),
JSON.parse(response.body, symbolize_names: true)
end
private private
def http_auth_key def http_auth_key

View file

@ -2,7 +2,7 @@ require 'test_helper'
class BouncesApiV1CreateTest < ActionDispatch::IntegrationTest class BouncesApiV1CreateTest < ActionDispatch::IntegrationTest
def setup def setup
@api_key = "Basic #{ENV['api_shared_key']}" @api_key = "Basic #{ENV['rwhois_bounces_api_shared_key']}"
@headers = { "Authorization": "#{@api_key}" } @headers = { "Authorization": "#{@api_key}" }
@json_body = { "data": valid_bounce_request }.as_json @json_body = { "data": valid_bounce_request }.as_json
end end

View file

@ -0,0 +1,68 @@
require 'test_helper'
class ApiV1ContactRequestTest < ActionDispatch::IntegrationTest
def setup
@api_key = "Basic #{ENV['rwhois_internal_api_shared_key']}"
@headers = { "Authorization": "#{@api_key}" }
@json_create = { "contact_request": valid_contact_request_create }.as_json
@json_update = { "contact_request": valid_contact_request_update }.as_json
@contact_request = contact_requests(:new)
end
def test_authorizes_api_request
post api_v1_contact_requests_path, params: @json_create, headers: @headers
assert_response :created
invalid_headers = { "Authorization": "Basic invalid_api_key" }
post api_v1_contact_requests_path, params: @json_create, headers: invalid_headers
assert_response :unauthorized
end
def test_saves_new_contact_request
request_body = @json_create.dup
random_mail = "#{rand(10000..99999)}@registry.test"
request_body['contact_request']['email'] = random_mail
post api_v1_contact_requests_path, params: request_body, headers: @headers
assert_response :created
contact_request = ContactRequest.last
assert_equal contact_request.email, random_mail
assert ContactRequest::STATUS_NEW, contact_request.status
end
def test_updates_existing_contact_request
request_body = @json_update.dup
put api_v1_contact_request_path(@contact_request.id), params: request_body, headers: @headers
assert_response :ok
@contact_request.reload
assert ContactRequest::STATUS_CONFIRMED, @contact_request.status
end
def test_not_updates_if_status_error
request_body = @json_update.dup
request_body['contact_request']['status'] = 'some_error_status'
put api_v1_contact_request_path(@contact_request.id), params: request_body, headers: @headers
assert_response 400
@contact_request.reload
assert ContactRequest::STATUS_NEW, @contact_request.status
end
def valid_contact_request_create
{
"email": "aaa@bbb.com",
"whois_record_id": "1",
"name": "test"
}.as_json
end
def valid_contact_request_update
{
"status": "#{ContactRequest::STATUS_CONFIRMED}",
}.as_json
end
end

View file

@ -38,6 +38,20 @@ class WhiteIpTest < ActiveSupport::TestCase
assert white_ip.valid? assert white_ip.valid?
end end
def test_validates_include_empty_ipv4
white_ip = WhiteIp.new
white_ip.ipv4 = nil
white_ip.ipv6 = '001:0db8:85a3:0000:0000:8a2e:0370:7334'
white_ip.registrar = registrars(:bestnames)
assert_nothing_raised { white_ip.save }
assert white_ip.valid?
assert WhiteIp.include_ip?(white_ip.ipv6)
assert_not WhiteIp.include_ip?('192.168.1.1')
end
private private
def valid_white_ip def valid_white_ip

View file

@ -0,0 +1,49 @@
require 'application_system_test_case'
class RegistrarAreaAdminContactBulkChangeTest < ApplicationSystemTestCase
setup do
sign_in users(:api_bestnames)
end
def test_replace_domain_contacts_of_current_registrar
request_stub = stub_request(:patch, /domains\/admin_contacts/)
.with(body: { current_contact_id: 'william-001', new_contact_id: 'john-001' },
basic_auth: ['test_bestnames', 'testtest'])
.to_return(body: { data: { affected_domains: %w[foo.test bar.test],
skipped_domains: %w[baz.test qux.test] } }.to_json,
status: 200)
visit registrar_domains_url
click_link 'Bulk change'
click_link 'Admin contact'
find('.current_admin_contact').fill_in 'Current contact ID', with: 'william-001'
find('.new_admin_contact').fill_in 'New contact ID', with: 'john-001'
click_on 'Replace admin contacts'
assert_requested request_stub
assert_current_path registrar_domains_path
assert_text 'Admin contacts have been successfully replaced'
assert_text 'Affected domains: foo.test, bar.test'
assert_text 'Skipped domains: baz.test, qux.test'
end
def test_fails_gracefully
stub_request(:patch, /domains\/admin_contacts/)
.to_return(status: 400,
body: { message: 'epic fail' }.to_json,
headers: { 'Content-type' => Mime[:json] })
visit registrar_domains_url
click_link 'Bulk change'
click_link 'Admin contact'
find('.current_admin_contact').fill_in 'Current contact ID', with: 'william-001'
find('.new_admin_contact').fill_in 'New contact ID', with: 'john-001'
click_on 'Replace admin contacts'
assert_text 'epic fail'
assert_field 'Current contact ID', with: 'william-001'
assert_field 'New contact ID', with: 'john-001'
end
end

View file

@ -16,8 +16,8 @@ class RegistrarAreaTechContactBulkChangeTest < ApplicationSystemTestCase
visit registrar_domains_url visit registrar_domains_url
click_link 'Bulk change' click_link 'Bulk change'
fill_in 'Current contact ID', with: 'william-001' find('.current_tech_contact').fill_in 'Current contact ID', with: 'william-001'
fill_in 'New contact ID', with: 'john-001' find('.new_tech_contact').fill_in 'New contact ID', with: 'john-001'
click_on 'Replace technical contacts' click_on 'Replace technical contacts'
assert_requested request_stub assert_requested request_stub
@ -36,8 +36,8 @@ class RegistrarAreaTechContactBulkChangeTest < ApplicationSystemTestCase
visit registrar_domains_url visit registrar_domains_url
click_link 'Bulk change' click_link 'Bulk change'
fill_in 'Current contact ID', with: 'william-001' find('.current_tech_contact').fill_in 'Current contact ID', with: 'william-001'
fill_in 'New contact ID', with: 'john-001' find('.new_tech_contact').fill_in 'New contact ID', with: 'john-001'
click_on 'Replace technical contacts' click_on 'Replace technical contacts'
assert_text 'epic fail' assert_text 'epic fail'