diff --git a/app/models/domain.rb b/app/models/domain.rb index 611167d..6ab3f0c 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -1,81 +1,92 @@ class Domain < ActiveRecord::Base self.inheritance_column = :nx def self.domain_types [ 'NATIVE', 'MASTER', 'SLAVE', ] end belongs_to :group has_many :records has_one :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? after_create :generate_soa + after_create :generate_ns attr_writer :serial_strategy def serial_strategy @serial_strategy ||= WebDNS.settings[:serial_strategy] end def reverse? name.end_with?('.in-addr.arpa') || name.end_with?('.ip6.arpa') end 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 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 + end diff --git a/app/models/record.rb b/app/models/record.rb index 4152064..3e3f1a6 100644 --- a/app/models/record.rb +++ b/app/models/record.rb @@ -1,160 +1,161 @@ 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) } def self.record_types [ 'A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SPF', 'SRV', 'SSHFP', 'SOA', 'NS', 'PTR', 'NAPTR' ] end def self.forward_records record_types - ['SOA', 'PTR'] end def self.reverse_records ['PTR', 'CNAME', 'TXT', 'NS', 'NAPTR'] end 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 # Smart order a list of domains def self.smart_order(records) records.sort_by { |r| [ r.domain_record? ? 0 : 1, # Zone records 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 short return '' if name == domain.name return '' if name.blank? File.basename(name, ".#{domain.name}") end def domain_record? name.blank? || name == domain.name end def editable?(by = :user) return false if domain.slave? case by when :user return false unless Record.allowed_record_types.include?(type) return false if type == 'NS' && domain_record? end true end def supports_prio? false end # Create record specific urls for all record types # # Overrides default rails STI def self.model_name return super if self == Record Record.model_name end def to_dns [name, ttl, 'IN', type, supports_prio? ? prio : nil, content].compact.join(' ') end def to_short_dns [name, 'IN', type].join(' ') 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 end diff --git a/config/initializers/00_settings.rb b/config/initializers/00_settings.rb index 84a278a..d489788 100644 --- a/config/initializers/00_settings.rb +++ b/config/initializers/00_settings.rb @@ -1,18 +1,22 @@ WebDNS = Base WebDNS.settings[:soa_defaults] = { - primary_ns: 'ns.example.com', + primary_ns: 'ns1.example.com', contact: 'domainmaster@example.com', serial: 1, refresh: 10_800, retry: 3600, expire: 604_800, nx: 3600 } +WebDNS.settings[:default_ns] = [ + 'ns1.example.com', + 'ns2.example.com' +] WebDNS.settings[:serial_strategy] = Strategies::Date WebDNS.settings[:prohibit_records_types] = [] WebDNS.settings[:mail_from] = 'webdns@example.com' WebDNS.settings[:admin_group] = 'admin' diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb index 6c8b2a0..44852e2 100644 --- a/test/models/domain_test.rb +++ b/test/models/domain_test.rb @@ -1,81 +1,87 @@ 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 - assert_equal 1, @domain.soa.serial 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 end