You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

332 lines
10 KiB

  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. account.suspended = false
  74. user.account = account
  75. if user.save
  76. if options[:confirmed]
  77. user.confirmed_at = nil
  78. user.confirm!
  79. end
  80. say('OK', :green)
  81. say("New password: #{password}")
  82. else
  83. user.errors.to_h.each do |key, error|
  84. say('Failure/Error: ', :red)
  85. say(key)
  86. say(' ' + error, :red)
  87. end
  88. exit(1)
  89. end
  90. end
  91. option :role
  92. option :email
  93. option :confirm, type: :boolean
  94. option :enable, type: :boolean
  95. option :disable, type: :boolean
  96. option :disable_2fa, type: :boolean
  97. desc 'modify USERNAME', 'Modify a user'
  98. long_desc <<-LONG_DESC
  99. Modify a user account.
  100. With the --role option, update the user's role to one of "user",
  101. "moderator" or "admin".
  102. With the --email option, update the user's e-mail address. With
  103. the --confirm option, mark the user's e-mail as confirmed.
  104. With the --disable option, lock the user out of their account. The
  105. --enable option is the opposite.
  106. With the --disable-2fa option, the two-factor authentication
  107. requirement for the user can be removed.
  108. LONG_DESC
  109. def modify(username)
  110. user = Account.find_local(username)&.user
  111. if user.nil?
  112. say('No user with such username', :red)
  113. exit(1)
  114. end
  115. if options[:role]
  116. user.admin = options[:role] == 'admin'
  117. user.moderator = options[:role] == 'moderator'
  118. end
  119. user.email = options[:email] if options[:email]
  120. user.disabled = false if options[:enable]
  121. user.disabled = true if options[:disable]
  122. user.otp_required_for_login = false if options[:disable_2fa]
  123. user.confirm if options[:confirm]
  124. if user.save
  125. say('OK', :green)
  126. else
  127. user.errors.to_h.each do |key, error|
  128. say('Failure/Error: ', :red)
  129. say(key)
  130. say(' ' + error, :red)
  131. end
  132. exit(1)
  133. end
  134. end
  135. desc 'delete USERNAME', 'Delete a user'
  136. long_desc <<-LONG_DESC
  137. Remove a user account with a given USERNAME.
  138. LONG_DESC
  139. def delete(username)
  140. account = Account.find_local(username)
  141. if account.nil?
  142. say('No user with such username', :red)
  143. exit(1)
  144. end
  145. say("Deleting user with #{account.statuses_count} statuses, this might take a while...")
  146. SuspendAccountService.new.call(account, remove_user: true)
  147. say('OK', :green)
  148. end
  149. desc 'backup USERNAME', 'Request a backup for a user'
  150. long_desc <<-LONG_DESC
  151. Request a new backup for an account with a given USERNAME.
  152. The backup will be created in Sidekiq asynchronously, and
  153. the user will receive an e-mail with a link to it once
  154. it's done.
  155. LONG_DESC
  156. def backup(username)
  157. account = Account.find_local(username)
  158. if account.nil?
  159. say('No user with such username', :red)
  160. exit(1)
  161. end
  162. backup = account.user.backups.create!
  163. BackupWorker.perform_async(backup.id)
  164. say('OK', :green)
  165. end
  166. option :dry_run, type: :boolean
  167. desc 'cull', 'Remove remote accounts that no longer exist'
  168. long_desc <<-LONG_DESC
  169. Query every single remote account in the database to determine
  170. if it still exists on the origin server, and if it doesn't,
  171. remove it from the database.
  172. Accounts that have had confirmed activity within the last week
  173. are excluded from the checks.
  174. If 10 or more accounts from the same domain cannot be queried
  175. due to a connection error (such as missing DNS records) then
  176. the domain is considered dead, and all other accounts from it
  177. are deleted without further querying.
  178. With the --dry-run option, no deletes will actually be carried
  179. out.
  180. LONG_DESC
  181. def cull
  182. domain_thresholds = Hash.new { |hash, key| hash[key] = 0 }
  183. skip_threshold = 7.days.ago
  184. culled = 0
  185. dead_servers = []
  186. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  187. Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
  188. next if account.updated_at >= skip_threshold || account.last_webfingered_at >= skip_threshold
  189. unless dead_servers.include?(account.domain)
  190. begin
  191. code = Request.new(:head, account.uri).perform(&:code)
  192. rescue HTTP::ConnectionError
  193. domain_thresholds[account.domain] += 1
  194. if domain_thresholds[account.domain] >= 10
  195. dead_servers << account.domain
  196. end
  197. rescue StandardError
  198. next
  199. end
  200. end
  201. if [404, 410].include?(code)
  202. unless options[:dry_run]
  203. SuspendAccountService.new.call(account)
  204. account.destroy
  205. end
  206. culled += 1
  207. say('.', :green, false)
  208. else
  209. say('.', nil, false)
  210. end
  211. end
  212. # Remove dead servers
  213. unless dead_servers.empty? || options[:dry_run]
  214. dead_servers.each do |domain|
  215. Account.where(domain: domain).find_each do |account|
  216. SuspendAccountService.new.call(account)
  217. account.destroy
  218. culled += 1
  219. say('.', :green, false)
  220. end
  221. end
  222. end
  223. say
  224. say("Removed #{culled} accounts (#{dead_servers.size} dead servers)#{dry_run}", :green)
  225. unless dead_servers.empty?
  226. say('R.I.P.:', :yellow)
  227. dead_servers.each { |domain| say(' ' + domain) }
  228. end
  229. end
  230. option :all, type: :boolean
  231. option :domain
  232. desc 'refresh [USERNAME]', 'Fetch remote user data and files'
  233. long_desc <<-LONG_DESC
  234. Fetch remote user data and files for one or multiple accounts.
  235. With the --all option, all remote accounts will be processed.
  236. Through the --domain option, this can be narrowed down to a
  237. specific domain only. Otherwise, a single remote account must
  238. be specified with USERNAME.
  239. All processing is done in the background through Sidekiq.
  240. LONG_DESC
  241. def refresh(username = nil)
  242. if options[:domain] || options[:all]
  243. queued = 0
  244. scope = Account.remote
  245. scope = scope.where(domain: options[:domain]) if options[:domain]
  246. scope.select(:id).reorder(nil).find_in_batches do |accounts|
  247. Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts.map(&:id))
  248. queued += accounts.size
  249. end
  250. say("Scheduled refreshment of #{queued} accounts", :green, true)
  251. elsif username.present?
  252. username, domain = username.split('@')
  253. account = Account.find_remote(username, domain)
  254. if account.nil?
  255. say('No such account', :red)
  256. exit(1)
  257. end
  258. Maintenance::RedownloadAccountMediaWorker.perform_async(account.id)
  259. say('OK', :green)
  260. else
  261. say('No account(s) given', :red)
  262. exit(1)
  263. end
  264. end
  265. private
  266. def rotate_keys_for_account(account, delay = 0)
  267. if account.nil?
  268. say('No such account', :red)
  269. exit(1)
  270. end
  271. old_key = account.private_key
  272. new_key = OpenSSL::PKey::RSA.new(2048).to_pem
  273. account.update(private_key: new_key)
  274. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  275. end
  276. end
  277. end