ip bans: add hit counter, deleted flag, new ban type.

* Make IP bans soft deletable.
* Add a hit counter to track how many times an IP ban has blocked someone.
* Add a last hit timestamp to track when the IP ban last blocked someone.
* Add a new type of IP ban, the signup ban. Signup bans restrict new
  signups from editing anything until they've verified their email
  address.
This commit is contained in:
evazion
2020-04-06 14:12:56 -05:00
parent 98e84d83fb
commit b2ee1f0766
18 changed files with 178 additions and 40 deletions

View File

@@ -163,7 +163,7 @@ class ApplicationController < ActionController::Base
end
def ip_ban_check
raise User::PrivilegeError if !request.get? && IpBan.is_banned?(CurrentUser.ip_addr)
raise User::PrivilegeError if !request.get? && IpBan.hit!(:normal, CurrentUser.ip_addr)
end
def pundit_user

View File

@@ -19,9 +19,10 @@ class IpBansController < ApplicationController
respond_with(@ip_bans)
end
def destroy
def update
@ip_ban = authorize IpBan.find(params[:id])
@ip_ban.destroy
@ip_ban.update(permitted_attributes(@ip_ban))
respond_with(@ip_ban)
end
end

View File

@@ -59,9 +59,11 @@ class UsersController < ApplicationController
end
def create
requires_verification = IpLookup.new(CurrentUser.ip_addr).is_proxy? || IpBan.hit!(:signup, CurrentUser.ip_addr)
@user = authorize User.new(
last_ip_addr: CurrentUser.ip_addr,
requires_verification: IpLookup.new(CurrentUser.ip_addr).is_proxy?,
requires_verification: requires_verification,
name: params[:user][:name],
password: params[:user][:password],
password_confirmation: params[:user][:password_confirmation]

View File

@@ -122,7 +122,9 @@ module ApplicationHelper
end
def time_ago_in_words_tagged(time, compact: false)
if time.past?
if time.nil?
tag.em(tag.time("unknown"))
elsif time.past?
if compact
text = time_ago_in_words(time)
text = text.gsub(/almost|about|over/, "").strip

View File

@@ -1,13 +1,27 @@
class IpBan < ApplicationRecord
belongs_to :creator, class_name: "User"
validate :validate_ip_addr
validates_presence_of :reason
validates_uniqueness_of :ip_addr
after_create { ModAction.log("#{creator.name} created ip ban for #{ip_addr}", :ip_ban_create) }
after_destroy { ModAction.log("#{creator.name} deleted ip ban for #{ip_addr}", :ip_ban_delete) }
def self.is_banned?(ip_addr)
where("ip_addr >>= ?", ip_addr).exists?
validate :validate_ip_addr
validates :reason, presence: true
before_save :create_mod_action
deletable
enum category: {
normal: 0,
signup: 100
}, _suffix: "ban"
def self.ip_matches(ip_addr)
where("ip_addr >>= ?", ip_addr)
end
def self.hit!(category, ip_addr)
ip_ban = active.where(category: category).ip_matches(ip_addr).first
return false unless ip_ban
IpBan.increment_counter(:hit_count, ip_ban.id, touch: [:last_hit_at])
true
end
def self.search(params)
@@ -21,15 +35,31 @@ class IpBan < ApplicationRecord
q.apply_default_order(params)
end
def create_mod_action
if new_record?
ModAction.log("#{creator.name} created ip ban for #{ip_addr}", :ip_ban_create)
elsif is_deleted? == true && is_deleted_was == false
ModAction.log("#{creator.name} deleted ip ban for #{ip_addr}", :ip_ban_delete)
elsif is_deleted? == false && is_deleted_was == true
ModAction.log("#{creator.name} undeleted ip ban for #{ip_addr}", :ip_ban_undelete)
end
end
def validate_ip_addr
if ip_addr.blank?
errors[:ip_addr] << "is invalid"
elsif ip_addr.ipv4? && ip_addr.prefix < 24
errors[:ip_addr] << "may not have a subnet bigger than /24"
elsif ip_addr.ipv6? && ip_addr.prefix < 64
errors[:ip_addr] << "may not have a subnet bigger than /64"
elsif ip_addr.private? || ip_addr.loopback? || ip_addr.link_local?
errors[:ip_addr] << "must be a public address"
elsif normal_ban? && ip_addr.ipv4? && ip_addr.prefix < 24
errors[:ip_addr] << "may not have a subnet bigger than /24"
elsif signup_ban? && ip_addr.ipv4? && ip_addr.prefix < 8
errors[:ip_addr] << "may not have a subnet bigger than /8"
elsif normal_ban? && ip_addr.ipv6? && ip_addr.prefix < 64
errors[:ip_addr] << "may not have a subnet bigger than /64"
elsif signup_ban? && ip_addr.ipv6? && ip_addr.prefix < 20
errors[:ip_addr] << "may not have a subnet bigger than /20"
elsif new_record? && IpBan.active.ip_matches(subnetted_ip).exists?
errors[:ip_addr] << "is already banned"
end
end

View File

@@ -48,6 +48,7 @@ class ModAction < ApplicationRecord
tag_implication_update: 141,
ip_ban_create: 160,
ip_ban_delete: 162,
ip_ban_undelete: 163,
mass_update: 1000,
bulk_revert: 1001, # XXX unused
other: 2000

View File

@@ -7,11 +7,11 @@ class IpBanPolicy < ApplicationPolicy
user.is_moderator?
end
def destroy?
def update?
user.is_moderator?
end
def permitted_attributes
[:ip_addr, :reason]
[:ip_addr, :reason, :is_deleted, :category]
end
end

View File

@@ -26,7 +26,7 @@
<tr>
<th>IP Banned</th>
<td>
<% if IpBan.is_banned?(ip_address.ip_addr.to_s) %>
<% if IpBan.ip_matches(ip_address.ip_addr.to_s).exists? %>
yes (<%= link_to "info", ip_bans_path(search: { ip_addr: ip_address.to_s }) %>)
<% else %>
no

View File

@@ -2,16 +2,35 @@
<div id="a-index">
<h1>IP Bans</h1>
<%= table_for @ip_bans, width: "100%" do |t| %>
<%= table_for @ip_bans, class: "striped autofit", width: "100%" do |t| %>
<% t.column "IP Address" do |ip_ban| %>
<%= link_to_ip ip_ban.subnetted_ip %>
<% end %>
<% t.column "Banner" do |ip_ban| %>
<%= link_to_user ip_ban.creator %>
<% t.column :reason, td: { class: "col-expand" } %>
<% t.column "Status" do |ip_ban| %>
<% if ip_ban.is_deleted? %>
Deleted
<% end %>
<% end %>
<% t.column "Type" do |ip_ban| %>
<%= ip_ban.category.delete_suffix("_ban").capitalize %>
<% end %>
<% t.column "Last Seen" do |ip_ban| %>
<%= time_ago_in_words_tagged ip_ban.last_hit_at %>
<% end %>
<% t.column :hit_count, name: "Hits" %>
<% t.column "Creator" do |ip_ban| %>
<%= link_to_user ip_ban.creator %>
<%= link_to "»", ip_bans_path(search: { creator_name: ip_ban.creator.name }) %>
<div><%= time_ago_in_words_tagged(ip_ban.created_at) %></div>
<% end %>
<% t.column :reason %>
<% t.column column: "control" do |ip_ban| %>
<%= link_to "Unban", ip_ban_path(ip_ban), :remote => true, :method => :delete, :data => {:confirm => "Do your really want to unban #{ip_ban.ip_addr}?"} %>
<%= link_to "Details", ip_address_path(ip_ban.ip_addr.to_s) %> |
<% if ip_ban.is_deleted? %>
<%= link_to "Undelete", ip_ban_path(ip_ban), remote: true, method: :put, "data-params": "ip_ban[is_deleted]=false", "data-confirm": "Are you sure you want to undelete this IP ban?" %>
<% else %>
<%= link_to "Delete", ip_ban_path(ip_ban), remote: true, method: :put, "data-params": "ip_ban[is_deleted]=true", "data-confirm": "Are you sure you want to remove this IP ban?" %>
<% end %>
<% end %>
<% end %>

View File

@@ -1,12 +1,23 @@
<div id="c-ip-bans">
<div id="a-new">
<div id="a-new" class="fixed-width-container">
<h1>New IP Ban</h1>
<p>
A normal IP ban restricts the IP from creating new accounts, logging in to
existing accounts, or editing the site in any way.
</p>
<p>
A signup IP ban restricts new signups from editing anything until after
they've verified their email address.
<p>
<%= error_messages_for "ip_ban" %>
<%= edit_form_for(@ip_ban) do |f| %>
<%= f.input :ip_addr, label: "IP Address", as: :string, hint: "Add /24 to ban a subnet. Example: 1.2.3.4/24" %>
<%= f.input :reason %>
<%= f.input :reason, as: :string %>
<%= f.input :category, as: :select, include_blank: false, collection: [["Normal", "normal"], ["Signup", "signup"]] %>
<%= f.button :submit, "Submit" %>
<% end %>
</div>