FEATURE: Support anonymizing a user's IP addresses

This commit is contained in:
Robin Ward 2018-04-30 13:42:51 -04:00
parent 93b40d5e59
commit e21a4ce1dd
2 changed files with 122 additions and 15 deletions

View File

@ -2,20 +2,23 @@ class UserAnonymizer
attr_reader :user_history attr_reader :user_history
def initialize(user, actor = nil) # opts:
# anonymize_ip - an optional new IP to update their logs with
def initialize(user, actor = nil, opts = nil)
@user = user @user = user
@actor = actor @actor = actor
@user_history = nil @user_history = nil
@opts = opts || {}
end end
def self.make_anonymous(user, actor = nil) def self.make_anonymous(user, actor = nil, opts = nil)
self.new(user, actor).make_anonymous self.new(user, actor, opts).make_anonymous
end end
def make_anonymous def make_anonymous
User.transaction do User.transaction do
prev_email = @user.email @prev_email = @user.email
prev_username = @user.username @prev_username = @user.username
if !UsernameChanger.change(@user, make_anon_username) if !UsernameChanger.change(@user, make_anon_username)
raise "Failed to change username" raise "Failed to change username"
@ -28,6 +31,11 @@ class UserAnonymizer
@user.date_of_birth = nil @user.date_of_birth = nil
@user.title = nil @user.title = nil
@user.uploaded_avatar_id = nil @user.uploaded_avatar_id = nil
if @opts.has_key?(:anonymize_ip)
anonymize_ips(@opts[:anonymize_ip])
end
@user.save @user.save
options = @user.user_option options = @user.user_option
@ -60,8 +68,8 @@ class UserAnonymizer
} }
if SiteSetting.log_anonymizer_details? if SiteSetting.log_anonymizer_details?
history_details[:email] = prev_email history_details[:email] = @prev_email
history_details[:details] = "username: #{prev_username}" history_details[:details] = "username: #{@prev_username}"
end end
@user_history = UserHistory.create(history_details) @user_history = UserHistory.create(history_details)
@ -69,11 +77,39 @@ class UserAnonymizer
@user @user
end end
def make_anon_username private
100.times do
new_username = "anon#{(SecureRandom.random_number * 100000000).to_i}" def make_anon_username
return new_username unless User.where(username_lower: new_username).exists? 100.times do
new_username = "anon#{(SecureRandom.random_number * 100000000).to_i}"
return new_username unless User.where(username_lower: new_username).exists?
end
raise "Failed to generate an anon username"
end end
raise "Failed to generate an anon username"
def ip_where(column = 'user_id')
["#{column} = :user_id AND ip_address IS NOT NULL", user_id: @user.id]
end end
def anonymize_ips(new_ip)
@user.ip_address = new_ip
@user.registration_ip_address = new_ip
IncomingLink.where(ip_where('current_user_id')).update_all(ip_address: new_ip)
ScreenedEmail.where(email: @prev_email).update_all(ip_address: new_ip)
SearchLog.where(ip_where).update_all(ip_address: new_ip)
TopicLinkClick.where(ip_where).update_all(ip_address: new_ip)
TopicViewItem.where(ip_where).update_all(ip_address: new_ip)
UserHistory.where(ip_where('acting_user_id')).update_all(ip_address: new_ip)
UserProfileView.where(ip_where).update_all(ip_address: new_ip)
# UserHistory for delete_user logs the user's IP. Note this is quite ugly but we don't
# have a better way of querying on details right now.
UserHistory.where(
"action = :action AND details LIKE 'id: #{@user.id}\n%'",
action: UserHistory.actions[:delete_user]
).update_all(ip_address: new_ip)
end
end end

View File

@ -2,10 +2,10 @@ require "rails_helper"
describe UserAnonymizer do describe UserAnonymizer do
describe "make_anonymous" do let(:admin) { Fabricate(:admin) }
let(:admin) { Fabricate(:admin) }
let(:user) { Fabricate(:user, username: "edward") }
describe "make_anonymous" do
let(:user) { Fabricate(:user, username: "edward") }
subject(:make_anonymous) { described_class.make_anonymous(user, admin) } subject(:make_anonymous) { described_class.make_anonymous(user, admin) }
it "changes username" do it "changes username" do
@ -157,4 +157,75 @@ describe UserAnonymizer do
end end
describe "anonymize_ip" do
let(:old_ip) { "1.2.3.4" }
let(:anon_ip) { "0.0.0.0" }
let(:user) { Fabricate(:user, ip_address: old_ip, registration_ip_address: old_ip) }
let(:post) { Fabricate(:post) }
let(:topic) { post.topic }
it "doesn't anonymize ips by default" do
UserAnonymizer.make_anonymous(user, admin)
expect(user.ip_address).to eq(old_ip)
end
it "is called if you pass an option" do
UserAnonymizer.make_anonymous(user, admin, anonymize_ip: anon_ip)
user.reload
expect(user.ip_address).to eq(anon_ip)
end
it "exhaustively replaces all user ips" do
link = IncomingLink.create!(current_user_id: user.id, ip_address: old_ip, post_id: post.id)
screened_email = ScreenedEmail.create!(email: user.email, ip_address: old_ip)
search_log = SearchLog.create!(
term: 'wat',
search_type: SearchLog.search_types[:header],
user_id: user.id,
ip_address: old_ip
)
topic_link = TopicLink.create!(
user_id: admin.id,
topic_id: topic.id,
url: 'https://discourse.org',
domain: 'discourse.org'
)
topic_link_click = TopicLinkClick.create!(
topic_link_id: topic_link.id,
user_id: user.id,
ip_address: old_ip
)
user_profile_view = UserProfileView.create!(
user_id: user.id,
user_profile_id: admin.user_profile.id,
ip_address: old_ip,
viewed_at: Time.now
)
TopicViewItem.create!(topic_id: topic.id, user_id: user.id, ip_address: old_ip, viewed_at: Time.now)
delete_history = StaffActionLogger.new(admin).log_user_deletion(user)
user_history = StaffActionLogger.new(user).log_backup_create
UserAnonymizer.make_anonymous(user, admin, anonymize_ip: anon_ip)
expect(user.registration_ip_address).to eq(anon_ip)
expect(link.reload.ip_address).to eq(anon_ip)
expect(screened_email.reload.ip_address).to eq(anon_ip)
expect(search_log.reload.ip_address).to eq(anon_ip)
expect(topic_link_click.reload.ip_address).to eq(anon_ip)
topic_view = TopicViewItem.where(topic_id: topic.id, user_id: user.id).first
expect(topic_view.ip_address).to eq(anon_ip)
expect(delete_history.reload.ip_address).to eq(anon_ip)
expect(user_history.reload.ip_address).to eq(anon_ip)
expect(user_profile_view.reload.ip_address).to eq(anon_ip)
expect("failed").to eq("success")
end
end
end end