diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bfe334ef..c2b85a279 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+31.03.2021
+* Implemented child to parent syncronisation (csync) for cdnskeys [#658](https://github.com/internetee/registry/issues/658)
+
29.03.2021
* Full EPP functionality to REPP API [#1756](https://github.com/internetee/registry/issues/1756)
diff --git a/Gemfile b/Gemfile
index be4a9756b..75c3c048e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,6 +5,8 @@ gem 'active_interaction', '~> 3.8'
gem 'apipie-rails', '~> 0.5.18'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'iso8601', '0.12.1' # for dates and times
+gem 'mime-types-data'
+gem 'mimemagic', '0.3.10'
gem 'rails', '~> 6.0'
gem 'rest-client'
gem 'uglifier'
@@ -39,6 +41,7 @@ gem 'devise', '~> 4.7'
# registry specfic
gem 'data_migrate', '~> 6.1'
+gem 'dnsruby', '~> 1.61'
gem 'isikukood' # for EE-id validation
gem 'simpleidn', '0.1.1' # For punycode
gem 'money-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 563c39fdc..cc4238920 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -76,60 +76,60 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.0.3.5)
- actionpack (= 6.0.3.5)
+ actioncable (6.0.3.6)
+ actionpack (= 6.0.3.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.0.3.5)
- actionpack (= 6.0.3.5)
- activejob (= 6.0.3.5)
- activerecord (= 6.0.3.5)
- activestorage (= 6.0.3.5)
- activesupport (= 6.0.3.5)
+ actionmailbox (6.0.3.6)
+ actionpack (= 6.0.3.6)
+ activejob (= 6.0.3.6)
+ activerecord (= 6.0.3.6)
+ activestorage (= 6.0.3.6)
+ activesupport (= 6.0.3.6)
mail (>= 2.7.1)
- actionmailer (6.0.3.5)
- actionpack (= 6.0.3.5)
- actionview (= 6.0.3.5)
- activejob (= 6.0.3.5)
+ actionmailer (6.0.3.6)
+ actionpack (= 6.0.3.6)
+ actionview (= 6.0.3.6)
+ activejob (= 6.0.3.6)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.0.3.5)
- actionview (= 6.0.3.5)
- activesupport (= 6.0.3.5)
+ actionpack (6.0.3.6)
+ actionview (= 6.0.3.6)
+ activesupport (= 6.0.3.6)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.0.3.5)
- actionpack (= 6.0.3.5)
- activerecord (= 6.0.3.5)
- activestorage (= 6.0.3.5)
- activesupport (= 6.0.3.5)
+ actiontext (6.0.3.6)
+ actionpack (= 6.0.3.6)
+ activerecord (= 6.0.3.6)
+ activestorage (= 6.0.3.6)
+ activesupport (= 6.0.3.6)
nokogiri (>= 1.8.5)
- actionview (6.0.3.5)
- activesupport (= 6.0.3.5)
+ actionview (6.0.3.6)
+ activesupport (= 6.0.3.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_interaction (3.8.3)
activemodel (>= 4, < 7)
- activejob (6.0.3.5)
- activesupport (= 6.0.3.5)
+ activejob (6.0.3.6)
+ activesupport (= 6.0.3.6)
globalid (>= 0.3.6)
- activemodel (6.0.3.5)
- activesupport (= 6.0.3.5)
- activerecord (6.0.3.5)
- activemodel (= 6.0.3.5)
- activesupport (= 6.0.3.5)
+ activemodel (6.0.3.6)
+ activesupport (= 6.0.3.6)
+ activerecord (6.0.3.6)
+ activemodel (= 6.0.3.6)
+ activesupport (= 6.0.3.6)
activerecord-import (1.0.8)
activerecord (>= 3.2)
- activestorage (6.0.3.5)
- actionpack (= 6.0.3.5)
- activejob (= 6.0.3.5)
- activerecord (= 6.0.3.5)
- marcel (~> 0.3.1)
- activesupport (6.0.3.5)
+ activestorage (6.0.3.6)
+ actionpack (= 6.0.3.6)
+ activejob (= 6.0.3.6)
+ activerecord (= 6.0.3.6)
+ marcel (~> 1.0.0)
+ activesupport (6.0.3.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -215,6 +215,8 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
+ dnsruby (1.61.5)
+ simpleidn (~> 0.1)
docile (1.3.5)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
@@ -277,14 +279,15 @@ GEM
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
- marcel (0.3.3)
- mimemagic (~> 0.3.2)
+ marcel (1.0.0)
method_source (0.8.2)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0225)
- mimemagic (0.3.5)
- mini_mime (1.0.2)
+ mimemagic (0.3.10)
+ nokogiri (~> 1)
+ rake
+ mini_mime (1.0.3)
mini_portile2 (2.4.0)
minitest (5.14.4)
monetize (1.9.4)
@@ -350,29 +353,29 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (6.0.3.5)
- actioncable (= 6.0.3.5)
- actionmailbox (= 6.0.3.5)
- actionmailer (= 6.0.3.5)
- actionpack (= 6.0.3.5)
- actiontext (= 6.0.3.5)
- actionview (= 6.0.3.5)
- activejob (= 6.0.3.5)
- activemodel (= 6.0.3.5)
- activerecord (= 6.0.3.5)
- activestorage (= 6.0.3.5)
- activesupport (= 6.0.3.5)
+ rails (6.0.3.6)
+ actioncable (= 6.0.3.6)
+ actionmailbox (= 6.0.3.6)
+ actionmailer (= 6.0.3.6)
+ actionpack (= 6.0.3.6)
+ actiontext (= 6.0.3.6)
+ actionview (= 6.0.3.6)
+ activejob (= 6.0.3.6)
+ activemodel (= 6.0.3.6)
+ activerecord (= 6.0.3.6)
+ activestorage (= 6.0.3.6)
+ activesupport (= 6.0.3.6)
bundler (>= 1.3.0)
- railties (= 6.0.3.5)
+ railties (= 6.0.3.6)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
- railties (6.0.3.5)
- actionpack (= 6.0.3.5)
- activesupport (= 6.0.3.5)
+ railties (6.0.3.6)
+ actionpack (= 6.0.3.6)
+ activesupport (= 6.0.3.6)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@@ -519,6 +522,7 @@ DEPENDENCIES
devise (~> 4.7)
digidoc_client!
directo!
+ dnsruby (~> 1.61)
domain_name
e_invoice!
epp!
@@ -531,6 +535,8 @@ DEPENDENCIES
jquery-ui-rails (= 5.0.5)
kaminari
lhv!
+ mime-types-data
+ mimemagic (= 0.3.10)
minitest (~> 5.14)
money-rails
nokogiri (~> 1.10.0)
diff --git a/app/jobs/csync_job.rb b/app/jobs/csync_job.rb
new file mode 100644
index 000000000..32fc1abcd
--- /dev/null
+++ b/app/jobs/csync_job.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+class CsyncJob < Que::Job
+ def run(generate: false)
+ @store = {}
+ @input_store = { secure: {}, insecure: {} }
+ @results = {}
+ @logger = Rails.env.test? ? Rails.logger : Logger.new(STDOUT)
+ generate ? generate_scanner_input : process_scanner_results
+
+ @logger.info 'CsyncJob: Finished.'
+ end
+
+ def qualified_for_monitoring?(domain, data)
+ result_types = data[:ns].map { |ns| ns[:type] }.uniq
+ ns_ok = redundant_data_for?(nameserver: true, input: result_types)
+ key_ok = redundant_data_for?(nameserver: false, input: data)
+
+ return true if ns_ok && key_ok
+
+ @logger.info "CsyncJob: #{domain}: Reseting state. Reason: " +
+ unqualification_reason(ns_ok, key_ok, result_types)
+
+ CsyncRecord.where(domain: Domain.where(name: domain)).delete_all
+
+ false
+ end
+
+ def redundant_data_for?(nameserver: false, input:)
+ if nameserver
+ input.size == 1 && (input & %w[secure insecure]).any?
+ else
+ input[:ns].map { |ns| ns[:cdnskey] }.uniq.size == 1
+ end
+ end
+
+ def unqualification_reason(nss, key, result_types)
+ return 'no CDNSKEY / nameservers reported different CDNSKEYs' unless key
+
+ if result_types.include? 'untrustworthy'
+ return 'current DNSSEC config invalid (required for rollover/delete)'
+ end
+
+ "Nameserver(s) not reachable / invalid data (#{result_types.join(', ')})" unless nss
+ end
+
+ def process_scanner_results
+ scanner_results
+
+ @results.keys.each do |domain|
+ next unless qualified_for_monitoring?(domain, @results[domain])
+
+ CsyncRecord.by_domain_name(domain)&.record_new_scan(@results[domain][:ns].first)
+ end
+ end
+
+ def scanner_results
+ scanner_line_results.each do |fetch|
+ domain_name = fetch[:domain]
+ @results[domain_name] = { ns: [] } unless @results[domain_name]
+ @results[domain_name][:ns] << fetch.except(:domain)
+ end
+ end
+
+ def scanner_line_results
+ records = []
+ File.open(ENV['cdns_scanner_output_file'], 'r').each_line do |line|
+ # Input type, NS host, NS IP, Domain name, Key type, Protocol, Algorithm, Public key
+ data = line.strip.split(' ')
+ if data[0] == 'secure'
+ type, domain, key_bit, proto, alg, pub, ns, ns_ip = data
+ else
+ type, ns, ns_ip, domain, key_bit, proto, alg, pub = data
+ end
+ cdnskey = key_bit && proto && alg && pub ? "#{key_bit} #{proto} #{alg} #{pub}" : nil
+ record = { domain: domain, type: type, ns: ns, ns_ip: ns_ip, flags: key_bit, proto: proto,
+ alg: alg, pub: pub, cdnskey: cdnskey }
+ records << record
+ end
+ records
+ end
+
+ # From this point we're working on generating input for cdnskey-scanner
+ def gather_pollable_domains
+ @logger.info 'CsyncJob Generate: Gathering current domain(s) data'
+ Nameserver.select(:hostname, :domain_id).all.each do |ns|
+ %i[secure insecure].each do |i|
+ @input_store[i][ns.hostname] = [] unless @input_store[i].key? ns.hostname
+ end
+
+ append_domains_to_list(ns)
+ end
+ end
+
+ def append_domains_to_list(nameserver)
+ Domain.where(id: nameserver.domain_id).all.each do |domain|
+ @input_store[domain.dnskeys.any? ? :secure : :insecure][nameserver.hostname].push domain.name
+ end
+ end
+
+ def generate_scanner_input
+ @logger.info 'CsyncJob Generate: Gathering current domain(s) data'
+ gather_pollable_domains
+
+ out_file = File.new(ENV['cdns_scanner_input_file'], 'w+')
+
+ %i[secure insecure].each do |state|
+ out_file.puts "[#{state}]"
+ create_input_lines(out_file, state)
+ end
+
+ out_file.close
+ @logger.info 'CsyncJob Generate: Finished writing output to ' + ENV['cdns_scanner_input_file']
+ end
+
+ def create_input_lines(out_file, state)
+ @input_store[state].keys.each do |nameserver|
+ domains = @input_store[state][nameserver].join(' ')
+ next unless domains.length.positive?
+
+ out_file.puts "#{nameserver} #{domains}"
+ end
+ end
+end
diff --git a/app/mailers/csync_mailer.rb b/app/mailers/csync_mailer.rb
new file mode 100644
index 000000000..2779ff8a5
--- /dev/null
+++ b/app/mailers/csync_mailer.rb
@@ -0,0 +1,23 @@
+class CsyncMailer < ApplicationMailer
+ def dnssec_updated(domain:)
+ @domain = domain
+ emails = contact_emails(domain)
+
+ subject = default_i18n_subject(domain_name: domain.name)
+ mail(to: emails, subject: subject)
+ end
+
+ def dnssec_deleted(domain:)
+ @domain = domain
+ emails = contact_emails(domain)
+
+ subject = default_i18n_subject(domain_name: domain.name)
+ mail(to: emails, subject: subject)
+ end
+
+ private
+
+ def contact_emails(domain)
+ (domain.contacts.map(&:email) << domain.registrant.email).uniq
+ end
+end
diff --git a/app/models/actions/contact_create.rb b/app/models/actions/contact_create.rb
new file mode 100644
index 000000000..22fabc7b9
--- /dev/null
+++ b/app/models/actions/contact_create.rb
@@ -0,0 +1,81 @@
+module Actions
+ class ContactCreate
+ attr_reader :contact, :legal_document, :ident
+
+ def initialize(contact, legal_document, ident)
+ @contact = contact
+ @legal_document = legal_document
+ @ident = ident
+ end
+
+ def call
+ maybe_remove_address
+ maybe_attach_legal_doc
+ validate_ident
+ commit
+ end
+
+ def maybe_remove_address
+ return if Contact.address_processing?
+
+ contact.city = nil
+ contact.zip = nil
+ contact.street = nil
+ contact.state = nil
+ contact.country_code = nil
+ end
+
+ def validate_ident
+ validate_ident_integrity
+ validate_ident_birthday
+
+ identifier = ::Contact::Ident.new(code: ident[:ident], type: ident[:ident_type],
+ country_code: ident[:ident_country_code])
+
+ identifier.validate
+ contact.identifier = identifier
+ end
+
+ def validate_ident_integrity
+ return if ident.blank?
+
+ if ident[:ident_type].blank?
+ contact.add_epp_error('2003', nil, 'ident_type',
+ I18n.t('errors.messages.required_ident_attribute_missing'))
+ @error = true
+ elsif !%w[priv org birthday].include?(ident[:ident_type])
+ contact.add_epp_error('2003', nil, 'ident_type', 'Invalid ident type')
+ @error = true
+ end
+ end
+
+ def validate_ident_birthday
+ return if ident.blank?
+ return unless ident[:ident_type] != 'birthday' && ident[:ident_country_code].blank?
+
+ contact.add_epp_error('2003', nil, 'ident_country_code',
+ I18n.t('errors.messages.required_ident_attribute_missing'))
+ @error = true
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ doc = LegalDocument.create(
+ documentable_type: Contact,
+ document_type: legal_document[:type], body: legal_document[:body]
+ )
+
+ contact.legal_documents = [doc]
+ contact.legal_document_id = doc.id
+ end
+
+ def commit
+ contact.id = nil # new record
+ return false if @error
+
+ contact.generate_code
+ contact.save
+ end
+ end
+end
diff --git a/app/models/actions/contact_delete.rb b/app/models/actions/contact_delete.rb
new file mode 100644
index 000000000..60d3252c4
--- /dev/null
+++ b/app/models/actions/contact_delete.rb
@@ -0,0 +1,46 @@
+module Actions
+ class ContactDelete
+ attr_reader :contact
+ attr_reader :new_attributes
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(contact, legal_document = nil)
+ @legal_document = legal_document
+ @contact = contact
+ end
+
+ def call
+ maybe_attach_legal_doc
+
+ if contact.linked?
+ contact.errors.add(:domains, :exist)
+ return
+ end
+
+ if contact.delete_prohibited?
+ contact.errors.add(:statuses, :delete_prohibited)
+ return
+ end
+
+ commit
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ document = contact.legal_documents.create(
+ document_type: legal_document[:type],
+ body: legal_document[:body]
+ )
+
+ contact.legal_document_id = document.id
+ contact.save
+ end
+
+ def commit
+ contact.destroy
+ end
+ end
+end
diff --git a/app/models/actions/contact_update.rb b/app/models/actions/contact_update.rb
new file mode 100644
index 000000000..7ca7b6b04
--- /dev/null
+++ b/app/models/actions/contact_update.rb
@@ -0,0 +1,109 @@
+module Actions
+ class ContactUpdate
+ attr_reader :contact
+ attr_reader :new_attributes
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(contact, new_attributes, legal_document, ident, user)
+ @contact = contact
+ @new_attributes = new_attributes
+ @legal_document = legal_document
+ @ident = ident
+ @user = user
+ end
+
+ def call
+ maybe_remove_address
+ maybe_update_statuses
+ maybe_update_ident if ident.present?
+ maybe_attach_legal_doc
+ commit
+ end
+
+ def maybe_remove_address
+ return if Contact.address_processing?
+
+ new_attributes.delete(:city)
+ new_attributes.delete(:zip)
+ new_attributes.delete(:street)
+ new_attributes.delete(:state)
+ new_attributes.delete(:country_code)
+ end
+
+ def maybe_update_statuses
+ return unless Setting.client_status_editing_enabled
+
+ new_statuses =
+ contact.statuses - new_attributes[:statuses_to_remove] + new_attributes[:statuses_to_add]
+
+ new_attributes[:statuses] = new_statuses
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ document = contact.legal_documents.create(
+ document_type: legal_document[:type],
+ body: legal_document[:body]
+ )
+
+ contact.legal_document_id = document.id
+ end
+
+ def maybe_update_ident
+ unless ident.is_a?(Hash)
+ contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident'))
+ @error = true
+ return
+ end
+
+ if contact.identifier.valid?
+ submitted_ident = ::Contact::Ident.new(code: ident[:ident],
+ type: ident[:ident_type],
+ country_code: ident[:ident_country_code])
+
+ if submitted_ident != contact.identifier
+ contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident'))
+ @error = true
+ end
+ else
+ ident_update_attempt = ident[:ident] != contact.ident
+
+ if ident_update_attempt
+ contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.ident_update'))
+ @error = true
+ end
+
+ identifier = ::Contact::Ident.new(code: ident[:ident],
+ type: ident[:ident_type],
+ country_code: ident[:ident_country_code])
+
+ identifier.validate
+
+ contact.identifier = identifier
+ contact.ident_updated_at ||= Time.zone.now
+ end
+ end
+
+ def commit
+ return false if @error
+
+ contact.upid = user.registrar&.id
+ contact.up_date = Time.zone.now
+
+ contact.attributes = new_attributes
+
+ email_changed = contact.will_save_change_to_email?
+ old_email = contact.email_was
+ updated = contact.save
+
+ if updated && email_changed && contact.registrant?
+ ContactMailer.email_changed(contact: contact, old_email: old_email).deliver_now
+ end
+
+ updated
+ end
+ end
+end
diff --git a/app/models/actions/domain_transfer.rb b/app/models/actions/domain_transfer.rb
new file mode 100644
index 000000000..1ff9aafe9
--- /dev/null
+++ b/app/models/actions/domain_transfer.rb
@@ -0,0 +1,74 @@
+module Actions
+ class DomainTransfer
+ attr_reader :domain
+ attr_reader :transfer_code
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(domain, transfer_code, user)
+ @domain = domain
+ @transfer_code = transfer_code
+ @user = user
+ end
+
+ def call
+ return unless domain_exists?
+ return unless valid_transfer_code?
+
+ run_validations
+
+ # return domain.pending_transfer if domain.pending_transfer
+ # attach_legal_document(::Deserializers::Xml::LegalDocument.new(frame).call)
+
+ return if domain.errors[:epp_errors].any?
+
+ commit
+ end
+
+ def domain_exists?
+ return true if domain.persisted?
+
+ domain.add_epp_error('2303', nil, nil, 'Object does not exist')
+
+ false
+ end
+
+ def run_validations
+ validate_registrar
+ validate_eligilibty
+ validate_not_discarded
+ end
+
+ def valid_transfer_code?
+ return true if transfer_code == domain.transfer_code
+
+ domain.add_epp_error('2202', nil, nil, 'Invalid authorization information')
+ false
+ end
+
+ def validate_registrar
+ return unless user == domain.registrar
+
+ domain.add_epp_error('2002', nil, nil,
+ I18n.t(:domain_already_belongs_to_the_querying_registrar))
+ end
+
+ def validate_eligilibty
+ return unless domain.non_transferable?
+
+ domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ end
+
+ def validate_not_discarded
+ return unless domain.discarded?
+
+ domain.add_epp_error('2106', nil, nil, 'Object is not eligible for transfer')
+ end
+
+ def commit
+ bare_domain = Domain.find(domain.id)
+ ::DomainTransfer.request(bare_domain, user)
+ end
+ end
+end
diff --git a/app/models/concerns/csync_record/diggable.rb b/app/models/concerns/csync_record/diggable.rb
new file mode 100644
index 000000000..fe5d3c072
--- /dev/null
+++ b/app/models/concerns/csync_record/diggable.rb
@@ -0,0 +1,45 @@
+module CsyncRecord::Diggable
+ extend ActiveSupport::Concern
+
+ def valid_security_level?(post: false)
+ res = post ? valid_post_action? : valid_pre_action?
+
+ log_dnssec_entry(valid: res, post: post)
+ res
+ rescue Dnsruby::NXDomain
+ log.info("CsyncRecord: #{domain.name}: Could not resolve (NXDomain)")
+ false
+ end
+
+ def valid_pre_action?
+ case domain.dnssec_security_level
+ when Dnsruby::Message::SecurityLevel.SECURE
+ return true if %w[rollover deactivate].include?(action)
+ when Dnsruby::Message::SecurityLevel.INSECURE, Dnsruby::Message::SecurityLevel.BOGUS
+ return true if action == 'initialized'
+ end
+
+ false
+ end
+
+ def valid_post_action?
+ secure_msg = Dnsruby::Message::SecurityLevel.SECURE
+ security_level = domain.dnssec_security_level(stubber: dnskey)
+ return true if action == 'deactivate' && security_level != secure_msg
+ return true if %w[rollover initialized].include?(action) && security_level == secure_msg
+
+ false
+ end
+
+ def dnssec_validates?
+ return false unless dnskey.valid?
+ return true if valid_security_level? && valid_security_level?(post: true)
+
+ false
+ end
+
+ def log_dnssec_entry(valid:, post:)
+ log.info("#{domain.name}: #{post ? 'Post' : 'Pre'} DNSSEC validation " \
+ "#{valid ? 'PASSED' : 'FAILED'} for action '#{action}'")
+ end
+end
diff --git a/app/models/csync_record.rb b/app/models/csync_record.rb
new file mode 100644
index 000000000..b4893c7d1
--- /dev/null
+++ b/app/models/csync_record.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+class CsyncRecord < ApplicationRecord
+ include CsyncRecord::Diggable
+ belongs_to :domain, optional: false
+ validates :domain, uniqueness: true
+ validates :cdnskey, :action, :last_scan, presence: true
+ validate :validate_unique_pub_key
+ validate :validate_csync_action
+ validate :validate_cdnskey_format
+ after_save :process_new_dnskey, if: proc { pushable? && !disable_requested? }
+ after_save :remove_dnskeys, if: proc { pushable? && disable_requested? }
+
+ SCAN_CYCLES = 3
+
+ def record_new_scan(result)
+ assign_scanner_data!(result)
+ prefix = "CsyncRecord: #{domain.name}:"
+
+ if save
+ log.info "#{prefix} Cycle done."
+ else
+ log.info "#{prefix}: not processing. Reason: #{errors.full_messages.join(' .')}"
+ CsyncRecord.where(domain: domain).delete_all
+ end
+ end
+
+ def assign_scanner_data!(result)
+ state = result[:type]
+ self.last_scan = Time.zone.now
+ self.times_scanned = (persisted? && cdnskey != result[:cdnskey] ? 1 : times_scanned + 1)
+ self.cdnskey = result[:cdnskey]
+ self.action = initializes_dnssec?(state) ? 'initialized' : determine_csync_intention(state)
+ end
+
+ def dnskey
+ key = Dnskey.new_from_csync(domain: domain, cdnskey: cdnskey)
+ log.info "DNSKEY not valid. #{key.errors.full_messages.join('. ')}." unless key.valid?
+
+ key
+ end
+
+ def destroy_all_but_last_one
+ domain.dnskeys.order(id: :desc).offset(1).destroy_all
+ end
+
+ def process_new_dnskey
+ return unless dnssec_validates?
+
+ if dnskey.save
+ destroy_all_but_last_one
+ finalize_and_notify
+ else
+ log.info "Failed to save DNSKEY. Errors: #{dnskey.errors.full_messages.join('. ')}"
+ end
+ end
+
+ def finalize_and_notify
+ CsyncMailer.dnssec_updated(domain: domain).deliver_now
+ notify_registrar_about_csync
+ CsyncRecord.where(domain: domain).destroy_all
+ log.info "CsyncRecord: #{domain.name}: DNSKEYs updated."
+ end
+
+ def pushable?
+ return true if domain.dnskeys.any? || times_scanned >= SCAN_CYCLES
+
+ false
+ end
+
+ def disable_requested?
+ ['0 3 0 AA==', '0 3 0 0'].include? cdnskey
+ end
+
+ def remove_dnskeys
+ log.info "CsyncJob: Removing DNSKEYs for domain '#{domain.name}'"
+ domain.dnskeys.destroy_all
+ CsyncMailer.dnssec_deleted(domain: domain).deliver_now
+ notify_registrar_about_csync
+
+ destroy
+ end
+
+ def notify_registrar_about_csync
+ domain.update_whois_record
+ domain.registrar.notifications.create!(text: I18n.t('notifications.texts.csync',
+ domain: domain.name, action: action))
+ end
+
+ def validate_unique_pub_key
+ return false unless domain
+ return true if disable_requested?
+ return true unless domain.dnskeys.where(public_key: dnskey.public_key).any?
+
+ errors.add(:public_key, 'already tied to this domain')
+ end
+
+ def self.by_domain_name(domain_name)
+ domain = Domain.find_by(name: domain_name)
+ log.info "CsyncRecord: '#{domain_name}' not in zone. Not initializing record." unless domain
+ CsyncRecord.find_or_initialize_by(domain: domain) if domain
+ end
+
+ def determine_csync_intention(scan_state)
+ return unless domain.dnskeys.any? && scan_state == 'secure'
+
+ disable_requested? ? 'deactivate' : 'rollover'
+ end
+
+ def initializes_dnssec?(scan_state)
+ true if domain.dnskeys.empty? && !disable_requested? && scan_state == 'insecure'
+ end
+
+ def log
+ @log ||= Rails.env.test? ? logger : Logger.new(STDOUT)
+ @log
+ end
+
+ def validate_csync_action
+ return true if %w[initialized rollover].include? action
+ return true if action == 'deactivate' && disable_requested?
+
+ errors.add(:action, :invalid)
+ end
+
+ def validate_cdnskey_format
+ return true if disable_requested?
+ return true if dnskey.valid?
+
+ errors.add(:cdnskey, :invalid)
+ end
+end
diff --git a/app/models/dnskey.rb b/app/models/dnskey.rb
index 6c6df6e44..58d4d9e31 100644
--- a/app/models/dnskey.rb
+++ b/app/models/dnskey.rb
@@ -28,7 +28,7 @@ class Dnskey < ApplicationRecord
PROTOCOLS = %w(3)
FLAGS = %w(0 256 257) # 256 = ZSK, 257 = KSK
DS_DIGEST_TYPE = [1,2]
-
+ RESOLVERS = ENV['dnssec_resolver_ips'].to_s.strip.split(', ').freeze
self.ignored_columns = %w[legacy_domain_id]
def epp_code_map
@@ -122,6 +122,26 @@ class Dnskey < ApplicationRecord
errors.add(:public_key, :invalid)
end
+ def self.new_from_csync(cdnskey:, domain:)
+ cdnskey ||= '' # avoid strip() issues for gibberish key
+
+ flags, proto, alg, pub = cdnskey.strip.split(' ')
+ Dnskey.new(domain: domain, flags: flags, protocol: proto, alg: alg, public_key: pub)
+ end
+
+ def ds_rr
+ # Break the DNSSEC trust chain as we are not able to fake RRSIG's
+ Dnsruby::Dnssec.clear_trust_anchors
+ Dnsruby::Dnssec.clear_trusted_keys
+
+ # Basically let's configure domain as root anchor. We can still verify
+ # RRSIG's / DNSKEY targeted by DS of this domain
+ generate_digest
+ generate_ds_key_tag
+ Dnsruby::RR.create("#{domain.name}. 3600 IN DS #{ds_key_tag} #{ds_alg} " \
+ "#{ds_digest_type} #{ds_digest}")
+ end
+
class << self
def int_to_hex(s)
s = s.to_s(16)
diff --git a/app/models/domain.rb b/app/models/domain.rb
index 5f32e02e2..3deaf594b 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -58,6 +58,7 @@ class Domain < ApplicationRecord
has_many :legal_documents, as: :documentable
has_many :registrant_verifications, dependent: :destroy
+ has_one :csync_record, dependent: :destroy
after_initialize do
self.pending_json = {} if pending_json.blank?
@@ -167,6 +168,35 @@ class Domain < ApplicationRecord
validate :validate_nameserver_ips
validate :statuses_uniqueness
+
+ def security_level_resolver
+ resolver = Dnsruby::Resolver.new(nameserver: Dnskey::RESOLVERS)
+ resolver.do_validation = true
+ resolver.do_caching = false
+ resolver.dnssec = true
+ resolver
+ end
+
+ def dnssec_security_level(stubber: nil)
+ Dnsruby::Dnssec.reset
+ resolver = security_level_resolver
+ Dnsruby::Recursor.clear_caches(resolver)
+ if Rails.env.staging?
+ clear_dnssec_trusted_anchors_and_keys
+ elsif stubber
+ Dnsruby::Dnssec.add_trust_anchor(stubber.ds_rr)
+ end
+ recursor = Dnsruby::Recursor.new(resolver)
+ recursor.dnssec = true
+ recursor.query(name, 'A', 'IN').security_level
+ end
+
+ def clear_dnssec_trusted_anchors_and_keys
+ Dnsruby::Dnssec.clear_trust_anchors
+ Dnsruby::Dnssec.clear_trusted_keys
+ Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ENV['trusted_dnskey']))
+ end
+
def statuses_uniqueness
return if statuses.uniq == statuses
errors.add(:statuses, :taken)
diff --git a/app/models/epp/contact.rb b/app/models/epp/contact.rb
index 4cd876d5f..6bd5507b9 100644
--- a/app/models/epp/contact.rb
+++ b/app/models/epp/contact.rb
@@ -44,6 +44,8 @@ class Epp::Contact < Contact
def check_availability(codes, reg:)
codes = [codes] if codes.is_a?(String)
+ codes = codes.map { |c| c.include?(':') ? c : "#{reg}:#{c}" }
+
res = []
codes.map { |c| c.include?(':') ? c : "#{reg}:#{c}" }.map { |c| c.strip.upcase }.each do |x|
c = find_by_epp_code(x)
diff --git a/app/models/nameserver.rb b/app/models/nameserver.rb
index 85e64ce41..fff757b95 100644
--- a/app/models/nameserver.rb
+++ b/app/models/nameserver.rb
@@ -58,8 +58,8 @@ class Nameserver < ApplicationRecord
end
def hostname=(hostname)
- self[:hostname] = SimpleIDN.to_unicode(hostname)
- self[:hostname_puny] = SimpleIDN.to_ascii(hostname)
+ self[:hostname] = SimpleIDN.to_unicode(hostname).gsub(/\.+$/, '')
+ self[:hostname_puny] = SimpleIDN.to_ascii(hostname).gsub(/\.+$/, '')
end
class << self
diff --git a/app/views/mailers/csync_mailer/dnssec_deleted.html.erb b/app/views/mailers/csync_mailer/dnssec_deleted.html.erb
new file mode 100644
index 000000000..5729c5e02
--- /dev/null
+++ b/app/views/mailers/csync_mailer/dnssec_deleted.html.erb
@@ -0,0 +1,14 @@
+Tere,
+
+Oleme eemaldanud Teie domeeni <%= @domain.name %> DNSSEC kirjed registrist.
+
+Domeeni <%= @domain.name %> DNSSEC on nüüd välja lülitatud.
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+
+
+Hi,
+
+We have removed DNSSEC data in our registry for domain <%= @domain.name %>.
+
+DNSSEC has been turned off for <%= @domain.name %> as of now.
+<%= render 'mailers/shared/signatures/signature.en.html' %>
diff --git a/app/views/mailers/csync_mailer/dnssec_updated.html.erb b/app/views/mailers/csync_mailer/dnssec_updated.html.erb
new file mode 100644
index 000000000..916154f72
--- /dev/null
+++ b/app/views/mailers/csync_mailer/dnssec_updated.html.erb
@@ -0,0 +1,14 @@
+Tere,
+
+Oleme uuendatud Teie domeeni <%= @domain.name %> DNSSEC andmeid registris.
+
+Lisasime CDNSKEY väärtuses kajastatud võtme oma tsoonifaili ning on nüüd aktiveeritud.
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+
+
+Hi,
+
+We have updated DNSSEC data in our registry for domain <%= @domain.name %>.
+
+DNS key specified in CDNSKEY has been added to our zone file and is now taking effect.
+<%= render 'mailers/shared/signatures/signature.en.html' %>
diff --git a/config/application.yml.sample b/config/application.yml.sample
index 88fc28a0d..02396aad3 100644
--- a/config/application.yml.sample
+++ b/config/application.yml.sample
@@ -39,8 +39,9 @@ ca_key_path: '/home/registry/registry/shared/ca/private/ca.key.pem'
ca_key_password: 'your-root-key-password'
directo_invoice_url: 'https://domain/ddddd.asp'
-
-
+cdns_scanner_input_file: '/opt/cdns/input.txt'
+cdns_scanner_output_file: '/opt/cdns/output.txt'
+dnssec_resolver_ips: 8.8.8.8, 8.8.4.4
#
# EPP
#
@@ -192,6 +193,10 @@ test:
action_mailer_force_delete_from: 'legal@registry.test'
lhv_p12_keystore: 'test/fixtures/files/keystore.p12'
lhv_keystore_password: 'testtest'
+ lhv_keystore_alias: 'testtest'
+ cdns_scanner_input_file: 'tmp/cdns_input.txt'
+ cdns_scanner_output_file: 'test/fixtures/files/cdns_output.txt'
+ dnssec_resolver_ips: 8.8.8.8, 8.8.4.4
legal_documents_dir: 'test/fixtures/files'
# Airbrake // Errbit:
diff --git a/config/locales/admin/menu.en.yml b/config/locales/admin/menu.en.yml
index 52f0a210b..8fd8bdc07 100644
--- a/config/locales/admin/menu.en.yml
+++ b/config/locales/admin/menu.en.yml
@@ -16,6 +16,7 @@ en:
disputed_domains: Disputed domains
bulk_actions: Bulk actions
bounced_email_addresses: Bounced emails
+ mass_actions: Mass actions
epp_log: EPP log
repp_log: REPP log
que: Que
diff --git a/config/locales/admin/repp_logs.en.yml b/config/locales/admin/repp_logs.en.yml
index 2436f849a..0a58fe7ba 100644
--- a/config/locales/admin/repp_logs.en.yml
+++ b/config/locales/admin/repp_logs.en.yml
@@ -6,3 +6,4 @@ en:
reset_btn: Reset
show:
title: REPP log
+
diff --git a/config/locales/mailers/csync.en.yml b/config/locales/mailers/csync.en.yml
new file mode 100644
index 000000000..06f051689
--- /dev/null
+++ b/config/locales/mailers/csync.en.yml
@@ -0,0 +1,10 @@
+en:
+ csync_mailer:
+ dnssec_updated:
+ subject: >-
+ Teie domeeni %{domain_name} DNSSEC andmed on uuendatud
+ / DNSSEC data for %{domain_name} has been updated
+ dnssec_deleted:
+ subject: >-
+ Teie domeeni %{domain_name} DNSSEC andmed on eemaldatud
+ / DNSSEC data for %{domain_name} has been removed
diff --git a/config/locales/notifications.en.yml b/config/locales/notifications.en.yml
index d67fb5a5b..b5c1dfd47 100644
--- a/config/locales/notifications.en.yml
+++ b/config/locales/notifications.en.yml
@@ -6,5 +6,6 @@ en:
It was associated with registrant %{old_registrant_code}
and contacts %{old_contacts_codes}.
contact_update: Contact %{contact} has been updated by registrant
+ csync: CSYNC DNSSEC %{action} for domain %{domain}
registrar_locked: Domain %{domain_name} has been locked by registrant
registrar_unlocked: Domain %{domain_name} has been unlocked by registrant
diff --git a/db/migrate/20200605125332_create_csync_records.rb b/db/migrate/20200605125332_create_csync_records.rb
new file mode 100644
index 000000000..69f6f09c8
--- /dev/null
+++ b/db/migrate/20200605125332_create_csync_records.rb
@@ -0,0 +1,12 @@
+class CreateCsyncRecords < ActiveRecord::Migration[6.0]
+ def change
+ create_table :csync_records do |t|
+ t.belongs_to :domain, foreign_key: true, null: false, index: { unique: true }
+ t.string :cdnskey, null: false
+ t.string :action, null: false
+ t.integer :times_scanned, null: false, default: 0
+ t.datetime :last_scan, null: false
+ t.timestamps
+ end
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index de2243597..a28f7fabd 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1988,6 +1988,40 @@ CREATE TABLE public.log_users (
uuid character varying
);
+--
+-- Name: csync_records; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.csync_records (
+ id bigint NOT NULL,
+ domain_id bigint NOT NULL,
+ cdnskey character varying NOT NULL,
+ action character varying NOT NULL,
+ times_scanned integer DEFAULT 0 NOT NULL,
+ last_scan timestamp without time zone NOT NULL,
+ created_at timestamp(6) without time zone NOT NULL,
+ updated_at timestamp(6) without time zone NOT NULL
+);
+
+
+--
+-- Name: csync_records_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.csync_records_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: csync_records_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.csync_records_id_seq OWNED BY public.csync_records.id;
+
--
-- Name: log_users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
@@ -2251,6 +2285,12 @@ CREATE TABLE public.registrant_verifications (
action character varying NOT NULL,
domain_id integer NOT NULL,
action_type character varying NOT NULL,
+ creator_id integer,
+ updater_id integer,
+ session character varying,
+ children json,
+ ident_updated_at timestamp without time zone,
+ uuid character varying,
creator_str character varying,
updator_str character varying
);
@@ -3017,21 +3057,40 @@ ALTER TABLE ONLY public.que_jobs ALTER COLUMN job_id SET DEFAULT nextval('public
ALTER TABLE ONLY public.registrant_verifications ALTER COLUMN id SET DEFAULT nextval('public.registrant_verifications_id_seq'::regclass);
-
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
+ALTER TABLE ONLY public.accounts ALTER COLUMN id SET DEFAULT nextval('public.accounts_id_seq'::regclass);
+
+
+ALTER TABLE ONLY public.log_invoices
+ ADD CONSTRAINT log_invoices_pkey PRIMARY KEY (id);
+
+
ALTER TABLE ONLY public.registrars ALTER COLUMN id SET DEFAULT nextval('public.registrars_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
+ALTER TABLE ONLY public.disputes ALTER COLUMN id SET DEFAULT nextval('public.disputes_id_seq'::regclass);
+
+ALTER TABLE ONLY public.log_nameservers
+ ADD CONSTRAINT log_nameservers_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.reserved_domains ALTER COLUMN id SET DEFAULT nextval('public.reserved_domains_id_seq'::regclass);
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.setting_entries ALTER COLUMN id SET DEFAULT nextval('public.setting_entries_id_seq'::regclass);
+
+
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -3050,6 +3109,285 @@ ALTER TABLE ONLY public.settings ALTER COLUMN id SET DEFAULT nextval('public.set
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
+ALTER TABLE ONLY public.domains ALTER COLUMN id SET DEFAULT nextval('public.domains_id_seq'::regclass);
+
+
+--
+-- Name: versions id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.email_address_verifications ALTER COLUMN id SET DEFAULT nextval('public.email_address_verifications_id_seq'::regclass);
+
+
+--
+-- Name: white_ips id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.email_addresses_validations ALTER COLUMN id SET DEFAULT nextval('public.email_addresses_validations_id_seq'::regclass);
+
+
+--
+-- Name: whois_records id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.email_addresses_verifications ALTER COLUMN id SET DEFAULT nextval('public.email_addresses_verifications_id_seq'::regclass);
+
+
+--
+-- Name: zones id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.epp_sessions ALTER COLUMN id SET DEFAULT nextval('public.epp_sessions_id_seq'::regclass);
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.invoice_items ALTER COLUMN id SET DEFAULT nextval('public.invoice_items_id_seq'::regclass);
+
+
+--
+-- Name: invoices id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.invoices ALTER COLUMN id SET DEFAULT nextval('public.invoices_id_seq'::regclass);
+
+
+--
+-- Name: legal_documents id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.legal_documents ALTER COLUMN id SET DEFAULT nextval('public.legal_documents_id_seq'::regclass);
+
+
+--
+-- Name: log_account_activities id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_account_activities ALTER COLUMN id SET DEFAULT nextval('public.log_account_activities_id_seq'::regclass);
+
+
+--
+-- Name: log_accounts id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_accounts ALTER COLUMN id SET DEFAULT nextval('public.log_accounts_id_seq'::regclass);
+
+
+--
+-- Name: log_actions id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_actions ALTER COLUMN id SET DEFAULT nextval('public.log_actions_id_seq'::regclass);
+
+
+--
+-- Name: log_bank_statements id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_bank_statements ALTER COLUMN id SET DEFAULT nextval('public.log_bank_statements_id_seq'::regclass);
+
+
+--
+-- Name: log_bank_transactions id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_bank_transactions ALTER COLUMN id SET DEFAULT nextval('public.log_bank_transactions_id_seq'::regclass);
+
+
+--
+-- Name: log_blocked_domains id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_blocked_domains ALTER COLUMN id SET DEFAULT nextval('public.log_blocked_domains_id_seq'::regclass);
+
+
+--
+-- Name: log_certificates id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_certificates ALTER COLUMN id SET DEFAULT nextval('public.log_certificates_id_seq'::regclass);
+
+
+--
+-- Name: log_contacts id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_contacts ALTER COLUMN id SET DEFAULT nextval('public.log_contacts_id_seq'::regclass);
+
+
+--
+-- Name: log_dnskeys id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_dnskeys ALTER COLUMN id SET DEFAULT nextval('public.log_dnskeys_id_seq'::regclass);
+
+
+--
+-- Name: log_domain_contacts id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_domain_contacts ALTER COLUMN id SET DEFAULT nextval('public.log_domain_contacts_id_seq'::regclass);
+
+
+--
+-- Name: log_domains id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_domains ALTER COLUMN id SET DEFAULT nextval('public.log_domains_id_seq'::regclass);
+
+
+--
+-- Name: log_invoice_items id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_invoice_items ALTER COLUMN id SET DEFAULT nextval('public.log_invoice_items_id_seq'::regclass);
+
+
+--
+-- Name: log_invoices id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_invoices ALTER COLUMN id SET DEFAULT nextval('public.log_invoices_id_seq'::regclass);
+
+
+--
+-- Name: log_nameservers id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_nameservers ALTER COLUMN id SET DEFAULT nextval('public.log_nameservers_id_seq'::regclass);
+
+
+--
+-- Name: log_notifications id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_notifications ALTER COLUMN id SET DEFAULT nextval('public.log_notifications_id_seq'::regclass);
+
+
+--
+-- Name: log_payment_orders id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_payment_orders ALTER COLUMN id SET DEFAULT nextval('public.log_payment_orders_id_seq'::regclass);
+
+
+--
+-- Name: log_registrant_verifications id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_registrant_verifications ALTER COLUMN id SET DEFAULT nextval('public.log_registrant_verifications_id_seq'::regclass);
+
+
+--
+-- Name: log_registrars id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_registrars ALTER COLUMN id SET DEFAULT nextval('public.log_registrars_id_seq'::regclass);
+
+
+--
+-- Name: log_reserved_domains id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_reserved_domains ALTER COLUMN id SET DEFAULT nextval('public.log_reserved_domains_id_seq'::regclass);
+
+
+--
+-- Name: log_settings id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_settings ALTER COLUMN id SET DEFAULT nextval('public.log_settings_id_seq'::regclass);
+
+
+--
+-- Name: log_users id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_users ALTER COLUMN id SET DEFAULT nextval('public.log_users_id_seq'::regclass);
+
+
+--
+-- Name: log_white_ips id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.log_white_ips ALTER COLUMN id SET DEFAULT nextval('public.log_white_ips_id_seq'::regclass);
+
+
+--
+-- Name: nameservers id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.nameservers ALTER COLUMN id SET DEFAULT nextval('public.nameservers_id_seq'::regclass);
+
+
+--
+-- Name: notifications id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass);
+
+
+--
+-- Name: payment_orders id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.payment_orders ALTER COLUMN id SET DEFAULT nextval('public.payment_orders_id_seq'::regclass);
+
+
+--
+-- Name: prices id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.prices ALTER COLUMN id SET DEFAULT nextval('public.prices_id_seq'::regclass);
+
+
+--
+-- Name: que_jobs job_id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.que_jobs ALTER COLUMN job_id SET DEFAULT nextval('public.que_jobs_job_id_seq'::regclass);
+
+
+--
+-- Name: registrant_verifications id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.registrant_verifications ALTER COLUMN id SET DEFAULT nextval('public.registrant_verifications_id_seq'::regclass);
+
+
+--
+-- Name: registrars id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.registrars ALTER COLUMN id SET DEFAULT nextval('public.registrars_id_seq'::regclass);
+
+
+--
+-- Name: reserved_domains id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.reserved_domains ALTER COLUMN id SET DEFAULT nextval('public.reserved_domains_id_seq'::regclass);
+
+
+--
+-- Name: settings id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.settings ALTER COLUMN id SET DEFAULT nextval('public.settings_id_seq'::regclass);
+
+
+--
+-- Name: users id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
+
+ALTER TABLE ONLY public.log_prices
+ ADD CONSTRAINT log_prices_pkey PRIMARY KEY (id);
+
+
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
@@ -3369,22 +3707,6 @@ ALTER TABLE ONLY public.log_invoice_items
ADD CONSTRAINT log_invoice_items_pkey PRIMARY KEY (id);
---
--- Name: log_invoices_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
---
-
-ALTER TABLE ONLY public.log_invoices
- ADD CONSTRAINT log_invoices_pkey PRIMARY KEY (id);
-
-
---
--- Name: log_nameservers_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
---
-
-ALTER TABLE ONLY public.log_nameservers
- ADD CONSTRAINT log_nameservers_pkey PRIMARY KEY (id);
-
-
--
-- Name: log_notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -3401,14 +3723,6 @@ ALTER TABLE ONLY public.log_payment_orders
ADD CONSTRAINT log_payment_orders_pkey PRIMARY KEY (id);
---
--- Name: log_prices_pkey; Type: CONSTRAINT; Schema: public; Owner: -
---
-
-ALTER TABLE ONLY public.log_prices
- ADD CONSTRAINT log_prices_pkey PRIMARY KEY (id);
-
-
--
-- Name: log_registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -4006,6 +4320,17 @@ CREATE INDEX index_log_contacts_on_whodunnit ON public.log_contacts USING btree
CREATE INDEX index_log_dnskeys_on_item_type_and_item_id ON public.log_dnskeys USING btree (item_type, item_id);
+--
+-- Name: csync_records id; Type: DEFAULT; Schema: public; Owner: -
+--
+ALTER TABLE ONLY public.csync_records ALTER COLUMN id SET DEFAULT nextval('public.csync_records_id_seq'::regclass);
+
+--
+-- Name: index_csync_records_on_domain_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_csync_records_on_domain_id ON public.csync_records USING btree (domain_id);
+
--
-- Name: index_log_dnskeys_on_whodunnit; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
@@ -4313,9 +4638,8 @@ CREATE UNIQUE INDEX unique_data_migrations ON public.data_migrations USING btree
CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version);
-
--
--- Name: contacts_registrar_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
+-- Name: contacts contacts_registrar_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.contacts
@@ -4521,6 +4845,19 @@ ALTER TABLE ONLY public.nameservers
ALTER TABLE ONLY public.users
ADD CONSTRAINT user_registrar_id_fk FOREIGN KEY (registrar_id) REFERENCES public.registrars(id);
+--
+-- Name: csync_records csync_records_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.csync_records
+ ADD CONSTRAINT csync_records_pkey PRIMARY KEY (id);
+
+--
+-- Name: csync_records fk_rails_5df85aeb13; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.csync_records
+ ADD CONSTRAINT fk_rails_5df85aeb13 FOREIGN KEY (domain_id) REFERENCES public.domains(id);
--
-- PostgreSQL database dump complete
@@ -4951,6 +5288,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200605100827'),
('20200610090110'),
('20200630081231'),
+('20200605125332'),
('20200714115338'),
('20200807110611'),
('20200811074839'),
@@ -4961,6 +5299,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200910085157'),
('20200910102028'),
('20200916125326'),
-('20210215101019');
-
-
+('20200917104213'),
+('20210215101019'),
+('20200921084356');
diff --git a/test/fixtures/dnskeys.yml b/test/fixtures/dnskeys.yml
new file mode 100644
index 000000000..218c14ba6
--- /dev/null
+++ b/test/fixtures/dnskeys.yml
@@ -0,0 +1,10 @@
+one:
+ domain:
+ flags: 257
+ protocol: 3
+ alg: 13
+ public_key: mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
+ ds_key_tag: 2371
+ ds_alg: 13
+ ds_digest_type: 2
+ ds_digest: 39456058862EA09DD96992ED2BDAFAEDE8C7E949589E3DA903A46F4F9CD373EA
diff --git a/test/fixtures/files/cdns_output.txt b/test/fixtures/files/cdns_output.txt
new file mode 100644
index 000000000..9299ec195
--- /dev/null
+++ b/test/fixtures/files/cdns_output.txt
@@ -0,0 +1,4 @@
+insecure-empty ns1.bestnames.test 127.0.0.1 airport.test
+insecure-empty ns2.bestnames.test 127.0.0.1 airport.test
+insecure ns1.bestnames.test 127.0.0.1 shop.test 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
+insecure ns2.bestnames.test 127.0.0.1 shop.test 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
diff --git a/test/jobs/csync_job_test.rb b/test/jobs/csync_job_test.rb
new file mode 100644
index 000000000..8819fd5c7
--- /dev/null
+++ b/test/jobs/csync_job_test.rb
@@ -0,0 +1,73 @@
+require 'test_helper'
+
+class CsyncJobTest < ActiveSupport::TestCase
+ include ActionMailer::TestHelper
+
+ setup do
+ @dnskey = dnskeys(:one)
+ @domain = domains(:shop)
+ end
+
+ def test_generates_input_file_for_cdnskey_scanner
+ @dnskey.update(domain: domains(:shop))
+
+ expected_contents = "[secure]\nns1.bestnames.test shop.test\nns2.bestnames.test shop.test\n" \
+ "[insecure]\nns1.bestnames.test airport.test metro.test\nns2.bestnames.test airport.test\n"
+
+ CsyncJob.run(generate: true)
+
+ assert_equal expected_contents, IO.read(ENV['cdns_scanner_input_file'])
+ end
+
+ def test_creates_csync_record_when_new_cdnskey_discovered
+ assert_nil @domain.csync_record
+ CsyncJob.run
+
+ @domain.reload
+ assert @domain.csync_record
+ csync_record = @domain.csync_record
+ assert_equal 1, csync_record.times_scanned
+ assert_equal '257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==', csync_record.cdnskey
+
+ assert_not @domain.dnskeys.any?
+ end
+
+ def test_creates_dnskey_after_required_cycles
+ assert_equal 0, @domain.dnskeys.count
+ assert_nil @domain.csync_record
+ CsyncJob.run # Creates initial CsyncRecord for domain
+
+ @domain.reload
+ assert @domain.csync_record.present?
+
+ @domain.csync_record.update(times_scanned: 2) # 3rd time trigger DNSKEY push
+ assert_equal 0, @domain.dnskeys.count
+ assert_equal 2, @domain.csync_record.times_scanned
+
+ CsyncRecord.stub :by_domain_name, @domain.csync_record do
+ @domain.csync_record.stub :dnssec_validates?, true do
+ CsyncJob.run
+ end
+ end
+
+ @domain.reload
+ assert_equal 1, @domain.dnskeys.count
+ assert_equal 'mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==', @domain.dnskeys.last.public_key
+ assert_nil @domain.csync_record
+ end
+
+ def test_sends_mail_to_contacts_if_dnskey_updated
+ assert_emails 1 do
+ CsyncJob.run
+ @domain.reload
+
+ CsyncRecord.stub :by_domain_name, @domain.csync_record do
+ @domain.csync_record.stub :dnssec_validates?, true do
+ 2.times do
+ CsyncJob.run
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/models/contact_test.rb b/test/models/contact_test.rb
index 942b71b06..e6dc2422f 100644
--- a/test/models/contact_test.rb
+++ b/test/models/contact_test.rb
@@ -291,37 +291,45 @@ class ContactTest < ActiveSupport::TestCase
assert_equal 'US', contact.country_code
end
- def test_normalizes_ident_country_code
- contact = Contact.new(ident_country_code: 'us')
- contact.validate
- assert_equal 'US', contact.ident_country_code
+ def test_linked_scope_returns_contact_that_acts_as_admin_contact
+ domains(:shop).admin_contacts = [@contact]
+ assert Contact.linked.include?(@contact), 'Contact should be included'
end
- def test_generates_code
- contact = Contact.new(registrar: registrars(:bestnames))
- assert_nil contact.code
-
- contact.generate_code
-
- assert_not_empty contact.code
+ def test_linked_scope_returns_contact_that_acts_as_tech_contact
+ domains(:shop).tech_contacts = [@contact]
+ assert Contact.linked.include?(@contact), 'Contact should be included'
end
- def test_prohibits_code_change
- assert_no_changes -> { @contact.code } do
- @contact.code = 'new'
- @contact.save!
- @contact.reload
- end
+ def test_linked_scope_skips_unlinked_contact
+ contact = unlinked_contact
+ assert_not Contact.linked.include?(contact), 'Contact should be excluded'
end
- def test_removes_duplicate_statuses
- contact = Contact.new(statuses: %w[ok ok])
- assert_equal %w[ok], contact.statuses
+ def test_unlinked_scope_returns_unlinked_contact
+ contact = unlinked_contact
+ assert Contact.unlinked.include?(contact), 'Contact should be included'
end
- def test_default_status
- contact = Contact.new
- assert_equal %w[ok], contact.statuses
+ def test_unlinked_scope_skips_contact_that_is_linked_as_registrant
+ contact = unlinked_contact
+ domains(:shop).update_columns(registrant_id: contact.becomes(Registrant).id)
+
+ assert Contact.unlinked.exclude?(contact), 'Contact should be excluded'
+ end
+
+ def test_unlinked_scope_skips_contact_that_is_linked_as_admin_contact
+ contact = unlinked_contact
+ domains(:shop).admin_contacts = [contact]
+
+ assert Contact.unlinked.exclude?(contact), 'Contact should be excluded'
+ end
+
+ def test_unlinked_scope_skips_contact_that_is_linked_as_tech_contact
+ contact = unlinked_contact
+ domains(:shop).tech_contacts = [contact]
+
+ assert Contact.unlinked.exclude?(contact), 'Contact should be excluded'
end
def test_whois_gets_updated_after_contact_save
diff --git a/test/models/csync_record_test.rb b/test/models/csync_record_test.rb
new file mode 100644
index 000000000..9c044e63d
--- /dev/null
+++ b/test/models/csync_record_test.rb
@@ -0,0 +1,289 @@
+require 'test_helper'
+
+class CsyncRecordTest < ActiveSupport::TestCase
+ setup do
+ @domain = domains(:shop)
+
+ @csync_record = CsyncRecord.new
+ @csync_record.cdnskey = '257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ=='
+ @csync_record.action = 'initialized'
+ @csync_record.domain = domains(:shop)
+ @csync_record.last_scan = Time.now
+ end
+
+ def test_domain_must_be_present
+ assert @csync_record.valid?
+ @csync_record.domain = nil
+ assert_not @csync_record.valid?
+ end
+
+ def test_action_must_be_present_and_valid
+ @csync_record.action = nil
+ assert_not @csync_record.valid?
+
+ @csync_record.action = 'definitely invalid action'
+ assert_not @csync_record.valid?
+
+ @csync_record.action = 'initialized'
+ assert @csync_record.valid?
+
+ @csync_record.action = 'rollover'
+ assert @csync_record.valid?
+
+ @csync_record.action = 'deactivate'
+ assert_not @csync_record.valid?
+
+ @csync_record.action = 'deactivate'
+ @csync_record.cdnskey = '0 3 0 AA=='
+ assert @csync_record.valid?
+ end
+
+ def test_cdnskey_must_be_unique_for_domain
+ dnskey = @csync_record.dnskey
+ dnskey.save!
+
+ assert_not @csync_record.valid?
+ assert_includes @csync_record.errors.full_messages, 'Public key already tied to this domain'
+
+ @csync_record.cdnskey = nil
+ assert_not @csync_record.valid?
+ assert_includes @csync_record.errors.full_messages, 'Cdnskey is missing'
+ end
+
+ def test_cdnskey_must_be_parsable
+ @csync_record.cdnskey = 'gibberish'
+ assert_not @csync_record.valid?
+
+ @csync_record.cdnskey = nil
+ assert_not @csync_record.valid?
+ @csync_record.cdnskey = ''
+ assert_not @csync_record.valid?
+
+ @csync_record.cdnskey = '257 3 13 KlHFYV42UtxC7LpsolDpoUZ9DNPDRYQypalBRIqlubBg/zg78aqciLk+NaWUbrkN7AUaM7h7tx91sLN+ORVPxA=='
+ assert @csync_record.valid?
+ end
+
+ def test_initializes_valid_record_with_scanner_input
+ scanner_result = {
+ type: 'insecure', ns: 'ns1.bestnames.test', ns_ip: '127.0.0.1', flags: '257', proto: '3', alg: '13',
+ pub: 'mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==',
+ cdnskey: '257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ=='
+ }
+
+ csync_record = CsyncRecord.by_domain_name(@domain.name)
+ csync_record.assign_scanner_data!(scanner_result)
+
+ assert csync_record.valid?
+ assert_equal scanner_result[:cdnskey], csync_record.cdnskey
+ assert_equal 'initialized', csync_record.action
+ assert_equal 1, csync_record.times_scanned
+ end
+
+ def test_creates_rollover_record_with_scanner_input
+ # Rollover always requires valid dnssec conf beforehand
+ dnskey = @csync_record.dnskey
+ dnskey.save!
+
+ # Type 'secure' reflects from cdnskey-scanner that custom domain security level is SECURE (dnssec valid)
+ scanner_result = {
+ type: 'secure', ns: 'ns1.bestnames.test', ns_ip: '127.0.0.1', flags: '256', proto: '3', alg: '13',
+ pub: 'PiVTNvqOTrCSoXf5obNEPrDe0yhrKPmjyv+MWfoscBHF49rRIH1/yDdAXY3SyUD86qq/AiXDzsTQIqOjvak7gw==',
+ cdnskey: '256 3 13 PiVTNvqOTrCSoXf5obNEPrDe0yhrKPmjyv+MWfoscBHF49rRIH1/yDdAXY3SyUD86qq/AiXDzsTQIqOjvak7gw=='
+ }
+
+ csync_record = CsyncRecord.by_domain_name(@domain.name)
+ csync_record.assign_scanner_data!(scanner_result)
+ assert_equal 'rollover', csync_record.action
+ assert csync_record.valid?
+
+ scanner_result[:type] = 'insecure'
+ csync_record.assign_scanner_data!(scanner_result)
+ assert_not csync_record.valid?
+ assert_includes csync_record.errors.full_messages, 'Action is invalid'
+ end
+
+ def test_creates_deactivate_record_with_scanner_input
+ # Deactivate always requires valid dnssec conf beforehand
+ dnskey = @csync_record.dnskey
+ dnskey.save!
+
+ # Type 'secure' reflects from cdnskey-scanner that custom domain security level is SECURE (dnssec valid)
+ scanner_result = {
+ type: 'secure', ns: 'ns1.bestnames.test', ns_ip: '127.0.0.1', flags: '0', proto: '3', alg: '0',
+ pub: 'AA==',
+ cdnskey: '0 3 0 AA=='
+ }
+
+ csync_record = CsyncRecord.by_domain_name(@domain.name)
+ csync_record.assign_scanner_data!(scanner_result)
+ assert_equal 'deactivate', csync_record.action
+ assert csync_record.valid?
+
+ scanner_result[:type] = 'insecure'
+ csync_record.assign_scanner_data!(scanner_result)
+ assert_not csync_record.valid?
+ assert_includes csync_record.errors.full_messages, 'Action is invalid'
+ end
+
+ def test_initializes_dnssec_for_domain
+ scanner_result = {
+ type: 'insecure', ns: 'ns1.bestnames.test', ns_ip: '127.0.0.1', flags: '257', proto: '3', alg: '13',
+ pub: 'mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==',
+ cdnskey: '257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ=='
+ }
+
+ 2.times { CsyncRecord.by_domain_name(@domain.name).record_new_scan(scanner_result) }
+ @domain.reload
+
+ CsyncRecord.stub :by_domain_name, @domain.csync_record do
+ @domain.csync_record.stub :dnssec_validates?, true do
+ CsyncRecord.by_domain_name(@domain.name).record_new_scan(scanner_result)
+ end
+ end
+
+ assert_equal 1, @domain.dnskeys.count
+ assert_equal scanner_result[:pub], @domain.dnskeys.last.public_key
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal (@domain.contacts.map(&:email) << @domain.registrant.email).uniq, mail.to
+ assert_equal mail.subject, "Teie domeeni #{@domain.name} DNSSEC andmed on uuendatud / DNSSEC data for #{@domain.name} has been updated"
+ end
+
+ def test_rollovers_dnssec_for_domain
+ dnskey = @csync_record.dnskey
+ dnskey.save!
+
+ scanner_result = {
+ type: 'secure', ns: 'ns1.bestnames.test', ns_ip: '127.0.0.1', flags: '256', proto: '3', alg: '13',
+ pub: 'PiVTNvqOTrCSoXf5obNEPrDe0yhrKPmjyv+MWfoscBHF49rRIH1/yDdAXY3SyUD86qq/AiXDzsTQIqOjvak7gw==',
+ cdnskey: '256 3 13 PiVTNvqOTrCSoXf5obNEPrDe0yhrKPmjyv+MWfoscBHF49rRIH1/yDdAXY3SyUD86qq/AiXDzsTQIqOjvak7gw=='
+ }
+
+ stub_any_instance(CsyncRecord, :dnssec_validates?, true) do
+ CsyncRecord.by_domain_name(@domain.name).record_new_scan(scanner_result)
+ end
+
+ assert_equal 1, @domain.dnskeys.count
+ assert_equal scanner_result[:pub], @domain.dnskeys.last.public_key
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal (@domain.contacts.map(&:email) << @domain.registrant.email).uniq, mail.to
+ assert_equal mail.subject, "Teie domeeni #{@domain.name} DNSSEC andmed on uuendatud / DNSSEC data for #{@domain.name} has been updated"
+ end
+
+ def test_deactivates_dnssec_for_domain
+ dnskey = @csync_record.dnskey
+ dnskey.save!
+
+ scanner_result = {
+ type: 'secure', ns: 'ns1.bestnames.test', ns_ip: '127.0.0.1', flags: '0', proto: '3', alg: '0',
+ pub: 'AA==',
+ cdnskey: '0 3 0 AA=='
+ }
+
+ stub_any_instance(CsyncRecord, :dnssec_validates?, true) do
+ CsyncRecord.by_domain_name(@domain.name).record_new_scan(scanner_result)
+ end
+
+ assert @domain.dnskeys.empty?
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal (@domain.contacts.map(&:email) << @domain.registrant.email).uniq, mail.to
+ assert_equal mail.subject, "Teie domeeni #{@domain.name} DNSSEC andmed on eemaldatud / DNSSEC data for #{@domain.name} has been removed"
+ end
+
+ def test_validates_security_level_for_initialize_action
+ @csync_record.action = 'initialized'
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.INSECURE do
+ assert @csync_record.valid_pre_action?
+ assert_not @csync_record.valid_post_action?
+ end
+
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.BOGUS do
+ assert @csync_record.valid_pre_action?
+ assert_not @csync_record.valid_post_action?
+ end
+
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.SECURE do
+ assert @csync_record.valid_post_action?
+ assert_not @csync_record.valid_pre_action?
+ end
+ end
+
+ def test_validates_security_level_for_rollover_action
+ @csync_record.action = 'rollover'
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.INSECURE do
+ assert_not @csync_record.valid_pre_action?
+ assert_not @csync_record.valid_post_action?
+ end
+
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.BOGUS do
+ assert_not @csync_record.valid_pre_action?
+ assert_not @csync_record.valid_post_action?
+ end
+
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.SECURE do
+ assert @csync_record.valid_pre_action?
+ assert @csync_record.valid_post_action?
+ end
+ end
+
+ def test_validates_security_level_for_deactivate_action
+ @csync_record.action = 'deactivate'
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.INSECURE do
+ assert @csync_record.valid_post_action?
+ assert_not @csync_record.valid_pre_action?
+ end
+
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.BOGUS do
+ assert @csync_record.valid_post_action?
+ assert_not @csync_record.valid_pre_action?
+ end
+
+ @domain.stub :dnssec_security_level, Dnsruby::Message::SecurityLevel.SECURE do
+ assert @csync_record.valid_pre_action?
+ assert_not @csync_record.valid_post_action?
+ end
+ end
+
+ def test_returns_correct_result_if_pre_and_post_actions_succeed
+ stub_any_instance(CsyncRecord, :valid_post_action?, true) do
+ stub_any_instance(CsyncRecord, :valid_pre_action?, true) do
+ assert @csync_record.dnssec_validates?
+ end
+ end
+
+ stub_any_instance(CsyncRecord, :valid_post_action?, false) do
+ stub_any_instance(CsyncRecord, :valid_pre_action?, false) do
+ assert_not @csync_record.dnssec_validates?
+ end
+ end
+ end
+
+ def test_dnssec_validation_fails_if_domain_unreachable
+ @domain.name = 'definitely.not.valid.domain.123'
+ assert_not @csync_record.dnssec_validates?
+ end
+
+ def stub_any_instance(klass, method, value)
+ klass.class_eval do
+ alias_method :"new_#{method}", method
+
+ define_method(method) do
+ if value.respond_to?(:call)
+ value.call
+ else
+ value
+ end
+ end
+ end
+
+ yield
+ ensure
+ klass.class_eval do
+ undef_method method
+ alias_method method, :"new_#{method}"
+ undef_method :"new_#{method}"
+ end
+ end
+end