0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-13 13:17:29 +08:00
discourse/app/services/user_anonymizer.rb
Régis Hanol 4aed472271
FIX: Block associated account emails on user delete and anonymize (#37638)
When an admin deletes a user and blocks their email/IP, only primary
and secondary emails (from `user_emails`) were added to the screened
emails list. OAuth/social login emails (Google, Facebook, GitHub, etc.)
stored in `user_associated_accounts` were not blocked, allowing the
deleted user to re-register from a different IP using their OAuth
account.

Similarly, when anonymizing a user, invite cleanup, incoming email
deletion, and screened email IP anonymization only considered the
primary email, missing associated account emails entirely.

Collect emails from `user_associated_accounts.info['email']` alongside
`user_emails` using array union (`|`) for natural deduplication:

- `UserDestroyer`: block and clean up invites for all emails
- `UserAnonymizer`: capture associated emails before they are destroyed,
   pass them to the `AnonymizeUser` job
- `AnonymizeUser` job: use all emails for invite, incoming email, and
   screened email cleanup

Ref - t/173323
2026-02-09 18:26:35 +01:00

128 lines
3.7 KiB
Ruby
Vendored

# frozen_string_literal: true
class UserAnonymizer
attr_reader :user_history
EMAIL_SUFFIX = "@anonymized.invalid"
# opts:
# anonymize_ip - an optional new IP to update their logs with
def initialize(user, actor = nil, opts = nil)
@user = user
@actor = actor
@user_history = nil
@opts = opts || {}
end
def self.make_anonymous(user, actor = nil, opts = nil)
self.new(user, actor, opts).make_anonymous
end
def make_anonymous
User.transaction do
@prev_emails =
UserEmail.where(user_id: @user.id).pluck(:email) |
UserAssociatedAccount.where(user_id: @user.id).pluck(Arel.sql("info->>'email'")).compact
@prev_username = @user.username
unless UsernameChanger.new(@user, make_anon_username).change(run_update_job: false)
raise "Failed to change username"
end
@user.reload
@user.password = SecureRandom.hex
@user.name = SiteSetting.full_name_requirement == "required_at_signup" ? @user.username : nil
@user.date_of_birth = nil
@user.title = nil
@user.uploaded_avatar_id = nil
if @opts.has_key?(:anonymize_ip)
@user.ip_address = @opts[:anonymize_ip]
@user.registration_ip_address = @opts[:anonymize_ip]
end
@user.save!
@user.primary_email.update_attribute(:email, "#{@user.username}#{EMAIL_SUFFIX}")
@user.primary_email.update_attribute(:normalized_email, "#{@user.username}#{EMAIL_SUFFIX}")
options = @user.user_option
options.mailing_list_mode = false
options.email_digests = false
options.email_level = UserOption.email_level_types[:never]
options.email_messages_level = UserOption.email_level_types[:never]
options.save!
if profile = @user.user_profile
profile.update!(
location: nil,
website: nil,
bio_raw: nil,
bio_cooked: nil,
profile_background_upload: nil,
card_background_upload: nil,
)
end
@user.clear_status!
@user.user_avatar&.destroy!
@user.single_sign_on_record&.destroy!
@user.oauth2_user_infos.destroy_all
@user.user_associated_accounts.destroy_all
@user.api_keys.destroy_all
@user.user_api_keys.destroy_all
@user.user_auth_tokens.destroy_all
@user.user_second_factors.destroy_all
UserSecurityKey.where(user_id: @user.id).delete_all
@user.push_subscriptions.destroy_all
PostReplyKey.where(user_id: @user.id).delete_all
@user.user_emails.secondary.destroy_all
@user_history = log_action
end
UsernameChanger.update_username(
user_id: @user.id,
old_username: @prev_username,
new_username: @user.username,
avatar_template: @user.avatar_template,
)
Jobs.enqueue(
:anonymize_user,
user_id: @user.id,
prev_emails: @prev_emails,
prev_username: @prev_username,
anonymize_ip: @opts[:anonymize_ip],
)
DiscourseEvent.trigger(:user_anonymized, user: @user, opts: @opts)
@user
end
private
def make_anon_username
100.times do
new_username = "anon#{(SecureRandom.random_number * 100_000_000).to_i}"
return new_username unless User.where(username_lower: new_username).exists?
end
raise "Failed to generate an anon username"
end
def log_action
history_details = {
action: UserHistory.actions[:anonymize_user],
target_user_id: @user.id,
acting_user_id: @actor ? @actor.id : @user.id,
}
if SiteSetting.log_anonymizer_details?
history_details[:email] = @prev_emails.first
history_details[:details] = "username: #{@prev_username}"
end
UserHistory.create!(history_details)
end
end