Change e-mail domain blocks to block IPs dynamically (#17635)
* Change e-mail domain blocks to block IPs dynamically * Update app/workers/scheduler/email_domain_block_refresh_scheduler.rb Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh> * Update app/workers/scheduler/email_domain_block_refresh_scheduler.rb Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh> Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh>
This commit is contained in:
		| @@ -6,7 +6,20 @@ module Admin | ||||
|  | ||||
|     def index | ||||
|       authorize :email_domain_block, :index? | ||||
|  | ||||
|       @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page]) | ||||
|       @form                = Form::EmailDomainBlockBatch.new | ||||
|     end | ||||
|  | ||||
|     def batch | ||||
|       @form = Form::EmailDomainBlockBatch.new(form_email_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) | ||||
|       @form.save | ||||
|     rescue ActionController::ParameterMissing | ||||
|       flash[:alert] = I18n.t('admin.email_domain_blocks.no_email_domain_block_selected') | ||||
|     rescue Mastodon::NotPermittedError | ||||
|       flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') | ||||
|     ensure | ||||
|       redirect_to admin_email_domain_blocks_path | ||||
|     end | ||||
|  | ||||
|     def new | ||||
| @@ -19,41 +32,25 @@ module Admin | ||||
|  | ||||
|       @email_domain_block = EmailDomainBlock.new(resource_params) | ||||
|  | ||||
|       if @email_domain_block.save | ||||
|         log_action :create, @email_domain_block | ||||
|       if action_from_button == 'save' | ||||
|         EmailDomainBlock.transaction do | ||||
|           @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 = 5 | ||||
|  | ||||
|             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 | ||||
|           (@email_domain_block.other_domains || []).uniq.each do |domain| | ||||
|             other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block) | ||||
|             log_action :create, other_email_domain_block | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') | ||||
|       else | ||||
|         set_resolved_records | ||||
|         render :new | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def destroy | ||||
|       authorize @email_domain_block, :destroy? | ||||
|       @email_domain_block.destroy! | ||||
|       log_action :destroy, @email_domain_block | ||||
|       redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') | ||||
|     rescue ActiveRecord::RecordInvalid | ||||
|       set_resolved_records | ||||
|       render :new | ||||
|     end | ||||
|  | ||||
|     private | ||||
| @@ -62,8 +59,27 @@ module Admin | ||||
|       @email_domain_block = EmailDomainBlock.find(params[:id]) | ||||
|     end | ||||
|  | ||||
|     def set_resolved_records | ||||
|       Resolv::DNS.open do |dns| | ||||
|         dns.timeouts = 5 | ||||
|         @resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def resource_params | ||||
|       params.require(:email_domain_block).permit(:domain, :with_dns_records) | ||||
|       params.require(:email_domain_block).permit(:domain, other_domains: []) | ||||
|     end | ||||
|  | ||||
|     def form_email_domain_block_batch_params | ||||
|       params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: []) | ||||
|     end | ||||
|  | ||||
|     def action_from_button | ||||
|       if params[:delete] | ||||
|         'delete' | ||||
|       elsif params[:save] | ||||
|         'save' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -3,11 +3,13 @@ | ||||
| # | ||||
| # Table name: email_domain_blocks | ||||
| # | ||||
| #  id         :bigint(8)        not null, primary key | ||||
| #  domain     :string           default(""), not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  parent_id  :bigint(8) | ||||
| #  id              :bigint(8)        not null, primary key | ||||
| #  domain          :string           default(""), not null | ||||
| #  created_at      :datetime         not null | ||||
| #  updated_at      :datetime         not null | ||||
| #  parent_id       :bigint(8) | ||||
| #  ips             :inet             is an Array | ||||
| #  last_refresh_at :datetime | ||||
| # | ||||
|  | ||||
| class EmailDomainBlock < ApplicationRecord | ||||
| @@ -18,27 +20,42 @@ class EmailDomainBlock < ApplicationRecord | ||||
|  | ||||
|   validates :domain, presence: true, uniqueness: true, domain: true | ||||
|  | ||||
|   def with_dns_records=(val) | ||||
|     @with_dns_records = ActiveModel::Type::Boolean.new.cast(val) | ||||
|   # Used for adding multiple blocks at once | ||||
|   attr_accessor :other_domains | ||||
|  | ||||
|   def history | ||||
|     @history ||= Trends::History.new('email_domain_blocks', id) | ||||
|   end | ||||
|  | ||||
|   def with_dns_records? | ||||
|     @with_dns_records | ||||
|   end | ||||
|   def self.block?(domain_or_domains, ips: [], attempt_ip: nil) | ||||
|     domains = Array(domain_or_domains).map do |str| | ||||
|       domain = begin | ||||
|         if str.include?('@') | ||||
|           str.split('@', 2).last | ||||
|         else | ||||
|           str | ||||
|         end | ||||
|       end | ||||
|  | ||||
|   alias with_dns_records with_dns_records? | ||||
|  | ||||
|   def self.block?(email) | ||||
|     _, domain = email.split('@', 2) | ||||
|  | ||||
|     return true if domain.nil? | ||||
|  | ||||
|     begin | ||||
|       domain = TagManager.instance.normalize_domain(domain) | ||||
|       TagManager.instance.normalize_domain(domain) if domain.present? | ||||
|     rescue Addressable::URI::InvalidURIError | ||||
|       return true | ||||
|       nil | ||||
|     end | ||||
|  | ||||
|     where(domain: domain).exists? | ||||
|     # If some of the inputs passed in are invalid, we definitely want to | ||||
|     # block the attempt, but we also want to register hits against any | ||||
|     # other valid matches | ||||
|  | ||||
|     blocked = domains.any?(&:nil?) | ||||
|  | ||||
|     scope = where(domain: domains) | ||||
|     scope = scope.or(where('ips && ARRAY[?]::inet[]', ips)) if ips.any? | ||||
|  | ||||
|     scope.find_each do |block| | ||||
|       blocked = true | ||||
|       block.history.add(attempt_ip) if attempt_ip.present? | ||||
|     end | ||||
|  | ||||
|     blocked | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										30
									
								
								app/models/form/email_domain_block_batch.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/models/form/email_domain_block_batch.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Form::EmailDomainBlockBatch | ||||
