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.

319 lines
9.7 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. 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} statuses, this might take a while...")
  145. SuspendAccountService.new.call(account, remove_user: true)
  146. say('OK', :green)
  147. end
  148. desc 'backup USERNAME', 'Request a backup for a user'
  149. long_desc <<-LONG_DESC
  150. Request a new backup for an account with a given USERNAME.
  151. The backup will be created in Sidekiq asynchronously, and
  152. the user will receive an e-mail with a link to it once
  153. it's done.
  154. LONG_DESC
  155. def backup(username)
  156. account = Account.find_local(username)
  157. if account.nil?
  158. say('No user with such username', :red)
  159. exit(1)
  160. end
  161. backup = account.user.backups.create!
  162. BackupWorker.perform_async(backup.id)
  163. say('OK', :green)
  164. end
  165. option :dry_run, type: :boolean
  166. desc 'cull', 'Remove remote accounts that no longer exist'
  167. long_desc <<-LONG_DESC
  168. Query every single remote account in the database to determine
  169. if it still exists on the origin server, and if it doesn't,
  170. remove it from the database.
  171. Accounts that have had confirmed activity within the last week
  172. are excluded from the checks.
  173. If 10 or more accounts from the same domain cannot be queried
  174. due to a connection error (such as missing DNS records) then
  175. the domain is considered dead, and all other accounts from it
  176. are deleted without further querying.
  177. With the --dry-run option, no deletes will actually be carried
  178. out.
  179. LONG_DESC
  180. def cull
  181. domain_thresholds = Hash.new { |hash, key| hash[key] = 0 }
  182. skip_threshold = 7.days.ago
  183. culled = 0
  184. dead_servers = []
  185. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  186. Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
  187. next if account.updated_at >= skip_threshold || account.last_webfingered_at >= skip_threshold
  188. unless dead_servers.include?(account.domain)
  189. begin
  190. code = Request.new(:head, account.uri).perform(&:code)
  191. rescue HTTP::ConnectionError
  192. domain_thresholds[account.domain] += 1
  193. if domain_thresholds[account.domain] >= 10
  194. dead_servers << account.domain
  195. end
  196. rescue StandardError
  197. next
  198. end
  199. end
  200. if [404, 410].include?(code) || dead_servers.include?(account.domain)
  201. unless options[:dry_run]
  202. SuspendAccountService.new.call(account)
  203. account.destroy
  204. end
  205. culled += 1
  206. say('.', :green, false)
  207. else
  208. say('.', nil, false)
  209. end
  210. end
  211. say
  212. say("Removed #{culled} accounts (#{dead_servers.size} dead servers)#{dry_run}", :green)
  213. unless dead_servers.empty?
  214. say('R.I.P.:', :yellow)
  215. dead_servers.each { |domain| say(' ' + domain) }
  216. end
  217. end
  218. option :all, type: :boolean
  219. option :domain
  220. desc 'refresh [USERNAME]', 'Fetch remote user data and files'
  221. long_desc <<-LONG_DESC
  222. Fetch remote user data and files for one or multiple accounts.
  223. With the --all option, all remote accounts will be processed.
  224. Through the --domain option, this can be narrowed down to a
  225. specific domain only. Otherwise, a single remote account must
  226. be specified with USERNAME.
  227. All processing is done in the background through Sidekiq.
  228. LONG_DESC
  229. def refresh(username = nil)
  230. if options[:domain] || options[:all]
  231. queued = 0
  232. scope = Account.remote
  233. scope = scope.where(domain: options[:domain]) if options[:domain]
  234. scope.select(:id).reorder(nil).find_in_batches do |accounts|
  235. Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts.map(&:id))
  236. queued += accounts.size
  237. end
  238. say("Scheduled refreshment of #{queued} accounts", :green, true)
  239. elsif username.present?
  240. username, domain = username.split('@')
  241. account = Account.find_remote(username, domain)
  242. if account.nil?
  243. say('No such account', :red)
  244. exit(1)
  245. end
  246. Maintenance::RedownloadAccountMediaWorker.perform_async(account.id)
  247. say('OK', :green)
  248. else
  249. say('No account(s) given', :red)
  250. exit(1)
  251. end
  252. end
  253. private
  254. def rotate_keys_for_account(account, delay = 0)
  255. if account.nil?
  256. say('No such account', :red)
  257. exit(1)
  258. end
  259. old_key = account.private_key
  260. new_key = OpenSSL::PKey::RSA.new(2048).to_pem
  261. account.update(private_key: new_key)
  262. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  263. end
  264. end
  265. end