accounts_cli.rb 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. # frozen_string_literal: true
  2. require 'rubygems/package'
  3. require_relative '../../config/boot'
  4. require_relative '../../config/environment'
  5. require_relative 'cli_helper'
  6. module Mastodon
  7. class AccountsCLI < Thor
  8. option :all, type: :boolean
  9. desc 'rotate [USERNAME]', 'Generate and broadcast new keys'
  10. long_desc <<-LONG_DESC
  11. Generate and broadcast new RSA keys as part of security
  12. maintenance.
  13. With the --all option, all local accounts will be subject
  14. to the rotation. Otherwise, and by default, only a single
  15. account specified by the USERNAME argument will be
  16. processed.
  17. LONG_DESC
  18. def rotate(username = nil)
  19. if options[:all]
  20. processed = 0
  21. delay = 0
  22. Account.local.without_suspended.find_in_batches do |accounts|
  23. accounts.each do |account|
  24. rotate_keys_for_account(account, delay)
  25. processed += 1
  26. say('.', :green, false)
  27. end
  28. delay += 5.minutes
  29. end
  30. say
  31. say("OK, rotated keys for #{processed} accounts", :green)
  32. elsif username.present?
  33. rotate_keys_for_account(Account.find_local(username))
  34. say('OK', :green)
  35. else
  36. say('No account(s) given', :red)
  37. exit(1)
  38. end
  39. end
  40. option :email, required: true
  41. option :confirmed, type: :boolean
  42. option :role, default: 'user'
  43. option :reattach, type: :boolean
  44. option :force, type: :boolean
  45. desc 'create USERNAME', 'Create a new user'
  46. long_desc <<-LONG_DESC
  47. Create a new user account with a given USERNAME and an
  48. e-mail address provided with --email.
  49. With the --confirmed option, the confirmation e-mail will
  50. be skipped and the account will be active straight away.
  51. With the --role option one of "user", "admin" or "moderator"
  52. can be supplied. Defaults to "user"
  53. With the --reattach option, the new user will be reattached
  54. to a given existing username of an old account. If the old
  55. account is still in use by someone else, you can supply
  56. the --force option to delete the old record and reattach the
  57. username to the new account anyway.
  58. LONG_DESC
  59. def create(username)
  60. account = Account.new(username: username)
  61. password = SecureRandom.hex
  62. user = User.new(email: options[:email], password: password, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: Time.now.utc)
  63. if options[:reattach]
  64. account = Account.find_local(username) || Account.new(username: username)
  65. if account.user.present? && !options[:force]
  66. say('The chosen username is currently in use', :red)
  67. say('Use --force to reattach it anyway and delete the other user')
  68. return
  69. elsif account.user.present?
  70. account.user.destroy!
  71. end
  72. end
  73. user.account = account
  74. if user.save
  75. if options[:confirmed]
  76. user.confirmed_at = nil
  77. user.confirm!
  78. end
  79. say('OK', :green)
  80. say("New password: #{password}")
  81. else
  82. user.errors.to_h.each do |key, error|
  83. say('Failure/Error: ', :red)
  84. say(key)
  85. say(' ' + error, :red)
  86. end
  87. exit(1)
  88. end
  89. end
  90. option :role
  91. option :email
  92. option :confirm, type: :boolean
  93. option :enable, type: :boolean
  94. option :disable, type: :boolean
  95. option :disable_2fa, type: :boolean
  96. desc 'modify USERNAME', 'Modify a user'
  97. long_desc <<-LONG_DESC
  98. Modify a user account.
  99. With the --role option, update the user's role to one of "user",
  100. "moderator" or "admin".
  101. With the --email option, update the user's e-mail address. With
  102. the --confirm option, mark the user's e-mail as confirmed.
  103. With the --disable option, lock the user out of their account. The
  104. --enable option is the opposite.
  105. With the --disable-2fa option, the two-factor authentication
  106. requirement for the user can be removed.
  107. LONG_DESC
  108. def modify(username)
  109. user = Account.find_local(username)&.user
  110. if user.nil?
  111. say('No user with such username', :red)
  112. exit(1)
  113. end
  114. if options[:role]
  115. user.admin = options[:role] == 'admin'
  116. user.moderator = options[:role] == 'moderator'
  117. end
  118. user.email = options[:email] if options[:email]
  119. user.disabled = false if options[:enable]
  120. user.disabled = true if options[:disable]
  121. user.otp_required_for_login = false if options[:disable_2fa]
  122. user.confirm if options[:confirm]
  123. if user.save
  124. say('OK', :green)
  125. else
  126. user.errors.to_h.each do |key, error|
  127. say('Failure/Error: ', :red)
  128. say(key)
  129. say(' ' + error, :red)
  130. end
  131. exit(1)
  132. end
  133. end
  134. desc 'delete USERNAME', 'Delete a user'
  135. long_desc <<-LONG_DESC
  136. Remove a user account with a given USERNAME.
  137. LONG_DESC
  138. def delete(username)
  139. account = Account.find_local(username)
  140. if account.nil?
  141. say('No user with such username', :red)
  142. exit(1)
  143. end
  144. say("Deleting user with #{account.statuses_count}, this might take a while...")
  145. SuspendAccountService.new.call(account, remove_user: true)
  146. say('OK', :green)
  147. end
  148. option :dry_run, type: :boolean
  149. desc 'cull', 'Remove remote accounts that no longer exist'
  150. long_desc <<-LONG_DESC
  151. Query every single remote account in the database to determine
  152. if it still exists on the origin server, and if it doesn't,
  153. remove it from the database.
  154. Accounts that have had confirmed activity within the last week
  155. are excluded from the checks.
  156. If 10 or more accounts from the same domain cannot be queried
  157. due to a connection error (such as missing DNS records) then
  158. the domain is considered dead, and all other accounts from it
  159. are deleted without further querying.
  160. With the --dry-run option, no deletes will actually be carried
  161. out.
  162. LONG_DESC
  163. def cull
  164. domain_thresholds = Hash.new { |hash, key| hash[key] = 0 }
  165. skip_threshold = 7.days.ago
  166. culled = 0
  167. dead_servers = []
  168. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  169. Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
  170. next if account.updated_at >= skip_threshold || account.last_webfingered_at >= skip_threshold
  171. unless dead_servers.include?(account.domain)
  172. begin
  173. code = Request.new(:head, account.uri).perform(&:code)
  174. rescue HTTP::ConnectionError
  175. domain_thresholds[account.domain] += 1
  176. if domain_thresholds[account.domain] >= 10
  177. dead_servers << account.domain
  178. end
  179. rescue StandardError
  180. next
  181. end
  182. end
  183. if [404, 410].include?(code) || dead_servers.include?(account.domain)
  184. unless options[:dry_run]
  185. SuspendAccountService.new.call(account)
  186. account.destroy
  187. end
  188. culled += 1
  189. say('.', :green, false)
  190. else
  191. say('.', nil, false)
  192. end
  193. end
  194. say
  195. say("Removed #{culled} accounts (#{dead_servers.size} dead servers)#{dry_run}", :green)
  196. unless dead_servers.empty?
  197. say('R.I.P.:', :yellow)
  198. dead_servers.each { |domain| say(' ' + domain) }
  199. end
  200. end
  201. option :all, type: :boolean
  202. option :domain
  203. desc 'refresh [USERNAME]', 'Fetch remote user data and files'
  204. long_desc <<-LONG_DESC
  205. Fetch remote user data and files for one or multiple accounts.
  206. With the --all option, all remote accounts will be processed.
  207. Through the --domain option, this can be narrowed down to a
  208. specific domain only. Otherwise, a single remote account must
  209. be specified with USERNAME.
  210. All processing is done in the background through Sidekiq.
  211. LONG_DESC
  212. def refresh(username = nil)
  213. if options[:domain] || options[:all]
  214. queued = 0
  215. scope = Account.remote
  216. scope = scope.where(domain: options[:domain]) if options[:domain]
  217. scope.select(:id).reorder(nil).find_in_batches do |accounts|
  218. Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts.map(&:id))
  219. queued += accounts.size
  220. end
  221. say("Scheduled refreshment of #{queued} accounts", :green, true)
  222. elsif username.present?
  223. username, domain = username.split('@')
  224. account = Account.find_remote(username, domain)
  225. if account.nil?
  226. say('No such account', :red)
  227. exit(1)
  228. end
  229. Maintenance::RedownloadAccountMediaWorker.perform_async(account.id)
  230. say('OK', :green)
  231. else
  232. say('No account(s) given', :red)
  233. exit(1)
  234. end
  235. end
  236. private
  237. def rotate_keys_for_account(account, delay = 0)
  238. if account.nil?
  239. say('No such account', :red)
  240. exit(1)
  241. end
  242. old_key = account.private_key
  243. new_key = OpenSSL::PKey::RSA.new(2048).to_pem
  244. account.update(private_key: new_key)
  245. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  246. end
  247. end
  248. end