about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2020-10-12 16:33:49 +0200
committerGitHub <noreply@github.com>2020-10-12 16:33:49 +0200
commit5e1364c448222c964faa469b6b5bfe9adf701c1a (patch)
treebf13de38f07f6a8ec4bdce9c6242c3c472bfddea /app/models
parentdc52a778e111a67a5275dd4afecf3991e279e005 (diff)
Add IP-based rules (#14963)
Diffstat (limited to 'app/models')
-rw-r--r--app/models/concerns/expireable.rb10
-rw-r--r--app/models/form/ip_block_batch.rb31
-rw-r--r--app/models/ip_block.rb41
-rw-r--r--app/models/user.rb16
4 files changed, 95 insertions, 3 deletions
diff --git a/app/models/concerns/expireable.rb b/app/models/concerns/expireable.rb
index f7d2bab49..a66a4661b 100644
--- a/app/models/concerns/expireable.rb
+++ b/app/models/concerns/expireable.rb
@@ -6,7 +6,15 @@ module Expireable
   included do
     scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
 
-    attr_reader :expires_in
+    def expires_in
+      return @expires_in if defined?(@expires_in)
+
+      if expires_at.nil?
+        nil
+      else
+        (expires_at - created_at).to_i
+      end
+    end
 
     def expires_in=(interval)
       self.expires_at = interval.to_i.seconds.from_now if interval.present?
diff --git a/app/models/form/ip_block_batch.rb b/app/models/form/ip_block_batch.rb
new file mode 100644
index 000000000..f6fe9b593
--- /dev/null
+++ b/app/models/form/ip_block_batch.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class Form::IpBlockBatch
+  include ActiveModel::Model
+  include Authorization
+  include AccountableConcern
+
+  attr_accessor :ip_block_ids, :action, :current_account
+
+  def save
+    case action
+    when 'delete'
+      delete!
+    end
+  end
+
+  private
+
+  def ip_blocks
+    @ip_blocks ||= IpBlock.where(id: ip_block_ids)
+  end
+
+  def delete!
+    ip_blocks.each { |ip_block| authorize(ip_block, :destroy?) }
+
+    ip_blocks.each do |ip_block|
+      ip_block.destroy
+      log_action :destroy, ip_block
+    end
+  end
+end
diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb
new file mode 100644
index 000000000..aedd3ca0d
--- /dev/null
+++ b/app/models/ip_block.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: ip_blocks
+#
+#  id         :bigint(8)        not null, primary key
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#  expires_at :datetime
+#  ip         :inet             default(#<IPAddr: IPv4:0.0.0.0/255.255.255.255>), not null
+#  severity   :integer          default(NULL), not null
+#  comment    :text             default(""), not null
+#
+
+class IpBlock < ApplicationRecord
+  CACHE_KEY = 'blocked_ips'
+
+  include Expireable
+
+  enum severity: {
+    sign_up_requires_approval: 5000,
+    no_access: 9999,
+  }
+
+  validates :ip, :severity, presence: true
+
+  after_commit :reset_cache
+
+  class << self
+    def blocked?(remote_ip)
+      blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
+      blocked_ips_map.include?(remote_ip)
+    end
+  end
+
+  private
+
+  def reset_cache
+    Rails.cache.delete(CACHE_KEY)
+  end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 2c460e1fd..7c8124fed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -41,6 +41,7 @@
 #  sign_in_token             :string
 #  sign_in_token_sent_at     :datetime
 #  webauthn_id               :string
+#  sign_up_ip                :inet
 #
 
 class User < ApplicationRecord
@@ -97,7 +98,7 @@ class User < ApplicationRecord
   scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
   scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) }
   scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
-  scope :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.last_sign_in_ip <<= ?', value)).or(left_joins(:session_activations).where('session_activations.ip <<= ?', value)) }
+  scope :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.sign_up_ip <<= ?', value)).or(left_joins(:session_activations).where('users.last_sign_in_ip <<= ?', value)).or(left_joins(:session_activations).where('session_activations.ip <<= ?', value)) }
   scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
 
   before_validation :sanitize_languages
@@ -331,6 +332,7 @@ class User < ApplicationRecord
 
       arr << [current_sign_in_at, current_sign_in_ip] if current_sign_in_ip.present?
       arr << [last_sign_in_at, last_sign_in_ip] if last_sign_in_ip.present?
+      arr << [created_at, sign_up_ip] if sign_up_ip.present?
 
       arr.sort_by { |pair| pair.first || Time.now.utc }.uniq(&:last).reverse!
     end
@@ -385,7 +387,17 @@ class User < ApplicationRecord
   end
 
   def set_approved
-    self.approved = open_registrations? || valid_invitation? || external?
+    self.approved = begin
+      if sign_up_from_ip_requires_approval?
+        false
+      else
+        open_registrations? || valid_invitation? || external?
+      end
+    end
+  end
+
+  def sign_up_from_ip_requires_approval?
+    !sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists?
   end
 
   def open_registrations?