Page Menu
Home
GRNET
Search
Configure Global Search
Log In
Files
F461587
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sun, May 18, 3:46 AM
Size
17 KB
Mime Type
text/x-diff
Expires
Tue, May 20, 3:46 AM (1 d, 14 h)
Engine
blob
Format
Raw Data
Handle
220343
Attached To
rWEBDNS WebDNS (edet4)
View Options
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 »'.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
Log In to Comment