mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Follow-up to 1fd17242d1.
The previous implementation replaced all four UserHistory fields
(context, details, previous_value, new_value) whenever any one of them
contained the username. This caused confusing output in the staff action
logs UI - for example, a "check email" action that only had the username
in the context field would display:
New: [removed due to user anonymization]
Previous: [removed due to user anonymization]
[removed due to user anonymization]
...even though those fields were originally empty.
Now each field is updated independently using SQL CASE expressions, so
only fields that actually contain the username get replaced. Fields with
unrelated content or nil values are preserved. This keeps the
anonymization effective while avoiding the cluttered display.
Ref - https://meta.discourse.org/t/-/387500
490 lines
16 KiB
Ruby
Vendored
490 lines
16 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe UserAnonymizer do
|
|
let(:admin) { Fabricate(:admin) }
|
|
|
|
describe "event" do
|
|
subject(:make_anonymous) do
|
|
described_class.make_anonymous(user, admin, anonymize_ip: "2.2.2.2")
|
|
end
|
|
|
|
let(:user) { Fabricate(:user, username: "edward") }
|
|
|
|
it "triggers the event" do
|
|
events = DiscourseEvent.track_events { make_anonymous }
|
|
|
|
anon_event = events.detect { |e| e[:event_name] == :user_anonymized }
|
|
expect(anon_event).to be_present
|
|
params_hash = anon_event[:params][0]
|
|
|
|
expect(params_hash[:user]).to eq(user)
|
|
expect(params_hash[:opts][:anonymize_ip]).to eq("2.2.2.2")
|
|
end
|
|
end
|
|
|
|
describe ".make_anonymous" do
|
|
subject(:make_anonymous) { described_class.make_anonymous(user, admin) }
|
|
|
|
let(:original_email) { "edward@example.net" }
|
|
let(:user) { Fabricate(:user, username: "edward", email: original_email) }
|
|
fab!(:another_user, :evil_trout)
|
|
|
|
it "changes username" do
|
|
make_anonymous
|
|
expect(user.reload.username).to match(/^anon\d{3,}$/)
|
|
end
|
|
|
|
it "changes the primary email address" do
|
|
make_anonymous
|
|
expect(user.reload.email).to eq("#{user.username}@anonymized.invalid")
|
|
end
|
|
|
|
it "changes the primary email normalized email address" do
|
|
make_anonymous
|
|
|
|
primary_email = user.reload.primary_email
|
|
|
|
expect(primary_email.normalized_email).to eq("#{user.username}@anonymized.invalid")
|
|
end
|
|
|
|
it "changes the primary email address when there is an email domain allowlist" do
|
|
SiteSetting.allowed_email_domains = "example.net|wayne.com|discourse.org"
|
|
|
|
make_anonymous
|
|
expect(user.reload.email).to eq("#{user.username}@anonymized.invalid")
|
|
end
|
|
|
|
it "deletes secondary email addresses" do
|
|
Fabricate(:secondary_email, user: user, email: "secondary_email@example.com")
|
|
|
|
make_anonymous
|
|
expect(user.reload.secondary_emails).to be_blank
|
|
end
|
|
|
|
it "turns off all notifications" do
|
|
user.user_option.update_columns(
|
|
email_level: UserOption.email_level_types[:always],
|
|
email_messages_level: UserOption.email_level_types[:always],
|
|
)
|
|
|
|
make_anonymous
|
|
user.reload
|
|
expect(user.user_option.email_digests).to eq(false)
|
|
expect(user.user_option.email_level).to eq(UserOption.email_level_types[:never])
|
|
expect(user.user_option.email_messages_level).to eq(UserOption.email_level_types[:never])
|
|
expect(user.user_option.mailing_list_mode).to eq(false)
|
|
end
|
|
|
|
context "when Site Settings do not require full name" do
|
|
before { SiteSetting.full_name_requirement = "optional_at_signup" }
|
|
|
|
it "resets profile to default values" do
|
|
user.update!(name: "Bibi", date_of_birth: 19.years.ago, title: "Super Star")
|
|
|
|
profile = user.reload.user_profile
|
|
upload = Fabricate(:upload)
|
|
|
|
profile.update!(
|
|
location: "Moose Jaw",
|
|
website: "http://www.bim.com",
|
|
bio_raw: "I'm Bibi from Moosejaw. I sing and dance.",
|
|
bio_cooked: "I'm Bibi from Moosejaw. I sing and dance.",
|
|
profile_background_upload: upload,
|
|
bio_cooked_version: 2,
|
|
card_background_upload: upload,
|
|
)
|
|
|
|
prev_username = user.username
|
|
|
|
UserAuthToken.generate!(user_id: user.id)
|
|
|
|
make_anonymous
|
|
user.reload
|
|
|
|
expect(user.username).not_to eq(prev_username)
|
|
expect(user.name).not_to be_present
|
|
expect(user.date_of_birth).to eq(nil)
|
|
expect(user.title).not_to be_present
|
|
expect(user.user_auth_tokens.count).to eq(0)
|
|
|
|
profile = user.reload.user_profile
|
|
expect(profile.location).to eq(nil)
|
|
expect(profile.website).to eq(nil)
|
|
expect(profile.bio_cooked).to eq(nil)
|
|
expect(profile.profile_background_upload).to eq(nil)
|
|
expect(profile.bio_cooked_version).to eq(UserProfile::BAKED_VERSION)
|
|
expect(profile.card_background_upload).to eq(nil)
|
|
end
|
|
end
|
|
|
|
it "clears existing user status" do
|
|
user_status = Fabricate(:user_status, user: user)
|
|
|
|
expect do
|
|
make_anonymous
|
|
user.reload
|
|
end.to change { user.user_status }.from(user_status).to(nil)
|
|
end
|
|
|
|
context "when Site Settings require full name" do
|
|
before { SiteSetting.full_name_requirement = "required_at_signup" }
|
|
|
|
it "changes name to anonymized username" do
|
|
prev_username = user.username
|
|
|
|
user.update(name: "Bibi", date_of_birth: 19.years.ago, title: "Super Star")
|
|
|
|
make_anonymous
|
|
user.reload
|
|
|
|
expect(user.name).not_to eq(prev_username)
|
|
expect(user.name).to eq(user.username)
|
|
end
|
|
end
|
|
|
|
it "removes the avatar" do
|
|
upload = Fabricate(:upload, user: user)
|
|
user.user_avatar = UserAvatar.new(user_id: user.id, custom_upload_id: upload.id)
|
|
user.uploaded_avatar_id = upload.id # chosen in user preferences
|
|
user.save!
|
|
make_anonymous
|
|
user.reload
|
|
expect(user.user_avatar).to eq(nil)
|
|
expect(user.uploaded_avatar_id).to eq(nil)
|
|
end
|
|
|
|
it "updates the avatar in posts" do
|
|
Jobs.run_immediately!
|
|
upload = Fabricate(:upload, user: user)
|
|
user.user_avatar = UserAvatar.new(user_id: user.id, custom_upload_id: upload.id)
|
|
user.uploaded_avatar_id = upload.id # chosen in user preferences
|
|
user.save!
|
|
|
|
topic = Fabricate(:topic, user: user)
|
|
quoted_post = create_post(user: user, topic: topic, post_number: 1, raw: "quoted post")
|
|
|
|
stub_image_size
|
|
post = create_post(raw: <<~RAW)
|
|
Lorem ipsum
|
|
|
|
[quote="#{quoted_post.username}, post:1, topic:#{quoted_post.topic.id}"]
|
|
quoted post
|
|
[/quote]
|
|
RAW
|
|
|
|
old_avatar_url = user.avatar_template.gsub("{size}", "48")
|
|
expect(post.cooked).to include(old_avatar_url)
|
|
|
|
make_anonymous
|
|
post.reload
|
|
new_avatar_url = user.reload.avatar_template.gsub("{size}", "48")
|
|
|
|
expect(post.cooked).to_not include(old_avatar_url)
|
|
expect(post.cooked).to include(new_avatar_url)
|
|
end
|
|
|
|
it "logs the action with the original details" do
|
|
SiteSetting.log_anonymizer_details = true
|
|
helper = UserAnonymizer.new(user, admin)
|
|
orig_email = user.email
|
|
orig_username = user.username
|
|
helper.make_anonymous
|
|
|
|
history = helper.user_history
|
|
expect(history).to be_present
|
|
expect(history.email).to eq(orig_email)
|
|
expect(history.details).to match(orig_username)
|
|
end
|
|
|
|
it "logs the action without the original details" do
|
|
SiteSetting.log_anonymizer_details = false
|
|
helper = UserAnonymizer.new(user, admin)
|
|
orig_email = user.email
|
|
orig_username = user.username
|
|
helper.make_anonymous
|
|
|
|
history = helper.user_history
|
|
expect(history).to be_present
|
|
expect(history.email).not_to eq(orig_email)
|
|
expect(history.details).not_to match(orig_username)
|
|
end
|
|
|
|
it "removes external auth associations" do
|
|
user.user_associated_accounts = [
|
|
UserAssociatedAccount.create(
|
|
user_id: user.id,
|
|
provider_uid: "example",
|
|
provider_name: "facebook",
|
|
),
|
|
]
|
|
user.single_sign_on_record =
|
|
SingleSignOnRecord.create(
|
|
user_id: user.id,
|
|
external_id: "example",
|
|
last_payload: "looks good",
|
|
)
|
|
make_anonymous
|
|
user.reload
|
|
expect(user.user_associated_accounts).to be_empty
|
|
expect(user.single_sign_on_record).to eq(nil)
|
|
end
|
|
|
|
it "removes api key" do
|
|
ApiKey.create!(user_id: user.id)
|
|
|
|
expect { make_anonymous }.to change { ApiKey.count }.by(-1)
|
|
|
|
user.reload
|
|
expect(user.api_keys).to be_empty
|
|
end
|
|
|
|
it "removes user api key" do
|
|
user_api_key = Fabricate(:user_api_key, user: user)
|
|
|
|
expect { make_anonymous }.to change { UserApiKey.count }.by(-1)
|
|
|
|
user.reload
|
|
expect(user.user_api_keys).to be_empty
|
|
end
|
|
|
|
it "removes user auth tokens" do
|
|
UserAuthToken.generate!(user_id: user.id)
|
|
|
|
expect { make_anonymous }.to change { UserAuthToken.count }.by(-1)
|
|
expect(user.reload.user_auth_tokens).to be_empty
|
|
end
|
|
|
|
it "removes second factor credentials" do
|
|
Fabricate(:user_second_factor_totp, user: user)
|
|
|
|
expect { make_anonymous }.to change { UserSecondFactor.count }.by(-1)
|
|
expect(user.reload.user_second_factors).to be_empty
|
|
end
|
|
|
|
it "removes security keys" do
|
|
Fabricate(:user_security_key_with_random_credential, user: user)
|
|
|
|
expect { make_anonymous }.to change { UserSecurityKey.count }.by(-1)
|
|
expect(UserSecurityKey.where(user_id: user.id).count).to eq(0)
|
|
end
|
|
|
|
it "removes push subscriptions" do
|
|
Fabricate(:push_subscription, user: user)
|
|
|
|
expect { make_anonymous }.to change { PushSubscription.count }.by(-1)
|
|
expect(user.reload.push_subscriptions).to be_empty
|
|
end
|
|
|
|
it "removes post reply keys" do
|
|
post = Fabricate(:post)
|
|
PostReplyKey.create!(user_id: user.id, post_id: post.id)
|
|
|
|
expect { make_anonymous }.to change { PostReplyKey.count }.by(-1)
|
|
expect(PostReplyKey.where(user_id: user.id).count).to eq(0)
|
|
end
|
|
|
|
context "when executing jobs" do
|
|
before { Jobs.run_immediately! }
|
|
|
|
it "removes invites" do
|
|
Fabricate(:invited_user, invite: Fabricate(:invite), user: user)
|
|
Fabricate(:invited_user, invite: Fabricate(:invite), user: another_user)
|
|
|
|
expect { make_anonymous }.to change { InvitedUser.count }.by(-1)
|
|
expect(InvitedUser.where(user_id: user.id).count).to eq(0)
|
|
end
|
|
|
|
it "removes email tokens" do
|
|
Fabricate(:email_token, user: user)
|
|
Fabricate(:email_token, user: another_user)
|
|
|
|
expect { make_anonymous }.to change { EmailToken.count }.by(-1)
|
|
expect(EmailToken.where(user_id: user.id).count).to eq(0)
|
|
end
|
|
|
|
it "removes email log entries" do
|
|
Fabricate(:email_log, user: user)
|
|
Fabricate(:email_log, user: another_user)
|
|
|
|
expect { make_anonymous }.to change { EmailLog.count }.by(-1)
|
|
expect(EmailLog.where(user_id: user.id).count).to eq(0)
|
|
end
|
|
|
|
it "removes incoming emails" do
|
|
Fabricate(:incoming_email, user: user, from_address: user.email)
|
|
Fabricate(:incoming_email, from_address: user.email, error: "Some error")
|
|
Fabricate(:incoming_email, user: another_user, from_address: another_user.email)
|
|
|
|
expect { make_anonymous }.to change { IncomingEmail.count }.by(-2)
|
|
expect(IncomingEmail.where(user_id: user.id).count).to eq(0)
|
|
expect(IncomingEmail.where(from_address: original_email).count).to eq(0)
|
|
end
|
|
|
|
it "removes raw email from posts" do
|
|
post1 = Fabricate(:post, user: user, via_email: true, raw_email: "raw email from user")
|
|
post2 =
|
|
Fabricate(
|
|
:post,
|
|
user: another_user,
|
|
via_email: true,
|
|
raw_email: "raw email from another user",
|
|
)
|
|
|
|
make_anonymous
|
|
|
|
expect(post1.reload).to have_attributes(via_email: true, raw_email: nil)
|
|
expect(post2.reload).to have_attributes(
|
|
via_email: true,
|
|
raw_email: "raw email from another user",
|
|
)
|
|
end
|
|
|
|
it "does not delete profile views" do
|
|
UserProfileView.add(user.id, "127.0.0.1", another_user.id, Time.now, true)
|
|
expect { make_anonymous }.to_not change { UserProfileView.count }
|
|
end
|
|
|
|
it "removes user field values" do
|
|
field1 = Fabricate(:user_field)
|
|
field2 = Fabricate(:user_field)
|
|
|
|
user.custom_fields = {
|
|
some_field: "123",
|
|
"user_field_#{field1.id}": "foo",
|
|
"user_field_#{field2.id}": "bar",
|
|
another_field: "456",
|
|
}
|
|
|
|
expect { make_anonymous }.to change { user.custom_fields }
|
|
expect(user.reload.custom_fields).to eq("some_field" => "123", "another_field" => "456")
|
|
end
|
|
|
|
context "when log_anonymizer_details is disabled" do
|
|
before { SiteSetting.log_anonymizer_details = false }
|
|
|
|
it "anonymizes username only in fields that contain it" do
|
|
StaffActionLogger.new(admin).log_check_email(
|
|
user,
|
|
context: "/admin/users/#{user.id}/#{user.username}",
|
|
)
|
|
StaffActionLogger.new(admin).log_username_change(user, user.username, "newname")
|
|
|
|
make_anonymous
|
|
|
|
reason = I18n.t("user.anonymized")
|
|
check_email =
|
|
UserHistory.find_by(action: UserHistory.actions[:check_email], target_user_id: user.id)
|
|
username_change =
|
|
UserHistory.find_by(
|
|
action: UserHistory.actions[:change_username],
|
|
target_user_id: user.id,
|
|
)
|
|
|
|
expect(check_email.context).to eq(reason)
|
|
expect(check_email.details).to be_nil
|
|
expect(check_email.previous_value).to be_nil
|
|
expect(check_email.new_value).to be_nil
|
|
|
|
expect(username_change.previous_value).to eq(reason)
|
|
expect(username_change.new_value).to eq("newname")
|
|
end
|
|
|
|
it "does not affect records without the username" do
|
|
StaffActionLogger.new(admin).log_check_email(user, context: "/some/other/path")
|
|
make_anonymous
|
|
expect(UserHistory.find_by(target_user_id: user.id).context).to eq("/some/other/path")
|
|
end
|
|
end
|
|
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) }
|
|
fab!(: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
|
|
Jobs.run_immediately!
|
|
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
|
|
|
|
ip_address_history = UserIpAddressHistory.create!(user_id: user.id, ip_address: old_ip)
|
|
|
|
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(UserIpAddressHistory.exists?(id: ip_address_history.id)).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "anonymize_emails" do
|
|
it "destroys all associated invites" do
|
|
invite = Fabricate(:invite, email: "test@example.com")
|
|
user = invite.redeem
|
|
|
|
Jobs.run_immediately!
|
|
described_class.make_anonymous(user, admin)
|
|
|
|
expect(user.email).not_to eq("test@example.com")
|
|
expect(Invite.exists?(id: invite.id)).to eq(false)
|
|
end
|
|
end
|
|
end
|