mirror of
https://github.com/discourse/discourse.git
synced 2024-12-24 06:23:45 +08:00
183 lines
5.6 KiB
Ruby
183 lines
5.6 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, if: :will_save_change_to_ip_address?
|
|
|
|
ROLLED_UP_BLOCKS = [
|
|
# IPv4
|
|
[4, 32, 24],
|
|
# IPv6
|
|
[6, (65..128).to_a, 64],
|
|
[6, 64, 60],
|
|
[6, 60, 56],
|
|
[6, 56, 52],
|
|
[6, 52, 48],
|
|
]
|
|
|
|
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
|
|
if self.errors[:ip_address].blank?
|
|
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
|
|
ip_address = IPAddr === ip_address ? ip_address.to_cidr_s : ip_address.to_s
|
|
order("masklen(ip_address) DESC").find_by("? <<= ip_address", ip_address)
|
|
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.subnets(family, from_masklen, to_masklen)
|
|
sql = <<~SQL
|
|
WITH ips_and_subnets AS (
|
|
SELECT ip_address,
|
|
network(inet(host(ip_address) || '/' || :to_masklen))::text subnet
|
|
FROM screened_ip_addresses
|
|
WHERE family(ip_address) = :family AND
|
|
masklen(ip_address) IN (:from_masklen) AND
|
|
action_type = :blocked
|
|
)
|
|
SELECT subnet
|
|
FROM ips_and_subnets
|
|
GROUP BY subnet
|
|
HAVING COUNT(*) >= :min_ban_entries_for_roll_up
|
|
SQL
|
|
|
|
DB.query_single(
|
|
sql,
|
|
family: family,
|
|
from_masklen: from_masklen,
|
|
to_masklen: to_masklen,
|
|
blocked: ScreenedIpAddress.actions[:block],
|
|
min_ban_entries_for_roll_up: SiteSetting.min_ban_entries_for_roll_up,
|
|
)
|
|
end
|
|
|
|
def self.roll_up(current_user = Discourse.system_user)
|
|
ROLLED_UP_BLOCKS.each do |family, from_masklen, to_masklen|
|
|
ScreenedIpAddress
|
|
.subnets(family, from_masklen, to_masklen)
|
|
.map do |subnet|
|
|
next if ScreenedIpAddress.where("? <<= ip_address", subnet).exists?
|
|
|
|
old_ips =
|
|
ScreenedIpAddress
|
|
.where(action_type: ScreenedIpAddress.actions[:block])
|
|
.where("ip_address << ?", subnet)
|
|
.where("family(ip_address) = ?", family)
|
|
.where("masklen(ip_address) IN (?)", from_masklen)
|
|
|
|
sum_match_count, max_last_match_at, min_created_at =
|
|
old_ips.pick("SUM(match_count), MAX(last_match_at), MIN(created_at)")
|
|
|
|
ScreenedIpAddress.create!(
|
|
ip_address: subnet,
|
|
match_count: sum_match_count,
|
|
last_match_at: max_last_match_at,
|
|
created_at: min_created_at,
|
|
)
|
|
|
|
StaffActionLogger.new(current_user).log_roll_up(subnet, old_ips.map(&:ip_address))
|
|
old_ips.delete_all
|
|
end
|
|
end
|
|
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)
|
|
#
|