Page Menu
Home
GRNET
Search
Configure Global Search
Log In
Files
F5492731
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Tue, Sep 29, 4:41 PM
Size
35 KB
Mime Type
text/x-diff
Expires
Thu, Oct 1, 4:41 PM (18 h, 34 m)
Engine
blob
Format
Raw Data
Handle
458418
Attached To
rWEBDNS WebDNS (edet4)
View Options
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
index 2d5bf65..b578d15 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -1,27 +1,82 @@
class ApiController < ApplicationController
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+ rescue_from ActionController::ParameterMissing, with: :parameter_missing
+
# This a private trusted API
skip_before_action :verify_authenticity_token
before_action :authenticate_token
+ before_action :domain, only: [:list, :bulk]
# GET /ping
def ping
render json: { ok: true, response: :pong }
end
# GET /whoami
def whoami
render json: { ok: true, response: current_user.to_api }
end
+ # GET domain/<name>/list
+ def list
+ records = Record.smart_order(@domain.records).map(&:to_api)
+ render json: { ok: true, response: records }
+ end
+
+ # POST domain/<name>/list
+ def bulk
+ api_params = params.require(:api).permit!
+ ops, err, bulk_ops = domain.api_bulk(api_params)
+
+ if err.empty?
+ notify_record_bulk(domain, bulk_ops)
+
+ render json: { ok: true,
+ response: {
+ operations: ops
+ }
+ }
+ else
+ render json: { ok: false,
+ errors: err,
+ response: {
+ operations: ops
+ }
+ }
+ end
+ end
+
private
def authenticate_token
if user = User.find_by_token(params.require(:token))
warden.set_user(user, store: false)
else
head(403)
end
end
+ def domain
+ if params[:domain] =~ /^[0-9]+$/
+ params[:domain_id] = params[:domain]
+ else
+ params[:domain_id] = Domain.find_by_name!(params[:domain]).id
+ end
+
+ super
+ end
+
+ def record_not_found
+ render json: { ok: false, error: :record_not_found }
+ end
+
+ def parameter_missing
+ render json: { ok: false, error: :parameter_missing }
+ end
+
+ def notify_record_bulk(*args)
+ notification.notify_record_bulk(current_user, *args) if WebDNS.settings[:notifications]
+ end
+
end
diff --git a/app/models/domain.rb b/app/models/domain.rb
index edad604..2894121 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -1,321 +1,384 @@
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].keys.map(&:to_s)
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
belongs_to :dnssec_policy
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?
validates :dnssec_policy_id, presence: true, if: :dnssec?
after_create :generate_soa
after_create :generate_ns
after_create :install
before_save :check_convert
before_save :check_dnssec_parent_authority, if: :dnssec?
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_ds_removal) { |domain, _t| Job.dnssec_drop_ds(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_rollover_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
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_ds_removal] => :pending_remove
end
event :full_remove do
transition operational: :pending_ds_removal
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
# Returns the zone serial if a SOA record exists
def serial
return if !soa
soa.serial
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
def to_export
Hash[
:id, id,
:name, name,
:group, group.name,
].with_indifferent_access
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)
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 api bulk to operations to the zone
+ #
+ # 1) Deletions
+ # 2) Upserts
+ # 3) Additions
+ def api_bulk(opts)
+ api_deletes = opts[:deletes] || []
+ api_upserts = opts[:upserts] || []
+ api_additions = opts[:additions] || []
+ api_delete_errors = {}
+
+ deletes = []
+ additions = {}
+
+ api_deletes.each { |del|
+ rec = records.find_by(del)
+ # Fail-fast if record doesn't exist
+ if rec.nil?
+ return [{}, { deletes: { del: 'record not found'}}]
+ end
+
+ deletes << rec.id
+ }
+
+ # We delete records matching the same name & type
+ api_upserts.each { |ups|
+ query = ups.slice(:name, :type)
+ existing = records.where(query).to_a
+
+ # Skip upsert if we are trying to save the same record
+ next if existing.one? && ups.all? { |k, v| existing.first.to_api[k] == v }
+
+ deletes += existing.map(&:id)
+ api_additions << ups
+ }
+
+ api_additions.each { |add|
+ additions[add] = add
+ }
+
+ ops, errors = bulk(deletes: deletes, additions: additions)
+
+ # Serialize the response for API
+ api_ops = {}
+ api_errors = {}
+
+ # ops
+ ops.each { |op, recs| api_ops[op] = recs.map(&:to_api) }
+
+ # errors
+ if errors.any?
+ errors.each { |op, err|
+ api_errors[op] = err.map { |rec, err|
+ { operation: rec, error: err }
+ }
+ }
+ end
+
+ # This is a bit ugly, we return an ops hash with the original bulk
+ # responses so we can feed it to record notification.
+ [api_ops, api_errors, ops]
+ 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] = {} }
operations = 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
operations[:deletes] << rec
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|
if rec = to_change[Integer(rec_id)]
operations[:changes] << rec
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)
operations[:additions] << rec
errors[:additions][inc] = rec.errors.full_messages.join(', ') if !rec.save
}
raise ActiveRecord::Rollback if errors.any?
end
[operations, 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 check_dnssec_parent_authority
cfg = WebDNS.settings[:dnssec_parent_authorities][dnssec_parent_authority.to_sym]
return if !cfg[:valid]
return true if cfg[:valid].call(dnssec_parent)
errors.add(:dnssec_parent_authority, 'Parent zone is not accepted for the selected parent authority!')
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/record.rb b/app/models/record.rb
index c29fa47..228e4f9 100644
--- a/app/models/record.rb
+++ b/app/models/record.rb
@@ -1,311 +1,322 @@
require 'ipaddr'
require_dependency 'drop_privileges_validator'
class Record < ActiveRecord::Base
belongs_to :domain
# Powerdns inserts empty records on slave zones,
# we want to hide them
#
# http://mailman.powerdns.com/pipermail/pdns-users/2013-December/010389.html
default_scope { where.not(type: nil) }
# List all supported DNS RR types.
def self.record_types
[
'A', 'AAAA', 'CNAME',
'MX',
'TXT', 'SPF', 'SRV', 'SSHFP',
'SOA', 'NS',
'PTR', 'NAPTR',
'DS'
]
end
# List types usually used in forward zones.
def self.forward_records
record_types - ['SOA', 'PTR']
end
# List types usually used in reverse zones.
def self.reverse_records
['PTR', 'CNAME', 'TXT', 'NS', 'NAPTR', 'DS']
end
# List types usually used in enum zones.
def self.enum_records
['NAPTR', 'CNAME', 'TXT', 'NS', 'DS']
end
# List types that can be touched by a simple user.
def self.allowed_record_types
record_types - WebDNS.settings[:prohibit_records_types]
end
validates :name, presence: true
validates :type, inclusion: { in: record_types }
# http://mark.lindsey.name/2009/03/never-use-dns-ttl-of-zero-0.html
validates_numericality_of :ttl,
allow_nil: true, # Default pdns TTL
only_integer: true,
greater_than: 0,
less_than_or_equal_to: 2_147_483_647
# Don't allow the following actions on drop privileges mode
validate :no_touching_for_slave_zones, if: -> { domain.slave? }
validates_drop_privileges :type,
message: 'You cannot touch that record!',
unless: -> { Record.allowed_record_types.include?(type) }
validates_drop_privileges :name,
message: 'You cannot touch top level NS records!',
if: -> { type == 'NS' && domain_record? }
before_validation :guess_reverse_name
before_validation :set_name
after_save :update_zone_serial
after_destroy :update_zone_serial
before_create :generate_classless_delegations, unless: -> { domain.slave? }
before_destroy :delete_classless_delegations, unless: -> { domain.slave? }
# Smart sort a list of records.
#
# Order by:
# * Top level records
# * Record name
# * SOA
# * NS
# * Friendly type
# * Priority
# * Content
#
# records - The list of records to order.
#
# Returns the list sorted.
def self.smart_order(records)
records.sort_by { |r|
[
r.domain_record? ? 0 : 1, # Zone records
r.classless_delegated? ? 1 : 0,
r.name,
r.type == 'SOA' ? 0 : 1,
r.type == 'NS' ? 0 : 1,
record_types.index(r.type), # Friendly type
r.prio,
r.content
]
}
end
def self.search(query)
wild_search = "%#{query}%" # !index_friendly
where('name like :q or content like :q', q: wild_search)
end
# Get the a short name for the record (without the zone suffix).
#
# Returns a string.
def short
return '' if name == domain.name
return '' if name.blank?
File.basename(name, ".#{domain.name}")
end
# Returns true if this is a zone record.
def domain_record?
name.blank? || name == domain.name
end
# Find out if the record is edittable.
#
# by - Editable by :user or :admin.
#
# Returns true if the record is editable.
def editable?(by = :user)
return false if domain.slave?
return false if classless_delegated?
case by
when :user
return false unless Record.allowed_record_types.include?(type)
return false if type == 'NS' && domain_record?
end
true
end
# Find out this record type supports priorities.
#
# We set this to false by default, record types that support priorities.
# shoule override this.
#
# Returns true this record type support priorities.
def supports_prio?
false
end
# Make sure rails generates record specific urls for all record types.
#
# Overrides default rails STI behavior.
def self.model_name
return super if self == Record
Record.model_name
end
# Generate the usual admin friendly DNS record line.
#
# Returns a string.
def to_dns
[name, ttl, 'IN', type, supports_prio? ? prio : nil, content].compact.join(' ')
end
# Generate a shorter version of the DNS record line.
#
# Returns a string.
def to_short_dns
[name, 'IN', type].join(' ')
end
+ def to_api
+ Hash[
+ :name, name,
+ :content, content,
+ :type, type,
+ :ttl, ttl,
+ :prio, prio,
+ :disabled, disabled
+ ].with_indifferent_access
+ end
+
def classless_delegated?
return false if not type == 'CNAME'
return false if not domain.name.end_with?('.in-addr.arpa')
network, mask = parse_delegation(content)
return false if network.nil?
octet = name.split('.').first.to_i
return true if octet >= network
return true if octet <= network + 2 ^ (32 - mask) - 1 # max
false
end
def classless_delegation?
return true if classless_delegation
false
end
def as_bulky_json
Hash[
id: id,
name: name,
type: type,
ttl: ttl,
prio: prio,
content: content,
disabled: disabled
]
end
private
# Validations
def no_touching_for_slave_zones
# Allow automatic SOA creation for slave zones
# powerdns needs a valid serial to compare it with master
return if type == 'SOA' && validation_context == :create
errors.add(:type, 'This is a slave zone!')
end
# Hooks
def guess_reverse_name
return if not type == 'PTR'
return if not domain.reverse?
return if name.blank?
reverse = IPAddr.new(name).reverse
self.name = reverse if reverse.end_with?(domain.name)
rescue IPAddr::InvalidAddressError # rubycop:disable HandleExceptions
end
# Powerdns expects full domain names
def set_name
self.name = domain.name if name.blank?
self.name = "#{name}.#{domain.name}" if not name.end_with?(domain.name)
end
def remove_terminating_dot
self.content = content.gsub(/\.+\Z/, '')
end
def update_zone_serial
# SOA records handle serial themselves
return true if type == 'SOA'
return true if !domain
domain.soa.bump_serial!
end
def classless_delegation
return if not type == 'NS'
return if not domain.name.end_with?('.in-addr.arpa')
network, mask = parse_delegation(name)
return if network.nil?
range = IPAddr.new("0.0.0.#{network}/#{mask}").to_range
return if !range.first.to_s.end_with?(".#{network}")
range.map { |ip|
octet = ip.to_s.split('.').last
"#{octet}.#{domain.name}"
}
end
def parse_delegation(value)
first, _rest = value.split('.', 2)
first.gsub!('-', '/')
return if !first['/']
network, mask = first.split('/', 2).map { |i| Integer(i).abs }
return if [network, mask].join('/') != first
return if mask <= 24
return if mask > 31
return if network > 255
[network, mask]
rescue ArgumentError # Not an integer
end
def delete_classless_delegations
rnames = classless_delegation
return unless rnames
# Check if we have another NS for the same delegation
return if domain.records.where(type: 'NS', name: name)
.where.not(id: id).exists?
# Delete all CNAMEs
domain.records.where(name: rnames,
type: 'CNAME',
content: name).delete_all
end
def generate_classless_delegations
rnames = classless_delegation
return unless rnames
# Make sure no record exists for a delegated domain
if domain.records.where(name: rnames)
.where.not(content: name).exists?
errors.add(:name, 'Records already exist for the delegated octets!')
return false
end
rnames.each { |rname|
CNAME.find_or_create_by!(
domain: domain,
name: rname,
content: name
)
}
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 0ecff57..80b5e24 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,75 +1,77 @@
Rails.application.routes.draw do
# Override devise user removal
devise_scope :users do
delete :users, to: redirect('/')
end
devise_for :users
get '/auth/saml', to: 'auth#saml'
root to: redirect('/domains')
resources :users, only: [] do
get :token, to: 'users#token', on: :member
post :generate_token, to: 'users#generate_token', on: :member
end
resources :groups, only: [:show] do
get :search_member,
to: 'groups#search_member', on: :member
post :members,
to: 'groups#create_member', as: :create_member, on: :member
delete 'member/:user_id',
to: 'groups#destroy_member', as: :destroy_member, on: :member
end
resources :domains do
get :edit_dnssec, to: 'domains#edit_dnssec', on: :member
delete :full_destroy, to: 'domains#full_destroy', on: :member
resources :records, except: [:index, :show] do
# Reuse records#update instead of introducing new controller actions
#
# rubocop:disable Style/AlignHash
put :disable, to: 'records#update', on: :member,
defaults: { record: { disabled: true } }
put :enable, to: 'records#update', on: :member,
defaults: { record: { disabled: false } }
put :editable, to: 'records#editable', on: :collection
post :valid, to: 'records#valid', on: :collection
post :bulk, to: 'records#bulk', on: :collection
# rubocop:enable Style/AlignHash
end
end
get '/records/search', to: 'records#search'
# Admin
namespace :admin do
root to: redirect('/admin/groups')
resources :groups, except: [:show]
resources :jobs, only: [:index, :destroy] do
put :done, to: 'jobs#update', on: :member,
defaults: { job: { status: 1 } }
put :pending, to: 'jobs#update', on: :member,
defaults: { job: { status: 0 } }
end
resources :users, only: [:destroy] do
get :orphans, to: 'users#orphans', on: :collection
put :update_groups, to: 'users#update_groups', on: :collection
end
end
# API
scope '/api' do
get :ping, to: 'api#ping'
get :whoami, to: 'api#whoami'
+ get '/domain/:domain/list', to: 'api#list', constraints: { domain: /[^\/]+/}
+ post '/domain/:domain/bulk', to: 'api#bulk', constraints: { domain: /[^\/]+/}
end if WebDNS.settings[:api]
# Private
put 'private/replace_ds', to: 'private#replace_ds'
put 'private/trigger_event', to: 'private#trigger_event'
get 'private/zones', to: 'private#zones'
end
diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb
index b91ea82..fce86b1 100644
--- a/test/models/domain_test.rb
+++ b/test/models/domain_test.rb
@@ -1,308 +1,414 @@
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)
@policy = create(:dnssec_policy)
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
@domain.dnssec = true
@domain.dnssec_policy = @policy
@domain.dnssec_parent = @domain.name.split('.', 2).last
@domain.dnssec_parent_authority = 'test_authority'
@domain.save!
# After commit is not triggered in tests,
# so we have to trigger it manually
@domain.send(:after_commit_event)
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']) # triggered by ds-schedule 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 ds-schedule 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
test 'domain lifetime #full-destroy' 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
@domain.dnssec = true
@domain.dnssec_policy = @policy
@domain.dnssec_parent = @domain.name.split('.', 2).last
@domain.dnssec_parent_authority = 'test_authority'
@domain.save!
# After commit is not triggered in tests,
# so we have to trigger it manually
@domain.send(:after_commit_event)
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']) # triggered by ds-schedule 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 ds-schedule script
assert_equal 'pending_ds_rollover', @domain.state
end
assert @domain.complete_rollover # job triggered
assert_equal 'operational', @domain.state
# Full Remove (Drops DS records)
assert_jobs do
assert @domain.full_remove # user triggered
assert_equal 'pending_ds_removal', @domain.state
end
assert_jobs do
assert @domain.remove # job 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 'remove ds records' do
Domain.replace_ds(@domain.name, @child, [])
assert_equal 0, DS.where(name: "dnssec.#{@domain.name}").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
ops, 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
ops, 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
assert_equal 1, ops[:additions].size
assert_equal 1, ops[:changes].size
assert_equal 1, ops[:deletes].size
end
end
+
+ class ApiBulkTest < ActiveSupport::TestCase
+ def setup
+ @domain = create(:domain)
+ @a = create(:a, domain: @domain)
+ @aaaa = create(:aaaa, domain: @domain, content: '::42')
+ @new = build(:mx, domain: @domain)
+ @upsert_txt = build(:txt, domain: @domain)
+
+ end
+
+ def valid_changes
+ @valid_changes ||= begin
+ {}.tap { |c|
+ c[:deletes] = [@a.to_api]
+ c[:additions] = [@new.to_api]
+ c[:upserts] = [@upsert_txt.to_api]
+ }
+ end
+ end
+
+ test 'apply changes' do
+ ops, err = @domain.api_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
+ assert_equal 2, ops[:additions].size # upsert is accounted as in addition
+ assert_equal 1, ops[:deletes].size
+
+ end
+
+ test 'additions is invalid' do
+ changes = {
+ additions: [ @new.to_api.update(prio: -1) ]
+ }
+
+ ops, err = @domain.api_bulk changes
+
+ assert_not_empty err
+ assert_includes err[:additions].first[:error], 'not a valid DNS priority'
+ end
+
+
+ test 'delete not exists' do
+ changes = Hash[
+ :deletes, [{name: 'nx', type: 'TXT', content: 'not-exists'}]
+ ]
+
+ ops, err = @domain.api_bulk changes
+
+ assert_empty ops
+ assert_equal 1, err[:deletes].size
+ end
+
+ test 'upsert does not exist (single record)' do
+ a1 = create(:a, domain: @domain, name: 'rr', content: '127.0.0.1')
+
+ rr_name = "rr.#{@domain.name}"
+ changes = Hash[
+ :upserts, [{name: rr_name, type: 'A', content: '127.0.0.3'}]
+ ]
+
+ ops, err = @domain.api_bulk changes
+
+ assert_empty err
+ assert_equal 1, @domain.records.where(name: rr_name, type: 'A').count
+ assert_equal '127.0.0.3', @domain.records.find_by(name: rr_name, type: 'A').content
+ end
+
+ test 'upsert does not exist (multiple records)' do
+ a1 = create(:a, domain: @domain, name: 'rr', content: '127.0.0.1')
+ a2 = create(:a, domain: @domain, name: 'rr', content: '127.0.0.2')
+
+ rr_name = "rr.#{@domain.name}"
+ changes = Hash[
+ :upserts, [{name: rr_name, type: 'A', content: '127.0.0.3'}]
+ ]
+
+ ops, err = @domain.api_bulk changes
+
+ assert_empty err
+ assert_equal 1, @domain.records.where(name: rr_name, type: 'A').count
+ assert_equal '127.0.0.3', @domain.records.find_by(name: rr_name, type: 'A').content
+ end
+
+ test 'upsert exists' do
+ a1 = create(:a, domain: @domain, name: 'rr', content: '127.0.0.1')
+
+ rr_name = "rr.#{@domain.name}"
+ changes = Hash[
+ :upserts, [{name: rr_name, type: 'A', content: '127.0.0.1'}]
+ ]
+
+ ops, err = @domain.api_bulk changes
+
+ assert_empty err
+ assert_empty ops # upsert is a noop
+ assert_equal 1, @domain.records.where(name: rr_name, type: 'A').count
+ assert_equal '127.0.0.1', @domain.records.find_by(name: rr_name, type: 'A').content
+ end
+ end
end
Event Timeline
Log In to Comment