api: make IP addresses in the API.

Make the following fields visible in API responses:

* ip_bans.ip_addr
* ip_geolocations.ip_addr
* ip_geolocations.network
* users.last_ip_addr (mod only)
* user_sessions.ip_addr
* api_keys.last_ip_address
* api_keys.permitted_ip_addresses

Before IP addresses were globally hidden in API responses because IPs were
present in a lot of tables and we didn't want to accidentally leak them.
Now that we've gotten rid of IPs from most tables, it's safe to unhide them.
This commit is contained in:
evazion
2022-09-24 00:09:36 -05:00
parent 7bf824f0dd
commit adba70a0de
9 changed files with 34 additions and 32 deletions

View File

@@ -6,11 +6,11 @@
module Danbooru module Danbooru
class IpAddress class IpAddress
attr_reader :ip_address attr_reader :ip_address
delegate :ipv4?, :ipv6?, :loopback?, :link_local?, :unique_local?, :private?, :to_string, :prefix, :multicast?, :unspecified?, to: :ip_address delegate :ipv4?, :ipv6?, :loopback?, :link_local?, :unique_local?, :private?, :to_string, :network, :prefix, :multicast?, :unspecified?, to: :ip_address
delegate :ip_info, :is_proxy?, to: :ip_lookup delegate :ip_info, :is_proxy?, to: :ip_lookup
def initialize(string) def initialize(string)
@ip_address = ::IPAddress.parse(string.to_s) @ip_address = ::IPAddress.parse(string.to_s.strip)
end end
def ip_lookup def ip_lookup
@@ -39,9 +39,13 @@ module Danbooru
ip_address.include?(other.ip_address) ip_address.include?(other.ip_address)
end end
def as_json
to_s
end
# "1.2.3.4/24" if the address is a subnet, "1.2.3.4" otherwise. # "1.2.3.4/24" if the address is a subnet, "1.2.3.4" otherwise.
def to_s def to_s
ip_address.size > 1 ? ip_address.to_string : ip_address.to_s ip_address.size > 1 ? "#{network}/#{prefix}" : ip_address.to_s
end end
def inspect def inspect

View File

@@ -18,6 +18,8 @@ class IpAddressType < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Inet
def cast(value) def cast(value)
return nil if value.blank? return nil if value.blank?
super(Danbooru::IpAddress.new(value)) super(Danbooru::IpAddress.new(value))
rescue ArgumentError
nil
end end
# Serialize a Danbooru::IpAddress to a String for the database. # Serialize a Danbooru::IpAddress to a String for the database.

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class IpBan < ApplicationRecord class IpBan < ApplicationRecord
attribute :ip_addr, :ip_address
belongs_to :creator, class_name: "User" belongs_to :creator, class_name: "User"
validate :validate_ip_addr validate :validate_ip_addr
@@ -23,7 +25,7 @@ class IpBan < ApplicationRecord
end end
def self.ip_matches(ip_addr) def self.ip_matches(ip_addr)
where("ip_addr >>= ?", ip_addr) where("ip_addr >>= ?", ip_addr.to_s)
end end
def self.hit!(category, ip_addr) def self.hit!(category, ip_addr)
@@ -62,7 +64,7 @@ class IpBan < ApplicationRecord
def validate_ip_addr def validate_ip_addr
if ip_addr.blank? if ip_addr.blank?
errors.add(:ip_addr, "is invalid") errors.add(:ip_addr, "is invalid")
elsif ip_addr.private? || ip_addr.loopback? || ip_addr.link_local? elsif ip_addr.is_local?
errors.add(:ip_addr, "must be a public address") errors.add(:ip_addr, "must be a public address")
elsif full_ban? && ip_addr.ipv4? && ip_addr.prefix < 24 elsif full_ban? && ip_addr.ipv4? && ip_addr.prefix < 24
errors.add(:ip_addr, "may not have a subnet bigger than /24") errors.add(:ip_addr, "may not have a subnet bigger than /24")
@@ -72,25 +74,11 @@ class IpBan < ApplicationRecord
errors.add(:ip_addr, "may not have a subnet bigger than /48") errors.add(:ip_addr, "may not have a subnet bigger than /48")
elsif partial_ban? && ip_addr.ipv6? && ip_addr.prefix < 20 elsif partial_ban? && ip_addr.ipv6? && ip_addr.prefix < 20
errors.add(:ip_addr, "may not have a subnet bigger than /20") errors.add(:ip_addr, "may not have a subnet bigger than /20")
elsif new_record? && IpBan.active.where(category: category).ip_matches(subnetted_ip).exists? elsif new_record? && IpBan.active.where(category: category).ip_matches(ip_addr).exists?
errors.add(:ip_addr, "is already banned") errors.add(:ip_addr, "is already banned")
end end
end end
def has_subnet?
(ip_addr.ipv4? && ip_addr.prefix < 32) || (ip_addr.ipv6? && ip_addr.prefix < 128)
end
def subnetted_ip
str = ip_addr.to_s
str += "/" + ip_addr.prefix.to_s if has_subnet?
str
end
def ip_addr=(ip_addr)
super(ip_addr.strip)
end
def self.available_includes def self.available_includes
[:creator] [:creator]
end end

