Page MenuHomeGRNET

No OneTemporary

File Metadata

Created
Sun, May 18, 3:46 AM
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 67b4574..313e5f8 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,49 +1,58 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery.min
//= require jquery_ujs
//= require bootstrap.min
//= require typeahead.bundle.min
//= require_tree .
$(function() {
// Show priority on MX/SRV record only
$('#record_type').change(function() {
if ($(this).val() == 'MX' || $(this).val() == 'SRV') {
$('#record_prio').parents('div.form-group').removeClass('hidden');
} else {
$('#record_prio').parents('div.form-group').addClass('hidden');
}
});
+ // Show master only on SLAVE domains
+ $('#domain_type').change(function() {
+ if ($(this).val() == 'SLAVE') {
+ $('#domain_master').parents('div.form-group').removeClass('hidden');
+ } else {
+ $('#domain_master').parents('div.form-group').addClass('hidden');
+ }
+ });
+
var searchMembersGroup = $('#js-search-member').data('group');
var searchMembers = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('email'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: function(obj) { return obj.id; },
remote: {
url: '/groups/' + searchMembersGroup + '/search_member.json?q=%QUERY',
wildcard: '%QUERY'
}
});
$('#js-search-member').typeahead({
hint: true,
minLength: 2
}, {
name: 'members',
display: 'email',
source: searchMembers
});
});
diff --git a/app/controllers/domains_controller.rb b/app/controllers/domains_controller.rb
index 13d2bae..6eaeeaf 100644
--- a/app/controllers/domains_controller.rb
+++ b/app/controllers/domains_controller.rb
@@ -1,67 +1,67 @@
class DomainsController < ApplicationController
before_action :authenticate_user!
before_action :group_scope
before_action :domain, only: [:show, :edit, :update, :destroy]
before_action :group, only: [:show, :edit, :update, :destroy]
# GET /domains
def index
@domains = domain_scope.all
end
# GET /domains/1
def show
@record = Record.new(domain_id: @domain.id)
end
# GET /domains/new
def new
@domain = Domain.new
end
# GET /domains/1/edit
def edit
end
# POST /domains
def create
@domain = Domain.new(domain_params)
if @domain.save
redirect_to @domain, notice: "#{@domain.name} was successfully created."
else
render :new
end
end
# PATCH/PUT /domains/1
def update
if @domain.update(domain_params)
redirect_to @domain, notice: "#{@domain.name} was successfully updated."
else
render :edit
end
end
# DELETE /domains/1
def destroy
@domain.destroy
redirect_to domains_url, notice: "#{@domain.name} was successfully destroyed."
end
private
def group
domain.group
end
def domain_params
params.require(:domain).tap { |d|
# Make sure group id is permitted (belongs to group_scope)
d[:group_id] = group_scope.find_by_id(d[:group_id]).try(:id)
- }.permit(:name, :type, :group_id)
+ }.permit(:name, :type, :master, :group_id)
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7bc03bb..f356432 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,57 +1,57 @@
module ApplicationHelper
TIME_PERIODS = {
1.second => 'second',
1.minute => 'minute',
1.hour => 'hour',
1.day => 'day',
1.week => 'week',
1.month => 'month',
1.year => 'year',
}
def can_edit?(object)
- return true if admin?
return true unless object.respond_to?(:editable?)
+ by = admin? ? :admin : :user
- object.editable?
+ object.editable?(by)
end
def seconds_to_human(seconds)
acc = {}
remaining = seconds
TIME_PERIODS.to_a.reverse_each do |p, human|
period_count, remaining = remaining.divmod(p)
acc[human] = period_count if not period_count.zero?
end
acc.map { |singular, count|
human = count < 2 ? singular : "#{singular}s"
"#{count} #{human}"
}.join(', ')
end
def link_to_edit(*args, &block)
link_to(abbr_glyph(:pencil, 'Edit'), *args, &block)
end
def link_to_destroy(*args, &block)
link_to(abbr_glyph(:remove, 'Remove'), *args, &block)
end
def link_to_enable(*args, &block)
link_to(abbr_glyph(:'eye-close', 'Enable'), *args, &block)
end
def link_to_disable(*args, &block)
link_to(abbr_glyph(:'eye-open', 'Disable'), *args, &block)
end
def glyph(icon)
content_tag(:span, '', class: "glyphicon glyphicon-#{icon}")
end
def abbr_glyph(icon, title)
content_tag(:abbr, glyph(icon), title: title)
end
end
diff --git a/app/models/domain.rb b/app/models/domain.rb
index 1f69578..611167d 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -1,76 +1,81 @@
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
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
end
diff --git a/app/models/record.rb b/app/models/record.rb
index dd35396..47efd58 100644
--- a/app/models/record.rb
+++ b/app/models/record.rb
@@ -1,119 +1,140 @@
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',
]
end
def self.forward_records
record_types - ['SOA', 'PTR']
end
def self.reverse_records
['PTR', 'CNAME', 'TXT', 'NS']
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
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?
+ 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
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'
domain.soa.bump_serial!
end
end
diff --git a/app/views/domains/_form.html.erb b/app/views/domains/_form.html.erb
index 30a9cfa..914c298 100644
--- a/app/views/domains/_form.html.erb
+++ b/app/views/domains/_form.html.erb
@@ -1,6 +1,7 @@
<%= bootstrap_form_for(@domain, layout: :horizontal, label_col: 'col-sm-2', control_col: 'col-sm-4') do |f| %>
<%= f.text_field :name %>
<%= f.collection_select :group_id, @group_scope, :id, :name %>
<%= f.select :type, Domain.domain_types %>
+ <%= f.text_field :master, wrapper_class: 'hidden' %>
<%= f.submit 'Save', class: 'btn btn-primary col-sm-offset-2' %>
<% end %>
diff --git a/app/views/domains/index.html.erb b/app/views/domains/index.html.erb
index ca04204..b9e32cb 100644
--- a/app/views/domains/index.html.erb
+++ b/app/views/domains/index.html.erb
@@ -1,31 +1,34 @@
<table class="table table-striped">
<thead>
</thead>
<tbody>
<% @domains.group_by(&:group).each do |group, domains| %>
<tr>
<th colspan="2"><%= link_to group.name, group_path(group) %></th>
<th colspan="2">Controls</th>
</tr>
<% domains.each do |domain| %>
<tr>
<td><%= link_to domain.name, domain %></td>
<td>
<% if domain.reverse? %>
<%= abbr_glyph('chevron-left', 'Reverse') %>
<% else %>
<%= abbr_glyph('chevron-right', 'Forward') %>
<% end %>
+ <% if domain.slave? %>
+ <%= abbr_glyph('link', 'Slave') %>
+ <% end %>
</td>
<td><%= link_to_edit edit_domain_path(domain) %></td>
<td><%= link_to_destroy domain, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<p>
<%= link_to 'New Domain &raquo;'.html_safe, new_domain_path, class: 'btn btn-lg btn-primary' %>
</p>
diff --git a/app/views/domains/show.html.erb b/app/views/domains/show.html.erb
index f95db1c..d6f46e0 100644
--- a/app/views/domains/show.html.erb
+++ b/app/views/domains/show.html.erb
@@ -1,44 +1,44 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th colspan="6">Records</th>
- <th colspan="3">Controls</th>
+ <th colspan="3"><%= 'Controls' if !@domain.slave? %></th>
</tr>
</thead>
<tbody>
<% @domain.records.each do |record| %>
<tr class="<%= record.disabled? ? 'warning' : '' %>">
<td><%= record.name %></td>
<td><%= record.ttl %></td>
<td>IN</td>
<td><%= record.type %></td>
<td><%= record.supports_prio? ? record.prio : '' %></td>
<td><%= record.content %></td>
<% if can_edit?(record) %>
<td>
<% 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 %>
</td>
<td><%= link_to_edit edit_domain_record_path(@domain, record) if can_edit?(record) %></td>
<td><%= link_to_destroy [@domain, record], method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% else %>
<td/>
<td/>
<td/>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to 'Add Record', '#new_record', class: 'btn btn-primary', onclick: '$("#new_record_wrapper").toggleClass("hidden");' %>
</p>
<div class="jumbotron hidden" id="new_record_wrapper">
<%= render 'records/form' %>
</div>
diff --git a/test/factories/domain.rb b/test/factories/domain.rb
index e69573d..6f74dd5 100644
--- a/test/factories/domain.rb
+++ b/test/factories/domain.rb
@@ -1,24 +1,29 @@
FactoryGirl.define do
sequence(:domain) { |n| "example#{n}.com" }
factory :domain do
group
name { generate(:domain) }
serial_strategy Strategies::Date
type 'NATIVE'
end
+ factory :slave, parent: :domain do
+ type 'SLAVE'
+ master '1.2.3.4'
+ end
+
factory :date_domain, class: Domain do
group
name { generate(:domain) }
serial_strategy Strategies::Date
type 'NATIVE'
end
factory :v4_arpa_domain, parent: :domain do
name '2.0.192.in-addr.arpa'
end
factory :v6_arpa_domain, parent: :domain do
name '8.b.d.0.1.0.0.2.ip6.arpa'
end
end
diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb
index 986fbc4..6c8b2a0 100644
--- a/test/models/domain_test.rb
+++ b/test/models/domain_test.rb
@@ -1,47 +1,81 @@
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 '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

Event Timeline