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
class IpAddress
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
def initialize(string)
@ip_address = ::IPAddress.parse(string.to_s)
@ip_address = ::IPAddress.parse(string.to_s.strip)
end
def ip_lookup
@@ -39,9 +39,13 @@ module Danbooru
ip_address.include?(other.ip_address)
end
def as_json
to_s
end
# "1.2.3.4/24" if the address is a subnet, "1.2.3.4" otherwise.
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
def inspect

View File

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

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class IpBan < ApplicationRecord
attribute :ip_addr, :ip_address
belongs_to :creator, class_name: "User"
validate :validate_ip_addr
@@ -23,7 +25,7 @@ class IpBan < ApplicationRecord
end
def self.ip_matches(ip_addr)
where("ip_addr >>= ?", ip_addr)
where("ip_addr >>= ?", ip_addr.to_s)
end
def self.hit!(category, ip_addr)
@@ -62,7 +64,7 @@ class IpBan < ApplicationRecord
def validate_ip_addr
if ip_addr.blank?
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")
elsif full_ban? && ip_addr.ipv4? && ip_addr.prefix < 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")
elsif partial_ban? && ip_addr.ipv6? && ip_addr.prefix < 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")
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
[:creator]
end

View File

@@ -67,7 +67,7 @@ class User < ApplicationRecord
attribute :inviter_id
attribute :last_logged_in_at, default: -> { Time.zone.now }
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 :default_image_size, default: "large"
attribute :favorite_tags

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
<%= table_for @ip_bans, class: "striped autofit", width: "100%" do |t| %>
<% 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 %>
<% t.column "Reason", td: { class: "col-expand" } do |ban| %>
<div class="prose">

View File

@@ -127,21 +127,28 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end
should "show hidden attributes to the owner" do
get_auth user_path(@user), @user, params: {format: :json}
json = JSON.parse(response.body)
get_auth user_path(@user), @user, as: :json
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
should "not show hidden attributes to others" do
@another = create(:user)
get_auth user_path(@another), @user, params: {format: :json}
json = JSON.parse(response.body)
get_auth user_path(@another), @user, as: :json
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
should "strip '?' from attributes" do

View File

@@ -4,14 +4,14 @@ class IpBanTest < ActiveSupport::TestCase
should "be able to ban a user" do
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?)
end
should "be able to ban a subnet" do
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.255").exists?)
end