diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 95ca30d..65c59c6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,63 +1,72 @@ // 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') { // MX, default priority 10 $('#record_prio').parents('div.form-group').removeClass('hidden'); $('#record_prio').val('10'); } else if ($(this).val() == 'SRV') { // SRV $('#record_prio').val(''); $('#record_prio').parents('div.form-group').removeClass('hidden'); } else { $('#record_prio').val(''); $('#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'); } }); + // Disable DNSSEC options + $('#domain_dnssec').change(function() { + if ($(this).val()== 'true') { + $("#dnssec_fieldset").prop('disabled', false) + } else { + $("#dnssec_fieldset").prop('disabled', true); + } + }); + 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/application_controller.rb b/app/controllers/application_controller.rb index 954b3b3..6cdb77e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,65 +1,70 @@ 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? + helper_method :dnssec? def admin? return false if params.key?('user') return false if current_user.nil? @admin_count ||= begin current_user .groups .where(name: WebDNS.settings[:admin_group]).count end @admin_count != 0 end def admin_only! return if admin? redirect_to root_path, alert: 'Admin only area!' end + def dnssec? + WebDNS.settings[:dnssec] + end + private def group @group ||= edit_group_scope.find(params[:group_id] || params[:id]) end def domain @domain ||= edit_domain_scope.find(params[:domain_id] || params[:id]) end def record @record ||= record_scope.find(params[:record_id] || params[:id]) end def show_group_scope @show_group_scope ||= current_user.groups end def edit_group_scope @edit_group_scope ||= admin? ? Group.all : show_group_scope end def show_domain_scope @show_domain_scope ||= Domain.where(group: show_group_scope) end def edit_domain_scope @edit_domain_scope ||= admin? ? Domain.all : Domain.where(group: show_group_scope) end def record_scope @record_scope ||= domain.records end def notification Notification.instance end end diff --git a/app/controllers/domains_controller.rb b/app/controllers/domains_controller.rb index 077ca4c..10950b5 100644 --- a/app/controllers/domains_controller.rb +++ b/app/controllers/domains_controller.rb @@ -1,78 +1,86 @@ class DomainsController < ApplicationController before_action :authenticate_user! - before_action :domain, only: [:show, :edit, :update, :destroy] - before_action :group, only: [:show, :edit, :update, :destroy] + before_action :domain, only: [:show, :edit, :edit_dnssec, :update, :destroy] + before_action :group, only: [:show, :edit, :edit_dnssec, :update, :destroy] helper_method :edit_group_scope # GET /domains def index @domains = show_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(new_domain_params) end # GET /domains/1/edit def edit end + # GET /domains/1/edit_dnssec + def edit_dnssec + end + # POST /domains def create @domain = Domain.new(domain_params) if @domain.save notify_domain(@domain, :create) 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) notify_domain(@domain, :update) redirect_to @domain, notice: "#{@domain.name} was successfully updated." else - render :edit + if domain_params[:dnssec] # DNSSEC form + render :edit_dnssec + else + render :edit + end end end # DELETE /domains/1 def destroy @domain.destroy notify_domain(@domain, :destroy) redirect_to domains_url, notice: "#{@domain.name} was successfully destroyed." end private def group domain.group end def new_domain_params params.permit(:group_id) end def domain_params params.require(:domain).tap { |d| # Make sure group id is permitted (belongs to edit_group_scope) d[:group_id] = edit_group_scope.find_by_id(d[:group_id]).try(:id) - }.permit(:name, :type, :master, :group_id) + }.permit(:name, :type, :master, :group_id, :dnssec, :dnssec_parent, :dnssec_parent_authority) end def notify_domain(*args) notification.notify_domain(current_user, *args) end end diff --git a/app/models/domain.rb b/app/models/domain.rb index a88c911..741813b 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -1,187 +1,203 @@ class Domain < ActiveRecord::Base class NotAChild < StandardError; end self.inheritance_column = :nx # List all supported domain types. def self.domain_types [ 'NATIVE', 'MASTER', 'SLAVE', ] end # List domain types that can be created. def self.allowed_domain_types domain_types - WebDNS.settings[:prohibit_domain_types] end + # List parent authorities + def self.dnssec_parent_authorities + WebDNS.settings[:dnssec_parent_authorities] + end + belongs_to :group has_many :jobs has_many :records # BUG in bump_serial_trigger has_one :soa, -> { unscope(where: :type) }, 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? + validates :dnssec, inclusion: { in: [false] }, unless: :dnssec_elegible? + validates :dnssec_parent_authority, inclusion: { in: dnssec_parent_authorities }, if: :dnssec? + validates :dnssec_parent, hostname: true, if: :dnssec? + after_create :generate_soa after_create :generate_ns after_create :install before_save :fire_convert attr_writer :serial_strategy state_machine initial: :initial do after_transition(any => :pending_install) { |domain, _t| Job.add_domain(domain) } after_transition(any => :pending_remove) { |domain, _t| Job.remove_domain(domain) } after_transition(any => :pending_signing) { |domain, _t| Job.dnssec_sign(domain) } after_transition(any => :wait_for_ready) { |domain, _t| Job.wait_for_ready(domain) } after_transition(any => :pending_ds) { |domain, t| Job.dnssec_push_ds(domain, *t.args) } after_transition(any => :pending_plain) { |domain, _t| Job.convert_to_plain(domain) } after_transition(any => :destroy) { |domain, _t| domain.destroy } # User events event :install do transition initial: :pending_install end event :dnssec_sign do transition operational: :pending_signing end event :signed do transition pending_signing: :wait_for_ready end event :push_ds do # TODO: push_ds is triggered on multiple occasions # operational: :operational transition wait_for_ready: :pending_ds end event :plain_convert do transition operational: :pending_plain end event :remove do transition operational: :pending_remove end # Machine events event :installed do transition pending_install: :operational end event :converted do transition [:pending_ds, :pending_plain] => :operational end event :cleaned_up do transition pending_remove: :destroy end end + # Returns true if this domain is elegigble for DNSSEC + def dnssec_elegible? + return false if slave? + + true + end + # 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 def self.replace_ds(parent, child, records) parent = find_by_name!(parent) fail NotAChild if not child.end_with?(parent.name) existing = parent.records.where(name: child, type: 'DS') recs = records.map { |rec| DS.new(domain: parent, name: child, content: rec) } ActiveRecord::Base.transaction do existing.destroy_all recs.map(&:save!) 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 def fire_convert return if !dnssec_changed? event = dnssec ? :dnssec_convert : :plain_convert return true if fire_state_event(event) errors.add(:dnssec, 'You cannot modify dnssec settings in this state!') false end end diff --git a/app/views/domains/_dnssec_form.html.erb b/app/views/domains/_dnssec_form.html.erb new file mode 100644 index 0000000..534fdb1 --- /dev/null +++ b/app/views/domains/_dnssec_form.html.erb @@ -0,0 +1,12 @@ +<%= bootstrap_form_for(@domain, layout: :horizontal, label_col: 'col-sm-2', control_col: 'col-sm-4') do |f| %> + <%= f.hidden_field :group_id %> + <%= f.static_control :name %> + <%= f.select :dnssec, [['Enable', true], ['Disable', false]] %> + +
+ <%= f.submit 'Save', class: 'btn btn-primary col-sm-offset-2' %> +<% end %> diff --git a/app/views/domains/_form.html.erb b/app/views/domains/_form.html.erb index 6a32f42..90fe862 100644 --- a/app/views/domains/_form.html.erb +++ b/app/views/domains/_form.html.erb @@ -1,7 +1,10 @@ <%= 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, edit_group_scope, :id, :name %> <%= f.select :type, Domain.allowed_domain_types %> <%= f.text_field :master, wrapper_class: 'hidden' %> <%= f.submit 'Save', class: 'btn btn-primary col-sm-offset-2' %> + <% if dnssec? && @domain.dnssec_elegible? %> + <%= link_to 'Setup DNSSEC', edit_dnssec_domain_path(@domain), class: 'btn btn-default' %> + <% end %> <% end %> diff --git a/app/views/domains/edit_dnssec.html.erb b/app/views/domains/edit_dnssec.html.erb new file mode 100644 index 0000000..ab62777 --- /dev/null +++ b/app/views/domains/edit_dnssec.html.erb @@ -0,0 +1,3 @@ +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.
<%= link_to group.name, group_path(group) %> <%= link_to glyph('menu-down'), "##{group.id}", onclick: "$('tr.group-#{group.id}').toggleClass('hidden');" %> | State | Controls | ||
---|---|---|---|---|
<% 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 %> + <% if domain.dnssec? %> + <%= abbr_glyph('flash', 'DNSSEC') %> + <% end %> | <%= link_to domain.name, domain %> | <%= human_state(domain.state) %> | <%= 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 7eee3b4..4d2b300 100644 --- a/app/views/groups/show.html.erb +++ b/app/views/groups/show.html.erb @@ -1,82 +1,85 @@ <% content_for :more_breadcrumbs do %>Domain | State | Controls | ||
---|---|---|---|---|
<% 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 %> + <% if domain.dnssec? %> + <%= abbr_glyph('flash', 'DNSSEC') %> + <% end %> | <%= link_to domain.name, domain %> | <%= human_state(domain.state) %> | <%= 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' %>
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 %>