|   include ActiveModel::Model | ||||
|   include Authorization | ||||
|   include AccountableConcern | ||||
|  | ||||
|   attr_accessor :email_domain_block_ids, :action, :current_account | ||||
|  | ||||
|   def save | ||||
|     case action | ||||
|     when 'delete' | ||||
|       delete! | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def email_domain_blocks | ||||
|     @email_domain_blocks ||= EmailDomainBlock.where(id: email_domain_block_ids) | ||||
|   end | ||||
|  | ||||
|   def delete! | ||||
|     email_domain_blocks.each do |email_domain_block| | ||||
|       authorize(email_domain_block, :destroy?) | ||||
|       email_domain_block.destroy! | ||||
|       log_action :destroy, email_domain_block | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -24,6 +24,7 @@ | ||||
| #  poll_id                :bigint(8) | ||||
| #  deleted_at             :datetime | ||||
| #  edited_at              :datetime | ||||
| #  trendable              :boolean | ||||
| # | ||||
|  | ||||
| class Status < ApplicationRecord | ||||
|   | ||||
| @@ -4,41 +4,39 @@ class BlacklistedEmailValidator < ActiveModel::Validator | ||||
|   def validate(user) | ||||
|     return if user.valid_invitation? || user.email.blank? | ||||
|  | ||||
|     @email = user.email | ||||
|  | ||||
|     user.errors.add(:email, :blocked) if blocked_email_provider? | ||||
|     user.errors.add(:email, :taken) if blocked_canonical_email? | ||||
|     user.errors.add(:email, :blocked) if blocked_email_provider?(user.email, user.sign_up_ip) | ||||
|     user.errors.add(:email, :taken) if blocked_canonical_email?(user.email) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def blocked_email_provider? | ||||
|     disallowed_through_email_domain_block? || disallowed_through_configuration? || not_allowed_through_configuration? | ||||
|   def blocked_email_provider?(email, ip) | ||||
|     disallowed_through_email_domain_block?(email, ip) || disallowed_through_configuration?(email) || not_allowed_through_configuration?(email) | ||||
|   end | ||||
|  | ||||
|   def blocked_canonical_email? | ||||
|     CanonicalEmailBlock.block?(@email) | ||||
|   def blocked_canonical_email?(email) | ||||
|     CanonicalEmailBlock.block?(email) | ||||
|   end | ||||
|  | ||||
|   def disallowed_through_email_domain_block? | ||||
|     EmailDomainBlock.block?(@email) | ||||
|   def disallowed_through_email_domain_block?(email, ip) | ||||
|     EmailDomainBlock.block?(email, attempt_ip: ip) | ||||
|   end | ||||
|  | ||||
|   def not_allowed_through_configuration? | ||||
|   def not_allowed_through_configuration?(email) | ||||
|     return false if Rails.configuration.x.email_domains_whitelist.blank? | ||||
|  | ||||
|     domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.') | ||||
|     regexp  = Regexp.new("@(.+\\.)?(#{domains})$", true) | ||||
|  | ||||
|     @email !~ regexp | ||||
|     email !~ regexp | ||||
|   end | ||||
|  | ||||
|   def disallowed_through_configuration? | ||||
|   def disallowed_through_configuration?(email) | ||||
|     return false if Rails.configuration.x.email_domains_blacklist.blank? | ||||
|  | ||||
|     domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.') | ||||
|     regexp  = Regexp.new("@(.+\\.)?(#{domains})", true) | ||||
|  | ||||
|     regexp.match?(@email) | ||||
|     regexp.match?(email) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -11,11 +11,11 @@ class EmailMxValidator < ActiveModel::Validator | ||||
|     if domain.blank? | ||||
|       user.errors.add(:email, :invalid) | ||||
|     elsif !on_allowlist?(domain) | ||||
|       ips, hostnames = resolve_mx(domain) | ||||
|       resolved_ips, resolved_domains = resolve_mx(domain) | ||||
|  | ||||
|       if ips.empty? | ||||
|       if resolved_ips.empty? | ||||
|         user.errors.add(:email, :unreachable) | ||||
|       elsif on_blacklist?(hostnames + ips) | ||||
|       elsif on_blacklist?(resolved_domains, resolved_ips, user.sign_up_ip) | ||||
|         user.errors.add(:email, :blocked) | ||||
|       end | ||||
|     end | ||||
| @@ -40,24 +40,24 @@ class EmailMxValidator < ActiveModel::Validator | ||||
|   end | ||||
|  | ||||
|   def resolve_mx(domain) | ||||
|     hostnames = [] | ||||
|     ips       = [] | ||||
|     records = [] | ||||
|     ips     = [] | ||||
|  | ||||
|     Resolv::DNS.open do |dns| | ||||
|       dns.timeouts = 5 | ||||
|  | ||||
|       hostnames = dns.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } | ||||
|       records = dns.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } | ||||
|  | ||||
|       ([domain] + hostnames).uniq.each do |hostname| | ||||
|       ([domain] + records).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 | ||||
|  | ||||
|     [ips, hostnames] | ||||
|     [ips, records] | ||||
|   end | ||||
|  | ||||
|   def on_blacklist?(values) | ||||
|     EmailDomainBlock.where(domain: values.uniq).any? | ||||
|   def on_blacklist?(domains, resolved_ips, attempt_ip) | ||||
|     EmailDomainBlock.block?(domains, ips: resolved_ips, attempt_ip: attempt_ip) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1,15 +1,14 @@ | ||||
| %tr | ||||
|   %td | ||||
|     %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 | ||||
| .batch-table__row | ||||
|   %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox | ||||
|     = f.check_box :email_domain_block_ids, { multiple: true, include_hidden: false }, email_domain_block.id | ||||
|   .batch-table__row__content.pending-account | ||||
|     .pending-account__header | ||||
|       %samp= link_to email_domain_block.domain, admin_accounts_path(email: "%@#{email_domain_block.domain}") | ||||
|  | ||||
| - 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 | ||||
|       %br/ | ||||
|  | ||||
|       - if email_domain_block.parent.present? | ||||
|         = t('admin.email_domain_blocks.resolved_through_html', domain: content_tag(:samp, email_domain_block.parent.domain)) | ||||
|         • | ||||
|  | ||||
|       = t('admin.email_domain_blocks.attempts_over_week', count: email_domain_block.history.reduce(0) { |sum, day| sum + day.accounts }) | ||||
|   | ||||
| @@ -4,16 +4,22 @@ | ||||
| - content_for :heading_actions do | ||||
|   = link_to t('admin.email_domain_blocks.add_new'), new_admin_email_domain_block_path, class: 'button' | ||||
|  | ||||
| - if @email_domain_blocks.empty? | ||||
|   %div.muted-hint.center-text=t 'admin.email_domain_blocks.empty' | ||||
| - else | ||||
|   .table-wrapper | ||||
|     %table.table | ||||
|       %thead | ||||
|         %tr | ||||
|           %th= t('admin.email_domain_blocks.domain') | ||||
|           %th | ||||
|       %tbody | ||||
|         = render partial: 'email_domain_block', collection: @email_domain_blocks | ||||
| - content_for :header_tags do | ||||
|   = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' | ||||
|  | ||||
| = form_for(@form, url: batch_admin_email_domain_blocks_path) do |f| | ||||
|   = hidden_field_tag :page, params[:page] || 1 | ||||
|  | ||||
|   .batch-table | ||||
|     .batch-table__toolbar | ||||
|       %label.batch-table__toolbar__select.batch-checkbox-all | ||||
|         = check_box_tag :batch_checkbox_all, nil, false | ||||
|       .batch-table__toolbar__actions | ||||
|         = f.button safe_join([fa_icon('times'), t('admin.email_domain_blocks.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } | ||||
|     .batch-table__body | ||||
|       - if @email_domain_blocks.empty? | ||||
|         = nothing_here 'nothing-here--under-tabs' | ||||
|       - else | ||||
|         = render partial: 'email_domain_block', collection: @email_domain_blocks.flat_map { |x| [x, x.children.to_a].flatten }, locals: { f: f } | ||||
|  | ||||
| = paginate @email_domain_blocks | ||||
|   | ||||
| @@ -1,14 +1,38 @@ | ||||
| - content_for :page_title do | ||||
|   = t('.title') | ||||
|  | ||||
| - content_for :header_tags do | ||||
|   = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' | ||||
|  | ||||
| = simple_form_for @email_domain_block, url: admin_email_domain_blocks_path do |f| | ||||
|   = render 'shared/error_messages', object: @email_domain_block | ||||
|  | ||||
|   .fields-group | ||||
|     = f.input :domain, wrapper: :with_block_label, label: t('admin.email_domain_blocks.domain') | ||||
|     = f.input :domain, wrapper: :with_block_label, label: t('admin.email_domain_blocks.domain'), input_html: { readonly: defined?(@resolved_records) } | ||||
|  | ||||
|   .fields-group | ||||
|     = f.input :with_dns_records, as: :boolean, wrapper: :with_label | ||||
|   - if defined?(@resolved_records) | ||||
|     %p.hint= t('admin.email_domain_blocks.resolved_dns_records_hint_html') | ||||
|  | ||||
|     .batch-table | ||||
|       .batch-table__toolbar | ||||
|         %label.batch-table__toolbar__select.batch-checkbox-all | ||||
|           = check_box_tag :batch_checkbox_all, nil, false | ||||
|         .batch-table__toolbar__actions | ||||
|       .batch-table__body | ||||
|         - @resolved_records.each do |record| | ||||
|           .batch-table__row | ||||
|             %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox | ||||
|               = f.input_field :other_domains, as: :boolean, checked_value: record.exchange.to_s, include_hidden: false, multiple: true | ||||
|             .batch-table__row__content.pending-account | ||||
|               .pending-account__header | ||||
|                 %samp= record.exchange.to_s | ||||
|                 %br | ||||
|                 = t('admin.email_domain_blocks.dns.types.mx') | ||||
|  | ||||
|     %hr.spacer/ | ||||
|  | ||||
|   .actions | ||||
|     = f.button :button, t('.create'), type: :submit | ||||
|     - if defined?(@resolved_records) | ||||
|       = f.button :button, t('.create'), type: :submit, name: :save | ||||
|     - else | ||||
|       = f.button :button, t('.resolve'), type: :submit, name: :resolve | ||||
|   | ||||
| @@ -0,0 +1,30 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Scheduler::EmailDomainBlockRefreshScheduler | ||||
|   include Sidekiq::Worker | ||||
|   include Redisable | ||||
|  | ||||
|   sidekiq_options retry: 0 | ||||
|  | ||||
|   def perform | ||||
|     Resolv::DNS.open do |dns| | ||||
|       dns.timeouts = 5 | ||||
|  | ||||
|       EmailDomainBlock.find_each do |email_domain_block| | ||||
|         ips = begin | ||||
|           if ip?(email_domain_block.domain) | ||||
|             [email_domain_block.domain] | ||||
|           else | ||||
|             dns.getresources(email_domain_block.domain, Resolv::DNS::Resource::IN::A).to_a + dns.getresources(email_domain_block.domain, Resolv::DNS::Resource::IN::AAAA).to_a.map { |resource| resource.address.to_s } | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         email_domain_block.update(ips: ips, last_refresh_at: Time.now.utc) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def ip?(str) | ||||
|     str =~ Regexp.union([Resolv::IPv4::Regex, Resolv::IPv6::Regex]) | ||||
|   end | ||||
| end | ||||
| @@ -467,15 +467,22 @@ en: | ||||
|       view: View domain block | ||||
|     email_domain_blocks: | ||||
|       add_new: Add new | ||||
|       attempts_over_week: | ||||
|         one: "%{count} attempt over the last week" | ||||
|         other: "%{count} sign-up attempts over the last week" | ||||
|       created_msg: Successfully blocked e-mail domain | ||||
|       delete: Delete | ||||
|       destroyed_msg: Successfully unblocked e-mail domain | ||||
|       dns: | ||||
|         types: | ||||
|           mx: MX record | ||||
|       domain: Domain | ||||
|       empty: No e-mail domains currently blocked. | ||||
|       from_html: from %{domain} | ||||
|       new: | ||||
|         create: Add domain | ||||
|         resolve: Resolve domain | ||||
|         title: Block new e-mail domain | ||||
|       no_email_domain_block_selected: No e-mail domain blocks were changed as none were selected | ||||
|       resolved_dns_records_hint_html: The domain name resolves to the following MX domains, which are ultimately responsible for accepting e-mail. Blocking an MX domain will block sign-ups from any e-mail address which uses the same MX domain, even if the visible domain name is different. <strong>Be careful not to block major e-mail providers.</strong> | ||||
|       resolved_through_html: Resolved through %{domain} | ||||
|       title: Blocked e-mail domains | ||||
|     follow_recommendations: | ||||
|       description_html: "<strong>Follow recommendations help new users quickly find interesting content</strong>. When a user has not interacted with others enough to form personalized follow recommendations, these accounts are recommended instead. They are re-calculated on a daily basis from a mix of accounts with the highest recent engagements and highest local follower counts for a given language." | ||||
|   | ||||
| @@ -64,7 +64,7 @@ en: | ||||
|       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. | ||||
|         domain: This can be the domain name that shows up in the e-mail address or the MX record it uses. They will be checked upon sign-up. | ||||
|         with_dns_records: An attempt to resolve the given domain's DNS records will be made and the results will also be blocked | ||||
|       featured_tag: | ||||
|         name: 'You might want to use one of these:' | ||||
|   | ||||
| @@ -193,7 +193,12 @@ Rails.application.routes.draw do | ||||
|     resources :domain_allows, only: [:new, :create, :show, :destroy] | ||||
|     resources :domain_blocks, only: [:new, :create, :show, :destroy, :update, :edit] | ||||
|  | ||||
|     resources :email_domain_blocks, only: [:index, :new, :create, :destroy] | ||||
|     resources :email_domain_blocks, only: [:index, :new, :create] do | ||||
|       collection do | ||||
|         post :batch | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     resources :action_logs, only: [:index] | ||||
|     resources :warning_presets, except: [:new] | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,10 @@ | ||||
|     every: '5m' | ||||
|     class: Scheduler::Trends::RefreshScheduler | ||||
|     queue: scheduler | ||||
|   email_domain_block_refresh_scheduler: | ||||
|     every: '1h' | ||||
|     class: Scheduler::EmailDomainBlockRefreshScheduler | ||||
|     queue: scheduler | ||||
|   trends_review_notifications_scheduler: | ||||
|     every: '2h' | ||||
|     class: Scheduler::Trends::ReviewNotificationsScheduler | ||||
|   | ||||
| @@ -0,0 +1,6 @@ | ||||
| class AddIpsToEmailDomainBlocks < ActiveRecord::Migration[6.1] | ||||
|   def change | ||||
|     add_column :email_domain_blocks, :ips, :inet, array: true | ||||
|     add_column :email_domain_blocks, :last_refresh_at, :datetime | ||||
|   end | ||||
| end | ||||
| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
|  | ||||
| ActiveRecord::Schema.define(version: 2022_02_10_153119) do | ||||
| ActiveRecord::Schema.define(version: 2022_02_24_010024) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
| @@ -387,6 +387,9 @@ ActiveRecord::Schema.define(version: 2022_02_10_153119) do | ||||
|     t.datetime "created_at", null: false | ||||
|     t.datetime "updated_at", null: false | ||||
|     t.bigint "parent_id" | ||||
|     t.inet "ips", array: true | ||||
|     t.datetime "last_refresh_at" | ||||
|  | ||||
|     t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -17,43 +17,43 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do | ||||
|       EmailDomainBlock.paginates_per default_per_page | ||||
|     end | ||||
|  | ||||
|     it 'renders email blacks' do | ||||
|     it 'returns http success' do | ||||
|       2.times { Fabricate(:email_domain_block) } | ||||
|  | ||||
|       get :index, params: { page: 2 } | ||||
|  | ||||
|       assigned = assigns(:email_domain_blocks) | ||||
|       expect(assigned.count).to eq 1 | ||||
|       expect(assigned.klass).to be EmailDomainBlock | ||||
|       expect(response).to have_http_status(200) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET #new' do | ||||
|     it 'assigns a new email black' do | ||||
|     it 'returns http success' do | ||||
|       get :new | ||||
|  | ||||
|       expect(assigns(:email_domain_block)).to be_instance_of(EmailDomainBlock) | ||||
|       expect(response).to have_http_status(200) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST #create' do | ||||
|     it 'blocks the domain when succeeded to save' do | ||||
|       post :create, params: { email_domain_block: { domain: 'example.com' } } | ||||
|     context 'when resolve button is pressed' do | ||||
|       before do | ||||
|         post :create, params: { email_domain_block: { domain: 'example.com' } } | ||||
|       end | ||||
|  | ||||
|       expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.created_msg') | ||||
|       expect(response).to redirect_to(admin_email_domain_blocks_path) | ||||
|       it 'renders new template' do | ||||
|         expect(response).to render_template(:new) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'DELETE #destroy' do | ||||
|     it 'unblocks the domain' do | ||||
|       email_domain_block = Fabricate(:email_domain_block) | ||||
|       delete :destroy, params: { id: email_domain_block.id } | ||||
|     context 'when save button is pressed' do | ||||
|       before do | ||||
|         post :create, params: { email_domain_block: { domain: 'example.com' }, save: '' } | ||||
|       end | ||||
|  | ||||
|       expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.destroyed_msg') | ||||
|       expect(response).to redirect_to(admin_email_domain_blocks_path) | ||||
|       it 'blocks the domain' do | ||||
|         expect(EmailDomainBlock.find_by(domain: 'example.com')).to_not be_nil | ||||
|       end | ||||
|  | ||||
|       it 'redirects to e-mail domain blocks' do | ||||
|         expect(response).to redirect_to(admin_email_domain_blocks_path) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -9,14 +9,29 @@ RSpec.describe EmailDomainBlock, type: :model do | ||||
|   end | ||||
|  | ||||
|   describe 'block?' do | ||||
|     it 'returns true if the domain is registed' do | ||||
|       Fabricate(:email_domain_block, domain: 'example.com') | ||||
|       expect(EmailDomainBlock.block?('nyarn@example.com')).to eq true | ||||
|     let(:input) { nil } | ||||
|  | ||||
|     context 'given an e-mail address' do | ||||
|       let(:input) { 'nyarn@example.com' } | ||||
|  | ||||
|       it 'returns true if the domain is blocked' do | ||||
|         Fabricate(:email_domain_block, domain: 'example.com') | ||||
|         expect(EmailDomainBlock.block?(input)).to be true | ||||
|       end | ||||
|  | ||||
|       it 'returns false if the domain is not blocked' do | ||||
|         Fabricate(:email_domain_block, domain: 'other-example.com') | ||||
|         expect(EmailDomainBlock.block?(input)).to be false | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     it 'returns true if the domain is not registed' do | ||||
|       Fabricate(:email_domain_block, domain: 'example.com') | ||||
|       expect(EmailDomainBlock.block?('nyarn@example.net')).to eq false | ||||
|     context 'given an array of domains' do | ||||
|       let(:input) { %w(foo.com mail.foo.com) } | ||||
|  | ||||
|       it 'returns true if the domain is blocked' do | ||||
|         Fabricate(:email_domain_block, domain: 'mail.foo.com') | ||||
|         expect(EmailDomainBlock.block?(input)).to be true | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -4,7 +4,7 @@ require 'rails_helper' | ||||
|  | ||||
| RSpec.describe BlacklistedEmailValidator, type: :validator do | ||||
|   describe '#validate' do | ||||
|     let(:user)   { double(email: 'info@mail.com', errors: errors) } | ||||
|     let(:user)   { double(email: 'info@mail.com', sign_up_ip: '1.2.3.4', errors: errors) } | ||||
|     let(:errors) { double(add: nil) } | ||||
|  | ||||
|     before do | ||||
|   | ||||
| @@ -4,24 +4,28 @@ require 'rails_helper' | ||||
|  | ||||
| describe EmailMxValidator do | ||||
|   describe '#validate' do | ||||
|     let(:user) { double(email: 'foo@example.com', errors: double(add: nil)) } | ||||
|     let(:user) { double(email: 'foo@example.com', sign_up_ip: '1.2.3.4', errors: double(add: nil)) } | ||||
|  | ||||
|     it 'does not add errors if there are no DNS records for an e-mail domain that is explicitly allowed' do | ||||
|       old_whitelist = Rails.configuration.x.email_domains_whitelist | ||||
|       Rails.configuration.x.email_domains_whitelist = 'example.com' | ||||
|     context 'for an e-mail domain that is explicitly allowed' do | ||||
|       around do |block| | ||||
|         tmp = Rails.configuration.x.email_domains_whitelist | ||||
|         Rails.configuration.x.email_domains_whitelist = 'example.com' | ||||
|         block.call | ||||
|         Rails.configuration.x.email_domains_whitelist = tmp | ||||
|       end | ||||
|  | ||||
|       resolver = double | ||||
|       it 'does not add errors if there are no DNS records' do | ||||
|         resolver = double | ||||
|  | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) | ||||
|       allow(resolver).to receive(:timeouts=).and_return(nil) | ||||
|       allow(Resolv::DNS).to receive(:open).and_yield(resolver) | ||||
|         allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) | ||||
|         allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) | ||||
|         allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) | ||||
|         allow(resolver).to receive(:timeouts=).and_return(nil) | ||||
|         allow(Resolv::DNS).to receive(:open).and_yield(resolver) | ||||
|  | ||||
|       subject.validate(user) | ||||
|       expect(user.errors).to_not have_received(:add) | ||||
|  | ||||
|       Rails.configuration.x.email_domains_whitelist = old_whitelist | ||||
|         subject.validate(user) | ||||
|         expect(user.errors).to_not have_received(:add) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     it 'adds an error if there are no DNS records for the e-mail domain' do | ||||
| @@ -37,7 +41,7 @@ describe EmailMxValidator do | ||||
|       expect(user.errors).to have_received(:add) | ||||
|     end | ||||
|  | ||||
|     it 'adds an error if a MX record exists but does not lead to an IP' do | ||||
|     it 'adds an error if a MX record does not lead to an IP' do | ||||
|       resolver = double | ||||
|  | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) | ||||
| @@ -53,7 +57,7 @@ describe EmailMxValidator do | ||||
|     end | ||||
|  | ||||
|     it 'adds an error if the A record is blacklisted' do | ||||
|       EmailDomainBlock.create!(domain: '1.2.3.4') | ||||
|       EmailDomainBlock.create!(domain: 'alternate-example.com', ips: ['1.2.3.4']) | ||||
|       resolver = double | ||||
|  | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) | ||||
| @@ -67,7 +71,7 @@ describe EmailMxValidator do | ||||
|     end | ||||
|  | ||||
|     it 'adds an error if the AAAA record is blacklisted' do | ||||
|       EmailDomainBlock.create!(domain: 'fd00::1') | ||||
|       EmailDomainBlock.create!(domain: 'alternate-example.com', ips: ['fd00::1']) | ||||
|       resolver = double | ||||
|  | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) | ||||
| @@ -80,8 +84,8 @@ describe EmailMxValidator do | ||||
|       expect(user.errors).to have_received(:add) | ||||
|     end | ||||
|  | ||||
|     it 'adds an error if the MX record is blacklisted' do | ||||
|       EmailDomainBlock.create!(domain: '2.3.4.5') | ||||
|     it 'adds an error if the A record of the MX record is blacklisted' do | ||||
|       EmailDomainBlock.create!(domain: 'mail.other-domain.com', ips: ['2.3.4.5']) | ||||
|       resolver = double | ||||
|  | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) | ||||
| @@ -96,8 +100,8 @@ describe EmailMxValidator do | ||||
|       expect(user.errors).to have_received(:add) | ||||
|     end | ||||
|  | ||||
|     it 'adds an error if the MX IPv6 record is blacklisted' do | ||||
|       EmailDomainBlock.create!(domain: 'fd00::2') | ||||
|     it 'adds an error if the AAAA record of the MX record is blacklisted' do | ||||
|       EmailDomainBlock.create!(domain: 'mail.other-domain.com', ips: ['fd00::2']) | ||||
|       resolver = double | ||||
|  | ||||
|       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) | ||||
| @@ -112,7 +116,7 @@ describe EmailMxValidator do | ||||
|       expect(user.errors).to have_received(:add) | ||||
|     end | ||||
|  | ||||
|     it 'adds an error if the MX hostname is blacklisted' do | ||||
|     it 'adds an error if the MX record is blacklisted' do | ||||
|       EmailDomainBlock.create!(domain: 'mail.example.com') | ||||
|       resolver = double | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user