Page MenuHomeGRNET

No OneTemporary

File Metadata

Created
Sun, May 17, 10:30 AM
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 748289c..954b3b3 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,62 +1,65 @@
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?
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
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 160b07b..72f9c0b 100644
--- a/app/controllers/domains_controller.rb
+++ b/app/controllers/domains_controller.rb
@@ -1,67 +1,74 @@
class DomainsController < ApplicationController
before_action :authenticate_user!
before_action :domain, only: [:show, :edit, :update, :destroy]
before_action :group, only: [:show, :edit, :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
end
# GET /domains/1/edit
def edit
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
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 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)
end
+ def notify_domain(*args)
+ notification.notify_domain(current_user, *args)
+ end
+
end
diff --git a/app/controllers/records_controller.rb b/app/controllers/records_controller.rb
index 2090230..5123ddc 100644
--- a/app/controllers/records_controller.rb
+++ b/app/controllers/records_controller.rb
@@ -1,62 +1,69 @@
class RecordsController < ApplicationController
before_action :authenticate_user!
before_action :domain
before_action :record, only: [:edit, :update, :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
# DELETE /records/1
def destroy
@record.destroy
+ notify_record(@record, :destroy)
redirect_to domain, notice: 'Record was successfully destroyed.'
end
private
def edit_record_params
if @record.type == 'SOA'
permitted = [:contact, :serial, :refresh, :retry, :expire, :nx]
else
permitted = [:name, :content, :ttl, :prio, :disable]
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)
+ end
end
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
new file mode 100644
index 0000000..c7cde23
--- /dev/null
+++ b/app/mailers/notification_mailer.rb
@@ -0,0 +1,29 @@
+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_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_domain.text.erb b/app/views/notification_mailer/notify_domain.text.erb
new file mode 100644
index 0000000..ab2b51d
--- /dev/null
+++ b/app/views/notification_mailer/notify_domain.text.erb
@@ -0,0 +1,16 @@
+Action: <%= @context %>
+By: <%= @user.email %> <%= '(admin)' if @admin %>
+
+Domain: <%= @domain.name %>
+Type: <%= @domain.type %>
+<% if @domain.slave? %>
+Master: <%= @domain.master %>
+<% end -%>
+
+<% if @context == :update -%>
+Changes:
+<% @changes.each do |key, change| -%>
+<%= key %> from <%= change.first || "(empty)" %>
+<%= key %> to <%= change.last %>
+<% end -%>
+<% end -%>
diff --git a/app/views/notification_mailer/notify_record.text.erb b/app/views/notification_mailer/notify_record.text.erb
new file mode 100644
index 0000000..db5e335
--- /dev/null
+++ b/app/views/notification_mailer/notify_record.text.erb
@@ -0,0 +1,15 @@
+Action: <%= @context %>
+By: <%= @user.email %> <%= '(admin)' if @admin %>
+
+Record: <%= @record.name %>
+Type: <%= @record.type %>
+Domain: <%= @record.domain.name %>
+State: <%= @record.to_dns %>
+
+<% if @context == :update -%>
+Changes:
+<% @changes.each do |key, change| -%>
+<%= key %> from <%= change.first %>
+<%= key %> to <%= change.last %>
+<% end -%>
+<% end -%>
diff --git a/config/initializers/notification.rb b/config/initializers/notification.rb
new file mode 100644
index 0000000..ef750d3
--- /dev/null
+++ b/config/initializers/notification.rb
@@ -0,0 +1 @@
+Notification.instance.hook
diff --git a/lib/notification.rb b/lib/notification.rb
new file mode 100644
index 0000000..ec8063c
--- /dev/null
+++ b/lib/notification.rb
@@ -0,0 +1,93 @@
+require 'singleton'
+
+class Notification
+ include Singleton
+
+ # 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_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_domain
+ ActiveSupport::Notifications
+ .subscribe 'webdns.domain' do |_name, _started, _finished, _unique_id, data|
+ handle_domain(data)
+ end
+ end
+
+ def handle_record(data) # rubocop:disable Metrics/MethodLength
+ record, context, user = data.values_at(:object, :context, :user)
+ domain = record.domain
+ changes = record.previous_changes
+
+ # Nobody is interested in those
+ changes.delete('updated_at')
+ changes.delete('created_at')
+ return if changes.empty?
+
+ 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_domain(data) # rubocop:disable Metrics/MethodLength
+ domain, context, user = data.values_at(:object, :context, :user)
+ changes = domain.previous_changes
+
+ # Nobody is interested in those
+ changes.delete('updated_at')
+ changes.delete('created_at')
+ return if changes.empty?
+
+ 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
+end
diff --git a/test/mailers/notification_mailer_test.rb b/test/mailers/notification_mailer_test.rb
new file mode 100644
index 0000000..33a9d40
--- /dev/null
+++ b/test/mailers/notification_mailer_test.rb
@@ -0,0 +1,146 @@
+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
+ end
+end

Event Timeline