Page MenuHomeGRNET

No OneTemporary

File Metadata

Created
Fri, Aug 29, 7:53 PM
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

Event Timeline