diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8761224..fd70334 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,34 +1,39 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception attr_writer :breadcrumb + helper_method :admin? + + def admin? + params.key?(:admin) + end private def group @group ||= domain.group end def domain @domain ||= domain_scope.find(params[:domain_id] || params[:id]) end def record @record ||= record_scope.find(params[:record_id] || params[:id]) end def group_scope @group_scope ||= Group.all end def domain_scope @domain_scope ||= Domain.where(group: group_scope) end def record_scope @record_scope ||= domain.records end end diff --git a/app/controllers/records_controller.rb b/app/controllers/records_controller.rb index 2c8a1c0..60f7046 100644 --- a/app/controllers/records_controller.rb +++ b/app/controllers/records_controller.rb @@ -1,49 +1,53 @@ class RecordsController < ApplicationController 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) + params.require(:record).permit(:name, :content, :prio, :disabled).tap { |r| + r[:drop_privileges] = true if not admin? + } end def new_record_params - params.require(:record).permit(:name, :content, :type, :prio) + params.require(:record).permit(:name, :content, :type, :prio).tap { |r| + r[:drop_privileges] = true if not admin? + } end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be79..876d94e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,8 @@ module ApplicationHelper + def can_edit?(object) + return true if admin? + return true unless object.respond_to?(:editable?) + + object.editable? + end end diff --git a/app/models/record.rb b/app/models/record.rb index 9fe1d27..912eb05 100644 --- a/app/models/record.rb +++ b/app/models/record.rb @@ -1,75 +1,100 @@ 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 } + # 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(' ') 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 ec738f4..e866df0 100644 --- a/app/views/domains/show.html.erb +++ b/app/views/domains/show.html.erb @@ -1,31 +1,37 @@ <% @domain.records.each do |record| %> - - - + <% if can_edit?(record) %> + + + + <% else %> + <% end %>
Records Controls
<%= record.name %> 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) %><%= link_to 'Remove', [@domain, record], method: :delete, data: { confirm: 'Are you sure?' } %> + <% 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 'Remove', [@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 418f2ba..93c1fa7 100644 --- a/app/views/records/_form.html.erb +++ b/app/views/records/_form.html.erb @@ -1,14 +1,14 @@ <%= 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.record_types %> + <%= f.select :type, Record.allowed_record_types %> <% end %> <%= f.text_field :prio, placeholder: 10, wrapper_class: @record.supports_prio? ? '' : 'hidden' %> <%= f.text_field :content %> <%= f.submit 'Save', class: 'btn btn-primary col-sm-offset-2' %> <% end %> diff --git a/config/initializers/00_settings.rb b/config/initializers/00_settings.rb index 88695d4..c6bf360 100644 --- a/config/initializers/00_settings.rb +++ b/config/initializers/00_settings.rb @@ -1,13 +1,16 @@ WebDNS = Base WebDNS.settings[:soa_defaults] = { primary_ns: 'ns.example.com', contact: 'domainmaster@example.com', serial: 1, refresh: 10_800, retry: 3600, expire: 604_800, nx: 3600 } WebDNS.settings[:serial_strategy] = Strategies::Date + +# Don't allow to create SOA records +WebDNS.settings[:prohibit_records_types] = ['SOA'] diff --git a/test/models/ns_test.rb b/test/models/ns_test.rb index a099692..6d81be5 100644 --- a/test/models/ns_test.rb +++ b/test/models/ns_test.rb @@ -1,14 +1,29 @@ require 'test_helper' class NSTest < ActiveSupport::TestCase setup do @record = build(:ns) end test 'save' do @record.save assert_empty @record.errors end + test 'drop privileges on zone NS records' do + @record.drop_privileges = true + @record.save + + assert_not_empty @record.errors[:name] + end + + test 'doesnt drop privileges on non zone NS records' do + @record.name = 'other' + @record.drop_privileges = true + + @record.save + + assert_empty @record.errors[:name] + end end diff --git a/test/models/soa_test.rb b/test/models/soa_test.rb index a96a30f..4096ca2 100644 --- a/test/models/soa_test.rb +++ b/test/models/soa_test.rb @@ -1,60 +1,69 @@ require 'test_helper' class SOATest < ActiveSupport::TestCase def setup domain = create(:domain) @record = domain.soa end test 'bump_serial!' do @record.save! assert_serial_update @record do @record.bump_serial! end end test 'updating attributes bumps serial' do @record.save! assert_serial_update @record do @record.contact = 'admin@example.com' @record.save! end end + test 'drop privileges' do + @record.contact = 'admin@example.com' + @record.drop_privileges = true + assert_not @record.editable? + + @record.save + assert_not_empty @record.errors[:type] + end + class DateSerialTests < ActiveSupport::TestCase setup do domain = create(:date_domain) @record = domain.soa end test 'last bump of the day' do assert_equal Strategies::Date, @record.domain.serial_strategy freeze_time do last_for_day = Time.now.strftime('%Y%m%d99').to_i @record.serial = last_for_day @record.save! assert_serial_update @record do @record.bump_serial! end end end test 'existing serial points to a future date' do assert_equal Strategies::Date, @record.domain.serial_strategy freeze_time do future_day = (Time.now + 1.week).strftime('%Y%m%d00').to_i @record.serial = future_day @record.save! assert_serial_update @record do @record.bump_serial! end end end end end