discourse/app/models/screened_ip_address.rb

194 lines
6.0 KiB
Ruby

# frozen_string_literal: true
require 'screening_model'
# A ScreenedIpAddress record represents an IP address or subnet that is being watched,
# and possibly blocked from creating accounts.
class ScreenedIpAddress < ActiveRecord::Base
include ScreeningModel
default_action :block
validates :ip_address, ip_address_format: true, presence: true
after_validation :check_for_match
def self.watch(ip_address, opts = {})
match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address))
end
def check_for_match
unless self.errors[:ip_address].present?
matched = self.class.match_for_ip_address(self.ip_address)
if matched && matched.action_type == self.action_type
self.errors.add(:ip_address, :ip_address_already_screened)
end
end
end
# In Rails 4.0.0, validators are run to handle invalid assignments to inet columns (as they should).
# In Rails 4.0.1, an exception is raised before validation happens, so we need this hack for
# inet/cidr columns:
def ip_address=(val)
if val.nil?
self.errors.add(:ip_address, :invalid)
return
end
if val.is_a?(IPAddr)
write_attribute(:ip_address, val)
return
end
v = IPAddr.handle_wildcards(val)
if v.nil?
self.errors.add(:ip_address, :invalid)
return
end
write_attribute(:ip_address, v)
# this gets even messier, Ruby 1.9.2 raised a different exception to Ruby 2.0.0
# handle both exceptions
rescue ArgumentError, IPAddr::InvalidAddressError
self.errors.add(:ip_address, :invalid)
end
# Return a string with the ip address and mask in standard format. e.g., "127.0.0.0/8".
def ip_address_with_mask
ip_address.try(:to_cidr_s)
end
def self.match_for_ip_address(ip_address)
# The <<= operator on inet columns means "is contained within or equal to".
#
# Read more about PostgreSQL's inet data type here:
#
# http://www.postgresql.org/docs/9.1/static/datatype-net-types.html
# http://www.postgresql.org/docs/9.1/static/functions-net.html
order('masklen(ip_address) DESC').find_by("? <<= ip_address", ip_address.to_s)
end
def self.should_block?(ip_address)
exists_for_ip_address_and_action?(ip_address, actions[:block])
end
def self.is_allowed?(ip_address)
exists_for_ip_address_and_action?(ip_address, actions[:do_nothing])
end
def self.exists_for_ip_address_and_action?(ip_address, action_type, opts = {})
b = match_for_ip_address(ip_address)
found = !!b && b.action_type == action_type
b.record_match! if found && opts[:record_match] != false
found
end
def self.block_admin_login?(user, ip_address)
return false unless SiteSetting.use_admin_ip_allowlist
return false if user.nil?
return false if !user.admin?
return false if ScreenedIpAddress.where(action_type: actions[:allow_admin]).count == 0
return true if ip_address.nil?
!exists_for_ip_address_and_action?(ip_address, actions[:allow_admin], record_match: false)
end
def self.star_subnets_query
@star_subnets_query ||= <<~SQL
SELECT network(inet(host(ip_address) || '/24'))::text AS ip_range
FROM screened_ip_addresses
WHERE action_type = #{ScreenedIpAddress.actions[:block]}
AND family(ip_address) = 4
AND masklen(ip_address) = 32
GROUP BY ip_range
HAVING COUNT(*) >= :min_count
SQL
end
def self.star_star_subnets_query
@star_star_subnets_query ||= <<~SQL
WITH weighted_subnets AS (
SELECT network(inet(host(ip_address) || '/16'))::text AS ip_range,
CASE masklen(ip_address)
WHEN 32 THEN 1
WHEN 24 THEN :roll_up_weight
ELSE 0
END AS weight
FROM screened_ip_addresses
WHERE action_type = #{ScreenedIpAddress.actions[:block]}
AND family(ip_address) = 4
)
SELECT ip_range
FROM weighted_subnets
GROUP BY ip_range
HAVING SUM(weight) >= :min_count
SQL
end
def self.star_subnets
min_count = SiteSetting.min_ban_entries_for_roll_up
DB.query_single(star_subnets_query, min_count: min_count)
end
def self.star_star_subnets
weight = SiteSetting.min_ban_entries_for_roll_up
DB.query_single(star_star_subnets_query, min_count: 10, roll_up_weight: weight)
end
def self.roll_up(current_user = Discourse.system_user)
subnets = [star_subnets, star_star_subnets].flatten
StaffActionLogger.new(current_user).log_roll_up(subnets) unless subnets.blank?
subnets.each do |subnet|
ScreenedIpAddress.create(ip_address: subnet) unless ScreenedIpAddress.where("? <<= ip_address", subnet).exists?
sql = <<~SQL
UPDATE screened_ip_addresses
SET match_count = sum_match_count
, created_at = min_created_at
, last_match_at = max_last_match_at
FROM (
SELECT SUM(match_count) AS sum_match_count
, MIN(created_at) AS min_created_at
, MAX(last_match_at) AS max_last_match_at
FROM screened_ip_addresses
WHERE action_type = #{ScreenedIpAddress.actions[:block]}
AND family(ip_address) = 4
AND ip_address << :ip_address
) s
WHERE ip_address = :ip_address
SQL
DB.exec(sql, ip_address: subnet)
ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block])
.where("family(ip_address) = 4")
.where("ip_address << ?", subnet)
.delete_all
end
subnets
end
end
# == Schema Information
#
# Table name: screened_ip_addresses
#
# id :integer not null, primary key
# ip_address :inet not null
# action_type :integer not null
# match_count :integer default(0), not null
# last_match_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_screened_ip_addresses_on_ip_address (ip_address) UNIQUE
# index_screened_ip_addresses_on_last_match_at (last_match_at)
#