* CLI interface for creating/deleting local users - tootctl accounts add USERNAME - tootctl accounts del USERNAME * Add CLI interface for culling remote users that no longer exist - tootctl accounts cull
		
			
				
	
	
		
			195 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| require 'rubygems/package'
 | |
| require_relative '../../config/boot'
 | |
| require_relative '../../config/environment'
 | |
| require_relative 'cli_helper'
 | |
| 
 | |
| module Mastodon
 | |
|   class AccountsCLI < Thor
 | |
|     option :all, type: :boolean
 | |
|     desc 'rotate [USERNAME]', 'Generate and broadcast new keys'
 | |
|     long_desc <<-LONG_DESC
 | |
|       Generate and broadcast new RSA keys as part of security
 | |
|       maintenance.
 | |
| 
 | |
|       With the --all option, all local accounts will be subject
 | |
|       to the rotation. Otherwise, and by default, only a single
 | |
|       account specified by the USERNAME argument will be
 | |
|       processed.
 | |
|     LONG_DESC
 | |
|     def rotate(username = nil)
 | |
|       if options[:all]
 | |
|         processed = 0
 | |
|         delay     = 0
 | |
| 
 | |
|         Account.local.without_suspended.find_in_batches do |accounts|
 | |
|           accounts.each do |account|
 | |
|             rotate_keys_for_account(account, delay)
 | |
|             processed += 1
 | |
|             say('.', :green, false)
 | |
|           end
 | |
| 
 | |
|           delay += 5.minutes
 | |
|         end
 | |
| 
 | |
|         say
 | |
|         say("OK, rotated keys for #{processed} accounts", :green)
 | |
|       elsif username.present?
 | |
|         rotate_keys_for_account(Account.find_local(username))
 | |
|         say('OK', :green)
 | |
|       else
 | |
|         say('No account(s) given', :red)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     option :email, required: true
 | |
|     option :confirmed, type: :boolean
 | |
|     option :role, default: 'user'
 | |
|     option :reattach, type: :boolean
 | |
|     option :force, type: :boolean
 | |
|     desc 'add USERNAME', 'Create a new user'
 | |
|     long_desc <<-LONG_DESC
 | |
|       Create a new user account with a given USERNAME and an
 | |
|       e-mail address provided with --email.
 | |
| 
 | |
|       With the --confirmed option, the confirmation e-mail will
 | |
|       be skipped and the account will be active straight away.
 | |
| 
 | |
|       With the --role option one of  "user", "admin" or "moderator"
 | |
|       can be supplied. Defaults to "user"
 | |
| 
 | |
|       With the --reattach option, the new user will be reattached
 | |
|       to a given existing username of an old account. If the old
 | |
|       account is still in use by someone else, you can supply
 | |
|       the --force option to delete the old record and reattach the
 | |
|       username to the new account anyway.
 | |
|     LONG_DESC
 | |
|     def add(username)
 | |
|       account  = Account.new(username: username)
 | |
|       password = SecureRandom.hex
 | |
|       user     = User.new(email: options[:email], password: password, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: Time.now.utc)
 | |
| 
 | |
|       if options[:reattach]
 | |
|         account = Account.find_local(username) || Account.new(username: username)
 | |
| 
 | |
|         if account.user.present? && !options[:force]
 | |
|           say('The chosen username is currently in use', :red)
 | |
|           say('Use --force to reattach it anyway and delete the other user')
 | |
|           return
 | |
|         elsif account.user.present?
 | |
|           account.user.destroy!
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       user.account = account
 | |
| 
 | |
|       if user.save
 | |
|         if options[:confirmed]
 | |
|           user.confirmed_at = nil
 | |
|           user.confirm!
 | |
|         end
 | |
| 
 | |
|         say('OK', :green)
 | |
|         say("New password: #{password}")
 | |
|       else
 | |
|         user.errors.to_h.each do |key, error|
 | |
|           say('Failure/Error: ', :red)
 | |
|           say(key)
 | |
|           say('    ' + error, :red)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     desc 'del USERNAME', 'Delete a user'
 | |
|     long_desc <<-LONG_DESC
 | |
|       Remove a user account with a given USERNAME.
 | |
|     LONG_DESC
 | |
|     def del(username)
 | |
|       account = Account.find_local(username)
 | |
| 
 | |
|       if account.nil?
 | |
|         say('No user with such username', :red)
 | |
|         return
 | |
|       end
 | |
| 
 | |
|       say("Deleting user with #{account.statuses_count}, this might take a while...")
 | |
|       SuspendAccountService.new.call(account, remove_user: true)
 | |
|       say('OK', :green)
 | |
|     end
 | |
| 
 | |
|     option :dry_run, type: :boolean
 | |
|     desc 'cull', 'Remove remote accounts that no longer exist'
 | |
|     long_desc <<-LONG_DESC
 | |
|       Query every single remote account in the database to determine
 | |
|       if it still exists on the origin server, and if it doesn't,
 | |
|       remove it from the database.
 | |
| 
 | |
|       Accounts that have had confirmed activity within the last week
 | |
|       are excluded from the checks.
 | |
| 
 | |
|       If 10 or more accounts from the same domain cannot be queried
 | |
|       due to a connection error (such as missing DNS records) then
 | |
|       the domain is considered dead, and all other accounts from it
 | |
|       are deleted without further querying.
 | |
| 
 | |
|       With the --dry-run option, no deletes will actually be carried
 | |
|       out.
 | |
|     LONG_DESC
 | |
|     def cull
 | |
|       domain_thresholds = Hash.new { |hash, key| hash[key] = 0 }
 | |
|       skip_threshold    = 7.days.ago
 | |
|       culled            = 0
 | |
|       dead_servers      = []
 | |
|       dry_run           = options[:dry_run] ? ' (DRY RUN)' : ''
 | |
| 
 | |
|       Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
 | |
|         next if account.updated_at >= skip_threshold || account.last_webfingered_at >= skip_threshold
 | |
| 
 | |
|         unless dead_servers.include?(account.domain)
 | |
|           begin
 | |
|             code = Request.new(:head, account.uri).perform(&:code)
 | |
|           rescue HTTP::ConnectionError
 | |
|             domain_thresholds[account.domain] += 1
 | |
| 
 | |
|             if domain_thresholds[account.domain] >= 10
 | |
|               dead_servers << account.domain
 | |
|             end
 | |
|           rescue StandardError
 | |
|             next
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         if [404, 410].include?(code) || dead_servers.include?(account.domain)
 | |
|           unless options[:dry_run]
 | |
|             SuspendAccountService.new.call(account)
 | |
|             account.destroy
 | |
|           end
 | |
| 
 | |
|           culled += 1
 | |
|           say('.', :green, false)
 | |
|         else
 | |
|           say('.', nil, false)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       say
 | |
|       say("Removed #{culled} accounts (#{dead_servers.size} dead servers)#{dry_run}", :green)
 | |
| 
 | |
|       unless dead_servers.empty?
 | |
|         say('R.I.P.:', :yellow)
 | |
|         dead_servers.each { |domain| say('    ' + domain) }
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     private
 | |
| 
 | |
|     def rotate_keys_for_account(account, delay = 0)
 | |
|       old_key = account.private_key
 | |
|       new_key = OpenSSL::PKey::RSA.new(2048).to_pem
 | |
|       account.update(private_key: new_key)
 | |
|       ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
 | |
|     end
 | |
|   end
 | |
| end
 |