View File

@@ -67,7 +67,7 @@ class User < ApplicationRecord
attribute :inviter_id attribute :inviter_id
attribute :last_logged_in_at, default: -> { Time.zone.now } attribute :last_logged_in_at, default: -> { Time.zone.now }
attribute :last_forum_read_at, default: "1960-01-01 00:00:00" attribute :last_forum_read_at, default: "1960-01-01 00:00:00"
attribute :last_ip_addr attribute :last_ip_addr, :ip_address
attribute :comment_threshold, default: -8 attribute :comment_threshold, default: -8
attribute :default_image_size, default: "large" attribute :default_image_size, default: "large"
attribute :favorite_tags attribute :favorite_tags

View File

@@ -85,8 +85,7 @@ class ApplicationPolicy
# The list of attributes that are permitted to be returned by the API. # The list of attributes that are permitted to be returned by the API.
def api_attributes def api_attributes
# XXX allow inet record.class.column_names.map(&:to_sym)
record.class.attribute_types.reject { |_name, attr| attr.type.in?([:inet, :tsvector]) }.keys.map(&:to_sym)
end end
# The list of attributes that are permitted to be used as data-* attributes # The list of attributes that are permitted to be used as data-* attributes

View File

@@ -72,6 +72,8 @@ class UserPolicy < ApplicationPolicy
] ]
end end
attributes += [:last_ip_addr] if policy(:ip_address).show?
attributes attributes
end end

View File

@@ -14,7 +14,7 @@
<%= table_for @ip_bans, class: "striped autofit", width: "100%" do |t| %> <%= table_for @ip_bans, class: "striped autofit", width: "100%" do |t| %>
<% t.column "IP Address" do |ip_ban| %> <% t.column "IP Address" do |ip_ban| %>
<%= link_to ip_ban.subnetted_ip, ip_address_path(ip_ban.ip_addr.to_s) %> <%= link_to ip_ban.ip_addr, ip_address_path(ip_ban.ip_addr.to_s) %>
<% end %> <% end %>
<% t.column "Reason", td: { class: "col-expand" } do |ban| %> <% t.column "Reason", td: { class: "col-expand" } do |ban| %>
<div class="prose"> <div class="prose">

View File

@@ -127,21 +127,28 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end end
should "show hidden attributes to the owner" do should "show hidden attributes to the owner" do
get_auth user_path(@user), @user, params: {format: :json} get_auth user_path(@user), @user, as: :json
json = JSON.parse(response.body)
assert_response :success assert_response :success
assert_not_nil(json["last_logged_in_at"]) assert_not_nil(response.parsed_body["last_logged_in_at"])
end
should "show the last_ip_addr to mods" do
user = create(:user, last_ip_addr: "1.2.3.4")
get_auth user_path(user), create(:mod_user), as: :json
assert_response :success
assert_equal("1.2.3.4", response.parsed_body["last_ip_addr"])
end end
should "not show hidden attributes to others" do should "not show hidden attributes to others" do
@another = create(:user) @another = create(:user)
get_auth user_path(@another), @user, params: {format: :json} get_auth user_path(@another), @user, as: :json
json = JSON.parse(response.body)
assert_response :success assert_response :success
assert_nil(json["last_logged_in_at"]) assert_nil(response.parsed_body["last_logged_in_at"])
assert_nil(response.parsed_body["last_ip_addr"])
end end
should "strip '?' from attributes" do should "strip '?' from attributes" do

View File

@@ -4,14 +4,14 @@ class IpBanTest < ActiveSupport::TestCase
should "be able to ban a user" do should "be able to ban a user" do
ip_ban = create(:ip_ban, ip_addr: "1.2.3.4") ip_ban = create(:ip_ban, ip_addr: "1.2.3.4")
assert_equal("1.2.3.4", ip_ban.subnetted_ip) assert_equal("1.2.3.4", ip_ban.ip_addr.to_s)
assert(IpBan.ip_matches("1.2.3.4").exists?) assert(IpBan.ip_matches("1.2.3.4").exists?)
end end
should "be able to ban a subnet" do should "be able to ban a subnet" do
ip_ban = create(:ip_ban, ip_addr: "1.2.3.4/24") ip_ban = create(:ip_ban, ip_addr: "1.2.3.4/24")
assert_equal("1.2.3.0/24", ip_ban.subnetted_ip) assert_equal("1.2.3.0/24", ip_ban.ip_addr.to_s)
assert(IpBan.ip_matches("1.2.3.0").exists?) assert(IpBan.ip_matches("1.2.3.0").exists?)
assert(IpBan.ip_matches("1.2.3.255").exists?) assert(IpBan.ip_matches("1.2.3.255").exists?)
end end