Page MenuHomeGRNET

No OneTemporary

File Metadata

Created
Sun, May 17, 10:42 AM
diff --git a/app/helpers/domains_helper.rb b/app/helpers/domains_helper.rb
index 597f46a..89db2fb 100644
--- a/app/helpers/domains_helper.rb
+++ b/app/helpers/domains_helper.rb
@@ -1,24 +1,25 @@
module DomainsHelper
# Human names for domain states
def human_state(state)
human = case state.to_sym
when :initial then 'Initial'
when :pending_install then 'Becoming public'
when :pending_signing then 'Signing zone'
when :wait_for_ready then 'Waiting for KSK to become ready'
when :pending_ds then 'Publishing DS records'
+ when :pending_ds_rollover then 'Performing KSK rollover'
when :pending_plain then 'Removing dnssec'
when :pending_remove then 'Preparing removal'
when :operational then 'Operational'
when :destroy then 'Ready to be destroyed'
else
state
end
prog = Domain.dnssec_progress(state)
return human if prog.nil?
"#{human} (#{prog})"
end
end
diff --git a/app/models/domain.rb b/app/models/domain.rb
index 4ad9dd7..c841493 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -1,277 +1,284 @@
class Domain < ActiveRecord::Base
class NotAChild < StandardError; end
self.inheritance_column = :nx
# List all supported domain types.
def self.domain_types
[
'NATIVE',
'MASTER',
'SLAVE',
]
end
# List domain types that can be created.
def self.allowed_domain_types
domain_types - WebDNS.settings[:prohibit_domain_types]
end
# List parent authorities
def self.dnssec_parent_authorities
WebDNS.settings[:dnssec_parent_authorities]
end
# Fire event after transaction commmit
# Changing state inside a hook messes things up,
# this trick handles that
attr_accessor :fire_event
belongs_to :group
has_many :jobs
has_many :records
# BUG in bump_serial_trigger
has_one :soa, -> { unscope(where: :type).where(type: 'soa') }, class_name: SOA
validates :group_id, presence: true
validates :name, uniqueness: true, presence: true
validates :type, presence: true, inclusion: { in: domain_types }
validates :master, presence: true, ipv4: true, if: :slave?
validates :dnssec, inclusion: { in: [false] }, unless: :dnssec_elegible?
validates :dnssec_parent_authority, inclusion: { in: dnssec_parent_authorities }, if: :dnssec?
validates :dnssec_parent, hostname: true, if: :dnssec?
after_create :generate_soa
after_create :generate_ns
after_create :install
before_save :check_convert
after_commit :after_commit_event
attr_writer :serial_strategy
def self.dnssec_progress(current_state)
progress = [
:pending_signing, # 1/3
:wait_for_ready, # 2/3
:pending_ds] # 3/3
idx = progress.index(current_state.to_sym)
return if idx.nil?
[idx+1, progress.size].join('/')
end
state_machine initial: :initial do
after_transition(any => :pending_install) { |domain, _t| Job.add_domain(domain) }
after_transition(any => :pending_remove) { |domain, _t| Job.shutdown_domain(domain) }
after_transition(any => :pending_signing) { |domain, _t| Job.dnssec_sign(domain) }
after_transition(any => :wait_for_ready) { |domain, _t| Job.wait_for_ready(domain) }
after_transition(any => :pending_ds) { |domain, t| Job.dnssec_push_ds(domain, *t.args) }
+ after_transition(any => :pending_ds_rollover) { |domain, t| Job.dnssec_push_ds(domain, *t.args) }
after_transition(any => :pending_plain) { |domain, _t| Job.convert_to_plain(domain) }
after_transition(any => :destroy) { |domain, _t| domain.destroy }
# User events
event :install do
transition initial: :pending_install
end
event :dnssec_sign do
transition operational: :pending_signing
end
event :signed do
transition pending_signing: :wait_for_ready
end
event :push_ds do
- # TODO: push_ds is triggered on multiple occasions
- # operational: :operational
- transition wait_for_ready: :pending_ds
+ transition wait_for_ready: :pending_ds, operational: :pending_ds_rollover
end
event :plain_convert do
transition operational: :pending_plain
end
event :remove do
transition operational: :pending_remove
end
# Machine events
event :installed do
transition pending_install: :operational
end
event :converted do
transition [:pending_ds, :pending_plain] => :operational
end
+ event :complete_rollover do
+ transition pending_ds_rollover: :operational
+ end
+
event :cleaned_up do
transition pending_remove: :destroy
end
+
+ event :ksk_rollover_detected do
+ transition operational: :ksk_rollover
+ end
end
# Returns true if this domain is elegigble for DNSSEC
def dnssec_elegible?
return false if slave?
true
end
# Get the zone's serial strategy.
#
# Returns one of the supported serial strategies.
def serial_strategy
@serial_strategy ||= WebDNS.settings[:serial_strategy]
end
# Returns true if this a reverse zone.
def reverse?
name.end_with?('.in-addr.arpa') || name.end_with?('.ip6.arpa')
end
# Returns true if this a ENUM zone.
def enum?
name.end_with?('.e164.arpa')
end
# Returns true if this is a slave zone.
def slave?
type == 'SLAVE'
end
# Compute subnet for reverse records
def subnet
return if not reverse?
if name.end_with?('.in-addr.arpa')
subnet_v4
elsif name.end_with?('.ip6.arpa')
subnet_v6
end
end
def self.replace_ds(parent, child, records)
parent = find_by_name!(parent)
fail NotAChild if not child.end_with?(parent.name)
existing = parent.records.where(name: child, type: 'DS')
recs = records.map { |rec| DS.new(domain: parent, name: child, content: rec) }
ActiveRecord::Base.transaction do
existing.destroy_all
recs.map(&:save!)
end
end
# Apply bulk to operations to the zones
#
# 1) Deletions
# 2) Changes
# 3) Additions
def bulk(opts)
deletes = opts[:deletes] || []
changes = opts[:changes] || {}
additions = opts[:additions] || {}
errors = Hash.new { |h, k| h[k] = {} }
ActiveRecord::Base.transaction do
# Deletes
to_delete = records.where(id: deletes).index_by(&:id)
deletes.each { |rec_id|
if rec = to_delete[Integer(rec_id)]
rec.destroy
next
end
errors[:deletes][rec_id] = 'Deleted record not found'
}
# Changes
to_change = records.where(id: changes.keys).index_by(&:id)
changes.each {|rec_id, changes|
binding
if rec = to_change[Integer(rec_id)]
errors[:changes][rec_id] = rec.errors.full_messages.join(', ') if !rec.update(changes)
next
end
errors[:changes][rec_id] = 'Changed record not found'
}
# Additions
additions.each { |inc, attrs|
rec = records.new(attrs)
errors[:additions][inc] = rec.errors.full_messages.join(', ') if !rec.save
}
raise ActiveRecord::Rollback if errors.any?
end
errors
end
private
def subnet_v4
# get ip octets (remove .in-addr.arpa)
octets = name.split('.')[0...-2].reverse
return if octets.any? { |_| false }
mask = 8 * octets.size
octets += [0, 0, 0, 0]
ip = IPAddr.new octets[0, 4].join('.')
[ip, mask].join('/')
end
def subnet_v6
nibbles = name.split('.')[0...-2].reverse
return if nibbles.any? { |_| false }
mask = 4 * nibbles.size
nibbles += [0] * 32
ip = IPAddr.new nibbles[0, 32].in_groups_of(4).map(&:join).join(':')
[ip, mask].join('/')
end
# Hooks
def generate_soa
soa_record = SOA.new(domain: self)
soa_record.save!
end
def generate_ns
return if slave?
return if WebDNS.settings[:default_ns].empty?
WebDNS.settings[:default_ns].each { |ns|
Record.find_or_create_by!(domain: self, type: 'NS', name: '', content: ns)
}
end
def check_convert
return if !dnssec_changed?
event = dnssec ? :dnssec_sign : :plain_convert
if state_events.include?(event)
self.fire_event = event # Schedule event for after commit
return true
end
errors.add(:dnssec, 'You cannot modify dnssec settings in this state!')
false
end
def after_commit_event
return if !fire_event
fire_state_event(fire_event)
self.fire_event = nil
end
end
diff --git a/app/models/job.rb b/app/models/job.rb
index fc0db3c..74ba54e 100644
--- a/app/models/job.rb
+++ b/app/models/job.rb
@@ -1,89 +1,101 @@
class Job < ActiveRecord::Base
belongs_to :domain
scope :pending, -> { where(status: 0) }
scope :completed, -> { where(status: [1, 2]) }
def failed?
status == 2
end
def arguments
JSON.parse(args)
end
class << self
def add_domain(domain)
ActiveRecord::Base.transaction do
jobs_for_domain(domain, :add_domain)
trigger_event(domain, :installed)
end
end
def shutdown_domain(domain)
ActiveRecord::Base.transaction do
job_for_domain(domain, :remove_domain)
job_for_domain(domain, :opendnssec_remove) if domain.dnssec?
trigger_event(domain, :cleaned_up)
end
end
def dnssec_sign(domain)
ActiveRecord::Base.transaction do
jobs_for_domain(domain,
:opendnssec_add,
:bind_convert_to_dnssec)
trigger_event(domain, :signed)
end
end
def wait_for_ready(domain)
jobs_for_domain(domain,
:wait_for_ready_to_push_ds)
end
def dnssec_push_ds(domain, dss)
opts = Hash[:dnssec_parent, domain.dnssec_parent,
:dnssec_parent_authority, domain.dnssec_parent_authority,
:dss, dss]
ActiveRecord::Base.transaction do
job_for_domain(domain, :publish_ds, opts)
job_for_domain(domain, :wait_for_active)
trigger_event(domain, :converted)
end
end
+ def dnssec_rollover_ds(domain, dss)
+ opts = Hash[:dnssec_parent, domain.dnssec_parent,
+ :dnssec_parent_authority, domain.dnssec_parent_authority,
+ :dss, dss]
+ ActiveRecord::Base.transaction do
+ job_for_domain(domain, :publish_ds, opts)
+ job_for_domain(domain, :wait_for_active)
+
+ trigger_event(domain, :complete_rollover)
+ end
+ end
+
def convert_to_plain(domain)
ActiveRecord::Base.transaction do
jobs_for_domain(domain,
:remove_domain,
:add_domain,
:opendnssec_remove)
trigger_event(domain, :converted)
end
end
private
def trigger_event(domain, event)
job_for_domain(domain, :trigger_event, event: event)
end
def jobs_for_domain(domain, *job_names)
job_names.each { |job_name| job_for_domain(domain, job_name) }
end
def job_for_domain(domain, job_name, args = {})
args = { zone: domain.name }.merge!(args)
create!(domain: domain, job_type: job_name, args: args.to_json)
end
end
end
diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb
index ef79d41..ec82793 100644
--- a/test/models/domain_test.rb
+++ b/test/models/domain_test.rb
@@ -1,220 +1,228 @@
require 'test_helper'
class DomainTest < ActiveSupport::TestCase
def setup
@domain = build(:domain)
end
test 'automatic SOA creation' do
@domain.save!
@domain.reload
assert_not_nil @domain.soa
end
test 'increment serial on new record' do
@domain.save!
soa = @domain.soa
assert_serial_update soa do
www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
www.save!
end
end
test 'increment serial on record update' do
@domain.save!
www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
www.save!
soa = @domain.soa.reload
assert_serial_update soa do
www.content = '1.2.3.5'
www.save!
end
end
test 'automatic NS creation' do
@domain.save!
@domain.reload
assert_equal WebDNS.settings[:default_ns].sort,
@domain.records.where(type: 'NS').pluck(:content).sort
end
test 'increment serial on record destroy' do
@domain.save!
www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
www.save!
soa = @domain.soa.reload
assert_serial_update soa do
www.destroy!
end
end
class SlaveDomainTest < ActiveSupport::TestCase
def setup
@domain = build(:slave)
end
test 'saves' do
@domain.save
assert_empty @domain.errors
end
test 'automatic SOA creation' do
@domain.save!
@domain.reload
assert_not_nil @domain.soa
assert_equal 1, @domain.soa.serial
end
test 'validates master' do
@domain.master = 'not-an-ip'
@domain.save
assert_not_empty @domain.errors['master']
end
test 'no records are allowed for users' do
@domain.save!
rec = build(:a, domain_id: @domain.id)
assert_not rec.valid?
assert_not_empty rec.errors[:type]
end
end
class StatesDomainTest < ActiveSupport::TestCase
def setup
@domain = build(:domain)
end
test 'domain lifetime' do
assert_equal 'initial', @domain.state
# Create
assert_jobs do
@domain.save! # user triggered
assert_equal 'pending_install', @domain.state
end
@domain.installed # job triggered
assert_equal 'operational', @domain.state
# Convert to dnssec (sign)
assert_jobs do
assert @domain.dnssec_sign # user triggered
assert_equal 'pending_signing', @domain.state
end
assert_jobs do
assert @domain.signed # job triggered
assert_equal 'wait_for_ready', @domain.state
end
# Convert to dnssec (publish ds)
assert_jobs do
- assert @domain.push_ds([:dss1, :dss2]) # DS script triggered
+ assert @domain.push_ds([:dss1, :dss2]) # triggered by schedule-ds script
assert_equal 'pending_ds', @domain.state
end
assert @domain.converted # job triggered
assert_equal 'operational', @domain.state
+ # KSK rollover
+ assert_jobs do
+ assert @domain.push_ds([:dss3, :dss4]) # triggered by schedule-ds script
+ assert_equal 'pending_ds_rollover', @domain.state
+ end
+ assert @domain.complete_rollover # job triggered
+ assert_equal 'operational', @domain.state
+
# Convert to plain
assert_jobs do
assert @domain.plain_convert # user triggered
assert_equal 'pending_plain', @domain.state
end
assert @domain.converted # job triggered
assert_equal 'operational', @domain.state
# Remove
assert_jobs do
assert @domain.remove # user triggered
assert_equal 'pending_remove', @domain.state
end
assert @domain.cleaned_up # job triggered
assert_equal 'destroy', @domain.state
end
end
class DsDomainTest < ActiveSupport::TestCase
def setup
@domain = create(:domain)
@ds = [
'31406 8 1 189968811e6eba862dd6c209f75623d8d9ed9142',
'31406 8 2 f78cf3344f72137235098ecbbd08947c2c9001c7f6a085a17f518b5d8f6b916d',
]
@child = "dnssec.#{@domain.name}"
@extra = DS.create(domain: @domain, name: @child, content: 'other')
end
test 'add ds records' do
Domain.replace_ds(@domain.name, @child, @ds)
@extra.save! # Should be deleted
assert_equal @ds.size, DS.where(name: "dnssec.#{@domain.name}").count
@ds.each { |ds|
assert_equal 1, DS.where(name: "dnssec.#{@domain.name}", content: ds).count
}
end
test 'check if child is a valid subdomain' do
assert_raise Domain::NotAChild do
Domain.replace_ds(@domain.name, 'dnssec.example.net', @ds)
end
end
end
class BulkTest < ActiveSupport::TestCase
def setup
@domain = create(:domain)
@a = create(:a, domain: @domain)
@aaaa = create(:aaaa, domain: @domain)
@new = build(:mx, domain: @domain)
end
def valid_changes
@valid_changes ||= begin
{}.tap { |c|
c[:deletes] = [@a.id]
c[:changes] = { @aaaa.id => { content: '::42' }}
c[:additions] = { 1 => @new.as_bulky_json }
}
end
end
def invalid_changes
@invalid_changes ||= begin
{}.tap { |c|
c[:deletes] = [Record.maximum(:id) + 1]
c[:changes] = { @aaaa.id => { content: '1.2.3.4' }}
c[:additions] = { 1 => @new.as_bulky_json.update(prio: -1) }
}
end
end
test 'apply changes not' do
err = @domain.bulk invalid_changes
assert_not_empty err
assert_includes err[:deletes][Record.maximum(:id) + 1], 'record not found'
assert_includes err[:changes][@aaaa.id], 'not a valid IPv6'
assert_includes err[:additions][1], 'not a valid DNS priority'
end
test 'apply changes' do
err = @domain.bulk valid_changes
@domain.reload
@aaaa.reload
assert_empty err
assert_empty @domain.records.where(id: @a.id)
assert_equal '::42', @aaaa.content
assert_equal 1, @domain.records.where(type: :mx).count
end
end
end

Event Timeline