discourse/spec/services/user_anonymizer_spec.rb
Sam c2332d7505
FEATURE: reduce avatar sizes to 6 from 20 (#21319)
* FEATURE: reduce avatar sizes to 6 from 20

This PR introduces 3 changes:

1. SiteSetting.avatar_sizes, now does what is says on the tin.
previously it would introduce a large number of extra sizes, to allow for
various DPIs. Instead we now trust the admin with the size list.

2. When `avatar_sizes` changes, we ensure consistency and remove resized
avatars that are not longer allowed per site setting. This happens on the
12 hourly job and limited out of the box to 20k cleanups per cycle, given
this may reach out to AWS 20k times to remove things.

3.Our default avatar sizes are now "24|48|72|96|144|288" these sizes were
very specifically picked to limit amount of bluriness introduced by webkit.
Our avatars are already blurry due to 1px border, so this corrects old blur.

This change heavily reduces storage required by forums which simplifies
site moves and more.

Co-authored-by: David Taylor <david@taylorhq.com>
2023-06-01 10:00:01 +10:00

405 lines
13 KiB
Ruby

# frozen_string_literal: true
RSpec.describe UserAnonymizer do
let(:admin) { Fabricate(:admin) }
describe "event" do
let(:user) { Fabricate(:user, username: "edward") }
subject(:make_anonymous) do
described_class.make_anonymous(user, admin, anonymize_ip: "2.2.2.2")
end
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
let(:original_email) { "edward@example.net" }
let(:user) { Fabricate(:user, username: "edward", email: original_email) }
fab!(:another_user) { Fabricate(:evil_trout) }
subject(:make_anonymous) { described_class.make_anonymous(user, admin) }
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 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_required = false }
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_required = true }
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
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
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) { 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
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
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)
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