diff --git a/app/helpers/records_helper.rb b/app/helpers/records_helper.rb index 9ea218e..e0f974c 100644 --- a/app/helpers/records_helper.rb +++ b/app/helpers/records_helper.rb @@ -1,14 +1,22 @@ module RecordsHelper # Smart suffix for records # # On forward zones returns the zone name. # On reverse zones returns the zone name but also tries to infer the subnet. # # Returns a smart suffix string. def name_field_append(record) return ".#{record.domain.name}" if not record.domain.reverse? ".#{record.domain.name} (#{record.domain.subnet})" end + + # List of record types usually used for that domain type + def record_types_for_domain(domain) + return Record.reverse_records if domain.reverse? + return Record.enum_records if domain.enum? + + Record.forward_records + end end diff --git a/app/models/domain.rb b/app/models/domain.rb index 2e2a7fc..fc1fa52 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -1,99 +1,104 @@ class Domain < ActiveRecord::Base self.inheritance_column = :nx # List all supported domain types. 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 # 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 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 26f9b1b..6109ccb 100644 --- a/app/models/record.rb +++ b/app/models/record.rb @@ -1,198 +1,203 @@ 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' ] 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'] end + # List types usually used in enum zones. + def self.enum_records + ['NAPTR', 'CNAME', 'TXT', 'NS'] + 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 # 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.name, r.type == 'SOA' ? 0 : 1, r.type == 'NS' ? 0 : 1, record_types.index(r.type), # Friendly type r.prio, r.content ] } 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? 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 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/app/views/domains/index.html.erb b/app/views/domains/index.html.erb index 47058bc..2daced8 100644 --- a/app/views/domains/index.html.erb +++ b/app/views/domains/index.html.erb @@ -1,50 +1,52 @@ <% if current_user.memberships.empty? %>

Wellcome to WebDNS!

In order to manage domains you have to be a member of a group.

You can either contact an admin to create a new group for you, or ask another user for an invite to an existing group.

<% end %> <% @domains.group_by(&:group).each do |group, domains| %> <% domains.each do |domain| %> <% end %> <% end %>
<%= link_to group.name, group_path(group) %> Controls
<%= link_to domain.name, domain %> <% if domain.reverse? %> <%= abbr_glyph('chevron-left', 'Reverse') %> + <% elsif domain.enum? %> + <%= abbr_glyph('phone-alt', 'Enum') %> <% else %> <%= abbr_glyph('chevron-right', 'Forward') %> <% end %> <% if domain.slave? %> <%= abbr_glyph('link', 'Slave') %> <% end %> <%= link_to_edit edit_domain_path(domain) %> <%= link_to_destroy domain, method: :delete, data: { confirm: 'Are you sure?' } %>

<% if current_user.memberships.any? %> <%= link_to 'Add Domain', new_domain_path, class: 'btn btn-primary' %> <% else %> <%= link_to 'Add Domain', new_domain_path, class: 'btn btn-primary disabled' %> <% end %>

diff --git a/app/views/groups/show.html.erb b/app/views/groups/show.html.erb index 1417b3a..418aa55 100644 --- a/app/views/groups/show.html.erb +++ b/app/views/groups/show.html.erb @@ -1,78 +1,80 @@ <% content_for :more_breadcrumbs do %>
  • <%= link_to_edit edit_admin_group_path(@group) %> <%= link_to_destroy admin_group_path(@group), method: :delete, data: { confirm: 'Are you sure?' } %>
  • <% end if admin? %>
    <% @group.domains.each do |domain| %> <% end %>
    Domain Controls
    <%= link_to domain.name, domain %> <% if domain.reverse? %> <%= abbr_glyph('chevron-left', 'Reverse') %> + <% elsif domain.enum? %> + <%= abbr_glyph('phone-alt', 'Enum') %> <% else %> <%= abbr_glyph('chevron-right', 'Forward') %> <% end %> <% if domain.slave? %> <%= abbr_glyph('link', 'Slave') %> <% end %> <%= link_to_edit edit_domain_path(domain) %> <%= link_to_destroy domain, method: :delete, data: { confirm: 'Are you sure?' } %>

    <%= link_to 'Add Domain', new_domain_path(group_id: @group.id), class: 'btn btn-primary' %>

    <% @group.memberships.includes(:user).each do |membership| %> <% end %>
    Member Controls
    <%= membership.user.email %><%= " (you)" if current_user == membership.user %> <%= link_to_destroy destroy_member_group_path(@group, membership.user_id), method: :delete %>

    <%= bootstrap_form_tag(url: create_member_group_path(@group), layout: :inline) do |f| %> <%= f.text_field :email, prepend: 'Add Member', hide_label: true, id: 'js-search-member', data: { group: @group.id } %> <%= f.submit 'Add', class: 'btn btn-primary' %> <% end %>

    diff --git a/app/views/records/_form.html.erb b/app/views/records/_form.html.erb index 5e7da70..65f2cf6 100644 --- a/app/views/records/_form.html.erb +++ b/app/views/records/_form.html.erb @@ -1,15 +1,15 @@ <%= bootstrap_form_for([@domain, @record], layout: :horizontal, label_col: 'col-sm-2', control_col: 'col-sm-8') do |f| %> <%= f.text_field :name, value: @record.short, label: 'Record', append: name_field_append(@record) %> <% if @record.persisted? %> <%= f.static_control :type %> <% else %> - <%= f.select :type, @domain.reverse? ? Record.reverse_records : Record.forward_records %> + <%= f.select :type, record_types_for_domain(@domain) %> <% end %> <%= f.text_field :prio, placeholder: 10, wrapper_class: @record.supports_prio? ? '' : 'hidden' %> <%= f.text_field :ttl, label: 'TTL' %> <%= f.text_field :content %> <%= f.submit 'Save', class: 'btn btn-primary col-sm-offset-2' %> <% end %>