From bea0bb39d6c1762c97da484ffa8b5d73341e67e2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 12 Mar 2020 22:35:20 +0100 Subject: [PATCH] Add option to include resolved DNS records when blacklisting e-mail domains in admin UI (#13254) * Add shortcuts to blacklist a user's e-mail domain in admin UI * Add option to blacklist resolved MX and IP records for e-mail domains --- .../admin/email_domain_blocks_controller.rb | 28 +++++++++++++++++-- app/models/email_domain_block.rb | 14 ++++++++++ app/views/admin/accounts/show.html.haml | 13 +++++++-- .../_email_domain_block.html.haml | 10 +++++++ .../admin/email_domain_blocks/new.html.haml | 5 +++- config/locales/en.yml | 3 ++ config/locales/simple_form.en.yml | 5 ++++ ...43_add_parent_id_to_email_domain_blocks.rb | 5 ++++ db/schema.rb | 4 ++- 9 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 9fe85064e..c25919726 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -6,12 +6,12 @@ module Admin def index authorize :email_domain_block, :index? - @email_domain_blocks = EmailDomainBlock.page(params[:page]) + @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page]) end def new authorize :email_domain_block, :create? - @email_domain_block = EmailDomainBlock.new + @email_domain_block = EmailDomainBlock.new(domain: params[:_domain]) end def create @@ -21,6 +21,28 @@ module Admin if @email_domain_block.save log_action :create, @email_domain_block + + if @email_domain_block.with_dns_records? + hostnames = [] + ips = [] + + Resolv::DNS.open do |dns| + dns.timeouts = 1 + + hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } + + ([@email_domain_block.domain] + hostnames).uniq.each do |hostname| + ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s }) + ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s }) + end + end + + (hostnames + ips).each do |hostname| + another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: @email_domain_block) + log_action :create, another_email_domain_block if another_email_domain_block.save + end + end + redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') else render :new @@ -41,7 +63,7 @@ module Admin end def resource_params - params.require(:email_domain_block).permit(:domain) + params.require(:email_domain_block).permit(:domain, :with_dns_records) end end end diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index bc70dea25..f50fa46ba 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -7,13 +7,27 @@ # domain :string default(""), not null # created_at :datetime not null # updated_at :datetime not null +# parent_id :bigint(8) # class EmailDomainBlock < ApplicationRecord include DomainNormalizable + belongs_to :parent, class_name: 'EmailDomainBlock', optional: true + has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy + validates :domain, presence: true, uniqueness: true, domain: true + def with_dns_records=(val) + @with_dns_records = ActiveModel::Type::Boolean.new.cast(val) + end + + def with_dns_records? + @with_dns_records + end + + alias with_dns_records with_dns_records? + def self.block?(email) _, domain = email.split('@', 2) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index a30b78db2..744d17d1f 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -96,10 +96,17 @@ = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user) %tr - %th= t('admin.accounts.email') - %td= @account.user_email + %th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email') + %td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= @account.user_email %td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user) + %tr + %td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{@account.user_email.split('@').last}") + + - if can?(:create, :email_domain_block) + %tr + %td= table_link_to 'ban', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: @account.user_email.split('@').last) + - if @account.user_unconfirmed_email.present? %tr %th= t('admin.accounts.unconfirmed_email') @@ -204,7 +211,7 @@ = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account) - unless @account.local? - - if DomainBlock.where(domain: @account.domain).exists? + - if DomainBlock.rule_for(@account.domain) = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive' diff --git a/app/views/admin/email_domain_blocks/_email_domain_block.html.haml b/app/views/admin/email_domain_blocks/_email_domain_block.html.haml index bf66c9001..41ab8c171 100644 --- a/app/views/admin/email_domain_blocks/_email_domain_block.html.haml +++ b/app/views/admin/email_domain_blocks/_email_domain_block.html.haml @@ -3,3 +3,13 @@ %samp= email_domain_block.domain %td = table_link_to 'trash', t('admin.email_domain_blocks.delete'), admin_email_domain_block_path(email_domain_block), method: :delete + +- email_domain_block.children.each do |child_email_domain_block| + %tr + %td + %samp= child_email_domain_block.domain + %span.muted-hint + = surround '(', ')' do + = t('admin.email_domain_blocks.from_html', domain: content_tag(:samp, email_domain_block.domain)) + %td + = table_link_to 'trash', t('admin.email_domain_blocks.delete'), admin_email_domain_block_path(child_email_domain_block), method: :delete diff --git a/app/views/admin/email_domain_blocks/new.html.haml b/app/views/admin/email_domain_blocks/new.html.haml index f372fa512..4a346f240 100644 --- a/app/views/admin/email_domain_blocks/new.html.haml +++ b/app/views/admin/email_domain_blocks/new.html.haml @@ -5,7 +5,10 @@ = render 'shared/error_messages', object: @email_domain_block .fields-group - = f.input :domain, wrapper: :with_label, label: t('admin.email_domain_blocks.domain') + = f.input :domain, wrapper: :with_block_label, label: t('admin.email_domain_blocks.domain') + + .fields-group + = f.input :with_dns_records, as: :boolean, wrapper: :with_label .actions = f.button :button, t('.create'), type: :submit diff --git a/config/locales/en.yml b/config/locales/en.yml index 05d92b121..13f22d922 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -92,6 +92,7 @@ en: delete: Delete destroyed_msg: Moderation note successfully destroyed! accounts: + add_email_domain_block: Blacklist e-mail domain approve: Approve approve_all: Approve all are_you_sure: Are you sure? @@ -172,6 +173,7 @@ en: staff: Staff user: User search: Search + search_same_email_domain: Other users with the same e-mail domain search_same_ip: Other users with the same IP shared_inbox_url: Shared inbox URL show: @@ -358,6 +360,7 @@ en: destroyed_msg: Successfully deleted e-mail domain from blacklist domain: Domain empty: No e-mail domains currently blacklisted. + from_html: from %{domain} new: create: Add domain title: New e-mail blacklist entry diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index f279a8e4f..fd56a35bf 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -54,6 +54,9 @@ en: whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word domain_allow: domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored + email_domain_block: + domain: This can be the domain name that shows up in the e-mail address, the MX record that domain resolves to, or IP of the server that MX record resolves to. Those will be checked upon user sign-up and the sign-up will be rejected. + with_dns_records: An attempt to resolve the given domain's DNS records will be made and the results will also be blacklisted featured_tag: name: 'You might want to use one of these:' form_challenge: @@ -152,6 +155,8 @@ en: username: Username username_or_email: Username or Email whole_word: Whole word + email_domain_block: + with_dns_records: Include MX records and IPs of the domain featured_tag: name: Hashtag interactions: diff --git a/db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb b/db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb new file mode 100644 index 000000000..03915040c --- /dev/null +++ b/db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb @@ -0,0 +1,5 @@ +class AddParentIdToEmailDomainBlocks < ActiveRecord::Migration[5.2] + def change + add_reference :email_domain_blocks, :parent, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :email_domain_blocks }, index: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 5d3651e52..7f28f2ec4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_03_12_144258) do +ActiveRecord::Schema.define(version: 2020_03_12_185443) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -336,6 +336,7 @@ ActiveRecord::Schema.define(version: 2020_03_12_144258) do t.string "domain", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "parent_id" t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true end @@ -869,6 +870,7 @@ ActiveRecord::Schema.define(version: 2020_03_12_144258) do add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade add_foreign_key "custom_filters", "accounts", on_delete: :cascade + add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade add_foreign_key "featured_tags", "accounts", on_delete: :cascade