discourse/app/models/screened_ip_address.rb
Sam 5f64fd0a21 DEV: remove exec_sql and replace with mini_sql
Introduce new patterns for direct sql that are safe and fast.

MiniSql is not prone to memory bloat that can happen with direct PG usage.
It also has an extremely fast materializer and very a convenient API

- DB.exec(sql, *params) => runs sql returns row count
- DB.query(sql, *params) => runs sql returns usable objects (not a hash)
- DB.query_hash(sql, *params) => runs sql returns an array of hashes
- DB.query_single(sql, *params) => runs sql and returns a flat one dimensional array
- DB.build(sql) => returns a sql builder

See more at: https://github.com/discourse/mini_sql
2018-06-19 16:13:36 +10:00

193 lines
6.0 KiB
Ruby

require_dependency 'screening_model'
require_dependency 'ip_addr'
# 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
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_whitelisted?(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_whitelist
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)
#