diff --git a/app/controllers/records_controller.rb b/app/controllers/records_controller.rb index 77c2818..6d525f8 100644 --- a/app/controllers/records_controller.rb +++ b/app/controllers/records_controller.rb @@ -1,55 +1,55 @@ class RecordsController < ApplicationController before_action :authenticate_user! before_action :domain before_action :record, only: [:edit, :update, :destroy] # GET /records/new def new @record = domain.records.build end # GET /records/1/edit def edit end # POST /records def create @record = domain.records.new(new_record_params) if @record.save redirect_to domain, notice: 'Record was successfully created.' else render :new end end # PATCH/PUT /records/1 def update if @record.update(edit_record_params) redirect_to domain, notice: 'Record was successfully updated.' else render :edit end end # DELETE /records/1 def destroy @record.destroy redirect_to domain, notice: 'Record was successfully destroyed.' end private def edit_record_params - params.require(:record).permit(:name, :content, :prio, :disabled).tap { |r| + params.require(:record).permit(:name, :content, :ttl, :prio, :disabled).tap { |r| r[:drop_privileges] = true if not admin? } end def new_record_params - params.require(:record).permit(:name, :content, :type, :prio).tap { |r| + params.require(:record).permit(:name, :content, :ttl, :type, :prio).tap { |r| r[:drop_privileges] = true if not admin? } end end diff --git a/app/models/record.rb b/app/models/record.rb index 912eb05..c52d59c 100644 --- a/app/models/record.rb +++ b/app/models/record.rb @@ -1,100 +1,107 @@ require 'ipaddr' require_dependency 'drop_privileges_validator' class Record < ActiveRecord::Base belongs_to :domain def self.record_types [ 'SOA', 'NS', 'CNAME', 'A', 'AAAA', 'MX', 'TXT', 'SPF', 'SRV', 'SSHFP', 'PTR', ] 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 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 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 # Editable by a non-admin user def editable? return false unless Record.allowed_record_types.include?(type) return false if type == 'NS' && domain_record? 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, 'IN', type, supports_prio? ? prio : nil, content].compact.join(' ') + [name, ttl, 'IN', type, supports_prio? ? prio : nil, content].compact.join(' ') end private # 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 update_zone_serial # SOA records handle serial themselves return true if type == 'SOA' domain.soa.bump_serial! end end diff --git a/app/views/domains/show.html.erb b/app/views/domains/show.html.erb index 5fb742f..a838d6e 100644 --- a/app/views/domains/show.html.erb +++ b/app/views/domains/show.html.erb @@ -1,37 +1,38 @@ - + <% @domain.records.each do |record| %> + <% if can_edit?(record) %> <% else %> <% end %>
RecordsRecords Controls
<%= record.name %><%= record.ttl %> IN <%= record.type %> <%= record.supports_prio? ? record.prio : '' %> <%= record.content %> <% if record.disabled? %> <%= link_to_enable enable_domain_record_path(@domain, record), method: :put %> <% else %> <%= link_to_disable disable_domain_record_path(@domain, record), method: :put %> <% end %> <%= link_to_edit edit_domain_record_path(@domain, record) if can_edit?(record) %> <%= link_to_destroy [@domain, record], method: :delete, data: { confirm: 'Are you sure?' } %> <% end %>

<%= link_to 'New Record', new_domain_record_path(@domain) %>

diff --git a/app/views/records/_form.html.erb b/app/views/records/_form.html.erb index 93c1fa7..9dfe732 100644 --- a/app/views/records/_form.html.erb +++ b/app/views/records/_form.html.erb @@ -1,14 +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, Record.allowed_record_types %> <% 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 %> diff --git a/test/models/record_test.rb b/test/models/record_test.rb new file mode 100644 index 0000000..1559023 --- /dev/null +++ b/test/models/record_test.rb @@ -0,0 +1,19 @@ +require 'test_helper' + +class RecordTest < ActiveSupport::TestCase + ['text', -1, 0, 2_147_483_647 + 1].each { |ttl| + test "ttl invalid #{ttl}" do + rec = build(:a, ttl: ttl) + rec.valid? + assert_not_empty rec.errors[:ttl], "ttl #{ttl} should be invalid!" + end + } + + ['', 1, 2_147_483_647].each { |ttl| + test "ttl valid #{ttl}" do + rec = build(:a, ttl: ttl) + rec.valid? + assert_empty rec.errors[:ttl], "ttl #{ttl} should be valid!" + end + } +end