discourse/lib/tasks/users.rake
Penar Musaraj 80ac3275ba
DEV: update rake task to disable 2FA for a user (#29052)
- limits security key deletes to second factor keys
- also deletes backup codes (lingering backup codes break login flow entirely)

* Add spec for rake task to disable 2FA for a user
2024-10-16 09:11:29 +11:00

240 lines
7.1 KiB
Ruby

# frozen_string_literal: true
desc "Change topic/post ownership of all the topics/posts by a specific user (without creating new revision)"
task "users:change_post_ownership",
%i[old_username new_username archetype] => [:environment] do |_, args|
old_username = args[:old_username]
new_username = args[:new_username]
archetype = args[:archetype]
archetype = archetype.downcase if archetype
if !old_username || !new_username
puts "ERROR: Expecting rake users:change_post_ownership[old_username,new_username,archetype]"
exit 1
end
old_user = find_user(old_username)
new_user = find_user(new_username)
if archetype == "private"
posts = Post.private_posts.where(user_id: old_user.id)
elsif archetype == "public" || !archetype
posts = Post.public_posts.where(user_id: old_user.id)
else
puts "ERROR: Expecting rake users:change_post_ownership[old_username,new_username,archetype] where archetype is public or private"
exit 1
end
puts "Changing post ownership"
i = 0
posts.each do |p|
PostOwnerChanger.new(
post_ids: [p.id],
topic_id: p.topic.id,
new_owner: User.find_by(username_lower: new_user.username_lower),
acting_user: User.find_by(username_lower: "system"),
skip_revision: true,
).change_owner!
putc "."
i += 1
end
puts "", "#{i} posts ownership changed!", ""
end
desc "Merge the source user into the target user"
task "users:merge", %i[source_username target_username] => [:environment] do |_, args|
source_username = args[:source_username]
target_username = args[:target_username]
if !source_username || !target_username
puts "ERROR: Expecting rake users:merge[source_username,target_username]"
exit 1
end
source_user = find_user(source_username)
target_user = find_user(target_username)
UserMerger.new(source_user, target_user).merge!
puts "", "Users merged!", ""
end
desc "Rename a user"
task "users:rename", %i[old_username new_username] => [:environment] do |_, args|
old_username = args[:old_username]
new_username = args[:new_username]
if !old_username || !new_username
puts "ERROR: Expecting rake users:rename[old_username,new_username]"
exit 1
end
changer = UsernameChanger.new(find_user(old_username), new_username)
changer.change(asynchronous: false)
puts "", "User renamed!", ""
end
desc "Update username in quotes and mentions. Use this if the user was renamed before proper renaming existed."
task "users:update_posts", %i[old_username current_username] => [:environment] do |_, args|
old_username = args[:old_username]
current_username = args[:current_username]
if !old_username || !current_username
puts "ERROR: Expecting rake users:update_posts[old_username,current_username]"
exit 1
end
user = find_user(current_username)
UsernameChanger.update_username(
user_id: user.id,
old_username: old_username,
new_username: user.username,
avatar_template: user.avatar_template,
asynchronous: false,
)
puts "", "Username updated!", ""
end
desc "Recalculate post and topic counts in user stats"
task "users:recalculate_post_counts" => :environment do
puts "", "Updating user stats..."
filter_public_posts_and_topics = <<~SQL
p.deleted_at IS NULL
AND NOT COALESCE(p.hidden, 't')
AND p.post_type = 1
AND t.deleted_at IS NULL
AND COALESCE(t.visible, 't')
AND t.archetype <> 'private_message'
AND p.user_id > 0
SQL
puts "post counts..."
# all public replies
DB.exec <<~SQL
WITH X AS (
SELECT p.user_id, COUNT(p.id) post_count
FROM posts p
JOIN topics t ON t.id = p.topic_id
WHERE #{filter_public_posts_and_topics}
AND p.post_number > 1
GROUP BY p.user_id
)
UPDATE user_stats
SET post_count = X.post_count
FROM X
WHERE user_stats.user_id = X.user_id
AND user_stats.post_count <> X.post_count
SQL
puts "topic counts..."
# public topics
DB.exec <<~SQL
WITH X AS (
SELECT p.user_id, COUNT(p.id) topic_count
FROM posts p
JOIN topics t ON t.id = p.topic_id
WHERE #{filter_public_posts_and_topics}
AND p.post_number = 1
GROUP BY p.user_id
)
UPDATE user_stats
SET topic_count = X.topic_count
FROM X
WHERE user_stats.user_id = X.user_id
AND user_stats.topic_count <> X.topic_count
SQL
puts "Done!", ""
end
desc "Disable 2FA for user with the given username"
task "users:disable_2fa", [:username] => [:environment] do |_, args|
username = args[:username]
user = find_user(username)
UserSecondFactor.where(user_id: user.id, method: UserSecondFactor.methods[:totp]).each(&:destroy!)
UserSecurityKey.where(
user_id: user.id,
factor_type: UserSecurityKey.factor_types[:second_factor],
).destroy_all
UserSecondFactor.where(user_id: user.id, method: UserSecondFactor.methods[:backup_codes]).each(
&:destroy!
)
puts "2FA disabled for #{username}"
end
desc "Anonymize all users except staff"
task "users:anonymize_all" => :environment do
require "highline/import"
non_staff_users = User.where("NOT admin AND NOT moderator")
total = non_staff_users.count
anonymized = 0
confirm_anonymize = ask("Are you sure you want to anonymize #{total} users? (Y/n)")
exit 1 unless (confirm_anonymize == "" || confirm_anonymize.downcase == "y")
system_user = Discourse.system_user
non_staff_users.each do |user|
begin
UserAnonymizer.new(user, system_user).make_anonymous
print_status(anonymized += 1, total)
rescue StandardError
# skip
end
end
puts "", "#{total} users anonymized.", ""
end
desc "Anonymize user with the given username"
task "users:anonymize", [:username] => [:environment] do |_, args|
username = args[:username]
user = find_user(username)
system_user = Discourse.system_user
UserAnonymizer.new(user, system_user).make_anonymous
puts "User #{username} anonymized"
end
desc "List all users which have been staff in the last month"
task "users:list_recent_staff" => :environment do
current_staff_ids = User.human_users.where("admin OR moderator").pluck(:id)
recent_actions = UserHistory.where("created_at > ?", 1.month.ago)
recent_admin_ids =
recent_actions.where(action: UserHistory.actions[:revoke_admin]).pluck(:target_user_id)
recent_moderator_ids =
recent_actions.where(action: UserHistory.actions[:revoke_moderation]).pluck(:target_user_id)
all_ids = current_staff_ids + recent_admin_ids + recent_moderator_ids
users = User.where(id: all_ids.uniq)
puts "Users which have had staff privileges in the last month:"
users.each { |user| puts "#{user.id}: #{user.username} (#{user.email})" }
puts "----"
puts "user_ids = [#{all_ids.uniq.join(",")}]"
end
desc "Check if a user exists for given email address"
task "users:exists", [:email] => [:environment] do |_, args|
email = args[:email]
if User.find_by_email(email)
puts "User with email #{email} exists"
exit 0
end
puts "ERROR: User with email #{email} not found"
exit 1
end
def find_user(username)
user = User.find_by_username(username)
if !user
puts "ERROR: User with username #{username} does not exist"
exit 1
end
user
end