diff --git a/app/controllers/records_controller.rb b/app/controllers/records_controller.rb index 162ef80..3a9fd47 100644 --- a/app/controllers/records_controller.rb +++ b/app/controllers/records_controller.rb @@ -1,139 +1,143 @@ class RecordsController < ApplicationController before_action :authenticate_user! before_action :domain, except: [:search] before_action :editable_transform_params, only: [:editable] before_action :record, only: [:edit, :update, :editable, :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 notify_record(@record, :create) redirect_to domain, notice: 'Record was successfully created.' else flash[:alert] = 'There were some errors creating the record!' render :new end end # PATCH/PUT /records/1 def update if @record.update(edit_record_params) notify_record(@record, :update) redirect_to domain, notice: 'Record was successfully updated.' else render :edit end end def valid @record = domain.records.new(new_record_params) if @record.valid? response = { record: @record.as_bulky_json, errors: false } render json: response else render json: { errors: @record.errors.full_messages.join(', ') } end end def bulk ops, err = @domain.bulk(params) if err.empty? notify_record_bulk(@domain, ops) render json: { ok: true } else render json: { errors: err } end end def editable @record.assign_attributes(edit_record_params) if @record.valid? if @save @record.save! notify_record(@record, :update) end response = { attribute: @attr, value: @record.read_attribute(@attr), serial: @domain.soa(true).serial, record: @record.as_bulky_json, saved: @save } render json: response else render text: @record.errors[@attr].join(', '), status: 400 end end # DELETE /records/1 def destroy if @record.type == 'SOA' redirect_to domain, alert: 'SOA records cannot be deleted!' return end @record.destroy notify_record(@record, :destroy) redirect_to domain, notice: 'Record was successfully destroyed.' end # GET /search def search @records = Record .where(domain: show_domain_scope) .includes(:domain) .search(params[:q]) # scope by domain @records = Record.smart_order(@records) end private # Modify params to use standard Rails patterns def editable_transform_params @attr = params[:name] @save = params[:save] != 'false' params[:record] = { params[:name] => params[:value] } end def edit_record_params if @record.type == 'SOA' permitted = [:contact, :serial, :refresh, :retry, :expire, :nx] else permitted = [:name, :content, :ttl, :prio, :disabled] end params.require(:record).permit(*permitted).tap { |r| r[:drop_privileges] = true if not admin? } end def new_record_params params.require(:record).permit(:name, :content, :ttl, :type, :prio).tap { |r| r[:drop_privileges] = true if not admin? } end def notify_record(*args) notification.notify_record(current_user, *args) if WebDNS.settings[:notifications] end + + def notify_record_bulk(*args) + notification.notify_record_bulk(current_user, *args) if WebDNS.settings[:notifications] + end end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index c7cde23..1f5a649 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -1,29 +1,39 @@ class NotificationMailer < ActionMailer::Base default from: WebDNS.settings[:mail_from] PREFIXES = { create: 'Created', update: 'Modified', destroy: 'Deleted', } def notify_record(record:, context:, user:, admin:, others:, changes:) @record = record @context = context @user = user @admin = admin @changes = changes mail(to: others, subject: "[webdns] [record] #{PREFIXES[context.to_sym]} #{record.to_short_dns}") end + def notify_record_bulk(domain:, user:, admin:, others:, operations:) + @domain = domain + @user = user + @admin = admin + @operations = operations + + mail(to: others, subject: "[webdns] [record] Bulk operations for '#{domain.name}'") + end + def notify_domain(domain:, context:, user:, admin:, others:, changes:) @domain = domain @context = context @user = user @admin = admin @changes = changes mail(to: others, subject: "[webdns] [domain] #{PREFIXES[context.to_sym]} #{domain.name}") end + end diff --git a/app/views/notification_mailer/notify_record_bulk.text.erb b/app/views/notification_mailer/notify_record_bulk.text.erb new file mode 100644 index 0000000..f16b233 --- /dev/null +++ b/app/views/notification_mailer/notify_record_bulk.text.erb @@ -0,0 +1,17 @@ +Domain: <%= @domain.name %> +By: <%= @user.email %> <%= '(admin)' if @admin %> + +<% @operations.each do |context, record, changes| %> + +Action: <%= context %> +Record: <%= record.name %> +Type: <%= record.type %> +State: <%= record.to_dns %> +<% if context == :update -%> +Changes: +<% changes.each do |key, change| -%> +<%= key %> from <%= change.first %> +<%= key %> to <%= change.last %> +<% end -%> +<% end -%> +<% end -%> diff --git a/lib/notification.rb b/lib/notification.rb index ef60c99..2caa0da 100644 --- a/lib/notification.rb +++ b/lib/notification.rb @@ -1,100 +1,139 @@ require 'singleton' class Notification include Singleton + # Send out a notification about bulk record operations. + def notify_record_bulk(user, domain, ops) + ActiveSupport::Notifications.instrument( + 'webdns.record.bulk', + user: user, + domain: domain, + ops: ops) + end + # Send out a notification about notable record changes. def notify_record(user, record, context) ActiveSupport::Notifications.instrument( 'webdns.record', user: user, context: context, object: record) end # Send out a notification about notable domain changes. def notify_domain(user, domain, context) ActiveSupport::Notifications.instrument( 'webdns.domain', user: user, context: context, object: domain) end # Subscribe to domain/record notifications. def hook hook_record + hook_record_bulk hook_domain end private def hook_record ActiveSupport::Notifications .subscribe 'webdns.record' do |_name, _started, _finished, _unique_id, data| handle_record(data) end end + def hook_record_bulk + ActiveSupport::Notifications + .subscribe 'webdns.record.bulk' do |_name, _started, _finished, _unique_id, data| + handle_record_bulk(data) + end + end + def hook_domain ActiveSupport::Notifications .subscribe 'webdns.domain' do |_name, _started, _finished, _unique_id, data| handle_domain(data) end end def handle_record(data) record, context, user = data.values_at(:object, :context, :user) domain = record.domain changes = filter_changes(record) return if changes.empty? && context == :update others = domain.group.users.where.not(id: user.id).pluck(:email) return if others.empty? admin_action = !user.groups.exists?(domain.group_id) NotificationMailer.notify_record( record: record, context: context, user: user, admin: admin_action, others: others, changes: changes ).deliver end + def handle_record_bulk(data) + ops, domain, user = data.values_at(:ops, :domain, :user) + + operations = [] + operations += ops[:deletes].map { |rec| [:destroy, rec, nil] } + operations += ops[:changes].map { |rec| [:update, rec, filter_changes(rec)] } + operations += ops[:additions].map { |rec| [:create, rec, nil] } + + others = domain.group.users.where.not(id: user.id).pluck(:email) + return if others.empty? + + admin_action = !user.groups.exists?(domain.group_id) + + NotificationMailer.notify_record_bulk( + user: user, + admin: admin_action, + others: others, + domain: domain, + operations: operations, + ).deliver + end + def handle_domain(data) domain, context, user = data.values_at(:object, :context, :user) changes = filter_changes(domain) return if changes.empty? && context == :update others = domain.group.users.where.not(id: user.id).pluck(:email) return if others.empty? admin_action = !user.groups.exists?(domain.group_id) NotificationMailer.notify_domain( domain: domain, context: context, user: user, admin: admin_action, others: others, changes: changes ).deliver end private def filter_changes(record) changes = record.previous_changes # Nobody is interested in those changes.delete('updated_at') changes.delete('created_at') changes end end diff --git a/test/mailers/notification_mailer_test.rb b/test/mailers/notification_mailer_test.rb index 33a9d40..4d3bb4c 100644 --- a/test/mailers/notification_mailer_test.rb +++ b/test/mailers/notification_mailer_test.rb @@ -1,146 +1,175 @@ require 'test_helper' class NotificationMailerTest < ActionMailer::TestCase class DomainNotificationMailerTest < ActionMailer::TestCase def setup @notification = Notification.instance @group = create(:group_with_users) @domain = create(:domain, group: @group) @record = build(:a, name: 'a', domain: @domain) end test 'domain add' do @record.save! @notification.notify_domain(@group.users.first, @domain, :create) assert_not ActionMailer::Base.deliveries.empty? mail = ActionMailer::Base.deliveries.last assert_equal [@group.users.last.email], mail.to assert_includes mail.subject, 'Created' assert_includes mail.body.to_s, "Domain: #{@domain.name}" assert_includes mail.body.to_s, "By: #{@group.users.first.email}" end test 'domain edit' do @record.save! @domain.type = 'SLAVE' @domain.master = '1.2.3.4' @domain.save! @notification.notify_domain(@group.users.first, @domain, :update) assert_not ActionMailer::Base.deliveries.empty? mail = ActionMailer::Base.deliveries.last assert_equal [@group.users.last.email], mail.to assert_includes mail.subject, 'Modified' assert_includes mail.body.to_s, "Domain: #{@domain.name}" assert_includes mail.body.to_s, "By: #{@group.users.first.email}" assert_includes mail.body.to_s, 'type from NATIVE' assert_includes mail.body.to_s, 'type to SLAVE' assert_includes mail.body.to_s, 'master from (empty)' assert_includes mail.body.to_s, 'master to 1.2.3.4' end test 'domain destroy' do @record.save! @domain.destroy! @notification.notify_domain(@group.users.first, @domain, :destroy) assert_not ActionMailer::Base.deliveries.empty? mail = ActionMailer::Base.deliveries.last assert_equal [@group.users.last.email], mail.to assert_includes mail.subject, 'Deleted' assert_includes mail.body.to_s, "Domain: #{@domain.name}" assert_includes mail.body.to_s, "By: #{@group.users.first.email}" end end class DomainNotificationMailerTest < ActionMailer::TestCase test 'record add' do @record.save! @notification.notify_record(@group.users.first, @record, :create) assert_not ActionMailer::Base.deliveries.empty? mail = ActionMailer::Base.deliveries.last assert_equal [@group.users.last.email], mail.to assert_includes mail.subject, 'Created' assert_includes mail.body.to_s, "Record: #{@record.name}" assert_includes mail.body.to_s, "Domain: #{@domain.name}" assert_includes mail.body.to_s, "State: #{@record.to_dns}" assert_includes mail.body.to_s, "By: #{@group.users.first.email}" end test 'record edit' do @record.save! prev_content = @record.content @record.content = '1.1.1.1' @record.save! @notification.notify_record(@group.users.first, @record, :update) assert_not ActionMailer::Base.deliveries.empty? mail = ActionMailer::Base.deliveries.last assert_equal [@group.users.last.email], mail.to assert_includes mail.subject, 'Modified' assert_includes mail.body.to_s, "Record: #{@record.name}" assert_includes mail.body.to_s, "Domain: #{@domain.name}" assert_includes mail.body.to_s, "State: #{@record.to_dns}" assert_includes mail.body.to_s, "By: #{@group.users.first.email}" assert_includes mail.body.to_s, "content from #{prev_content}" assert_includes mail.body.to_s, 'content to 1.1.1.1' end test 'soa edit' do @record = @domain.soa prev_content = @record.content @record.nx = 10 @record.save! @notification.notify_record(@group.users.first, @record, :update) assert_not ActionMailer::Base.deliveries.empty? mail = ActionMailer::Base.deliveries.last assert_equal [@group.users.last.email], mail.to assert_includes mail.subject, 'Modified' assert_includes mail.body.to_s, "Record: #{@record.name}" assert_includes mail.body.to_s, "Domain: #{@domain.name}" assert_includes mail.body.to_s, "State: #{@record.to_dns}" assert_includes mail.body.to_s, "By: #{@group.users.first.email}" assert_includes mail.body.to_s, "content from #{prev_content}" assert_includes mail.body.to_s, "content to #{@record.content}" assert_includes mail.body.to_s, ' 10' end test 'record destroy' do @record.save! @record.destroy! @notification.notify_record(@group.users.first, @record, :destroy) assert_not ActionMailer::Base.deliveries.empty? mail = ActionMailer::Base.deliveries.last assert_equal [@group.users.last.email], mail.to assert_includes mail.subject, 'Deleted' assert_includes mail.body.to_s, "Record: #{@record.name}" assert_includes mail.body.to_s, "Domain: #{@domain.name}" assert_includes mail.body.to_s, "By: #{@group.users.first.email}" end + + test 'bulk operations' do + a = create(:a, domain: @domain) + aaaa = create(:aaaa, domain: @domain) + new = build(:mx, domain: @domain) + + changes = {}.tap { |c| + c[:deletes] = [a.id] + c[:changes] = { aaaa.id => { content: '::42' }} + c[:additions] = { 1 => new.as_bulky_json } + } + + ops, err = @domain.bulk(changes) + assert_empty err + + @notification.notify_record_bulk(@group.users.first, @domain, ops) + + assert_not ActionMailer::Base.deliveries.empty? + mail = ActionMailer::Base.deliveries.last + + assert_equal [@group.users.last.email], mail.to + assert_includes mail.subject, 'Bulk' + assert_includes mail.body.to_s, "Domain: #{@domain.name}" + assert_includes mail.body.to_s, "By: #{@group.users.first.email}" + assert_includes mail.body.to_s, "Action: destroy" + assert_includes mail.body.to_s, "Action: update" + assert_includes mail.body.to_s, "Action: create" + end + end end