From 76761d5fc0886e44a7a6eb94ab62aae8204d9e6e Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 17 Dec 2021 23:02:14 +0100 Subject: [PATCH] Add ability for admins to delete canonical email blocks (#16644) * Add admin option to remove canonical email blocks from a deleted account * Add tootctl canonical_email_blocks to inspect and remove canonical email blocks --- app/controllers/admin/accounts_controller.rb | 10 +++ app/models/admin/action_log_filter.rb | 1 + app/models/canonical_email_block.rb | 4 ++ app/policies/account_policy.rb | 4 ++ app/views/admin/accounts/show.html.haml | 4 +- config/locales/en.yml | 4 ++ config/routes.rb | 1 + lib/cli.rb | 4 ++ lib/mastodon/canonical_email_blocks_cli.rb | 64 +++++++++++++++++++ .../admin/accounts_controller_spec.rb | 32 ++++++++++ spec/policies/account_policy_spec.rb | 2 +- 11 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 lib/mastodon/canonical_email_blocks_cli.rb diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 948e70d5b5..0786985fac 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -117,6 +117,16 @@ module Admin redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct) end + def unblock_email + authorize @account, :unblock_email? + + CanonicalEmailBlock.where(reference_account: @account).delete_all + + log_action :unblock_email, @account + + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unblocked_email_msg', username: @account.acct) + end + private def set_account diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index d1ad465264..12136223be 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -50,6 +50,7 @@ class Admin::ActionLogFilter update_announcement: { target_type: 'Announcement', action: 'update' }.freeze, update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze, update_status: { target_type: 'Status', action: 'update' }.freeze, + unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze, }.freeze attr_reader :params diff --git a/app/models/canonical_email_block.rb b/app/models/canonical_email_block.rb index be8c45bfe3..94781386c9 100644 --- a/app/models/canonical_email_block.rb +++ b/app/models/canonical_email_block.rb @@ -24,4 +24,8 @@ class CanonicalEmailBlock < ApplicationRecord def self.block?(email) where(canonical_email_hash: email_to_canonical_email_hash(email)).exists? end + + def self.find_blocks(email) + where(canonical_email_hash: email_to_canonical_email_hash(email)) + end end diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index 672e1786bf..46237e45c1 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -64,4 +64,8 @@ class AccountPolicy < ApplicationPolicy def memorialize? admin? && !record.user&.admin? && !record.instance_actor? end + + def unblock_email? + staff? + end end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 2b6e28e8dd..64cfc9a77b 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -71,7 +71,9 @@ = t('admin.accounts.no_limits_imposed') .dashboard__counters__label= t 'admin.accounts.login_status' -- unless @account.local? && @account.user.nil? +- if @account.local? && @account.user.nil? + = link_to t('admin.accounts.unblock_email'), unblock_email_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unblock_email, @account) && CanonicalEmailBlock.where(reference_account_id: @account.id).exists? +- else .table-wrapper %table.table.inline-table %tbody diff --git a/config/locales/en.yml b/config/locales/en.yml index 080b209831..32b48dbfff 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -208,6 +208,8 @@ en: suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had. suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below. title: Accounts + unblock_email: Unblock email address + unblocked_email_msg: Successfully unblocked %{username}'s email address unconfirmed_email: Unconfirmed email undo_sensitized: Undo force-sensitive undo_silenced: Undo limit @@ -262,6 +264,7 @@ en: silence_account: Limit Account suspend_account: Suspend Account unassigned_report: Unassign Report + unblock_email_account: Unblock email address unsensitive_account: Undo Force-Sensitive Account unsilence_account: Undo Limit Account unsuspend_account: Unsuspend Account @@ -310,6 +313,7 @@ en: silence_account_html: "%{name} limited %{target}'s account" suspend_account_html: "%{name} suspended %{target}'s account" unassigned_report_html: "%{name} unassigned report %{target}" + unblock_email_account_html: "%{name} unblocked %{target}'s email address" unsensitive_account_html: "%{name} unmarked %{target}'s media as sensitive" unsilence_account_html: "%{name} undid limit of %{target}'s account" unsuspend_account_html: "%{name} unsuspended %{target}'s account" diff --git a/config/routes.rb b/config/routes.rb index b3b80624d3..2357ab6c74 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -249,6 +249,7 @@ Rails.application.routes.draw do post :memorialize post :approve post :reject + post :unblock_email end collection do diff --git a/lib/cli.rb b/lib/cli.rb index 8815e137ad..35c00e736f 100644 --- a/lib/cli.rb +++ b/lib/cli.rb @@ -13,6 +13,7 @@ require_relative 'mastodon/preview_cards_cli' require_relative 'mastodon/cache_cli' require_relative 'mastodon/upgrade_cli' require_relative 'mastodon/email_domain_blocks_cli' +require_relative 'mastodon/canonical_email_blocks_cli' require_relative 'mastodon/ip_blocks_cli' require_relative 'mastodon/maintenance_cli' require_relative 'mastodon/version' @@ -62,6 +63,9 @@ module Mastodon desc 'ip_blocks SUBCOMMAND ...ARGS', 'Manage IP blocks' subcommand 'ip_blocks', Mastodon::IpBlocksCLI + desc 'canonical_email_blocks SUBCOMMAND ...ARGS', 'Manage canonical e-mail blocks' + subcommand 'canonical_email_blocks', Mastodon::CanonicalEmailBlocksCLI + desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities' subcommand 'maintenance', Mastodon::MaintenanceCLI diff --git a/lib/mastodon/canonical_email_blocks_cli.rb b/lib/mastodon/canonical_email_blocks_cli.rb new file mode 100644 index 0000000000..64b72e6031 --- /dev/null +++ b/lib/mastodon/canonical_email_blocks_cli.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'concurrent' +require_relative '../../config/boot' +require_relative '../../config/environment' +require_relative 'cli_helper' + +module Mastodon + class CanonicalEmailBlocksCLI < Thor + include CLIHelper + + def self.exit_on_failure? + true + end + + desc 'find EMAIL', 'Find a given e-mail address in the canonical e-mail blocks' + long_desc <<-LONG_DESC + When suspending a local user, a hash of a "canonical" version of their e-mail + address is stored to prevent them from signing up again. + + This command can be used to find whether a known email address is blocked, + and if so, which account it was attached to. + LONG_DESC + def find(email) + accts = CanonicalEmailBlock.find_blocks(email).map(&:reference_account).map(&:acct).to_a + if accts.empty? + say("#{email} is not blocked", :yellow) + else + accts.each do |acct| + say(acct, :white) + end + end + end + + desc 'remove EMAIL', 'Remove a canonical e-mail block' + long_desc <<-LONG_DESC + When suspending a local user, a hash of a "canonical" version of their e-mail + address is stored to prevent them from signing up again. + + This command allows removing a canonical email block. + LONG_DESC + def remove(email) + blocks = CanonicalEmailBlock.find_blocks(email) + if blocks.empty? + say("#{email} is not blocked", :yellow) + else + blocks.destroy_all + say("Removed canonical email block for #{email}", :green) + end + end + + private + + def color(processed, failed) + if !processed.zero? && failed.zero? + :green + elsif failed.zero? + :yellow + else + :red + end + end + end +end diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index a5ef396ae4..3edbde03cb 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -192,4 +192,36 @@ RSpec.describe Admin::AccountsController, type: :controller do end end end + + describe 'POST #unblock_email' do + subject do + -> { post :unblock_email, params: { id: account.id } } + end + + let(:current_user) { Fabricate(:user, admin: admin) } + let(:account) { Fabricate(:account, suspended: true) } + let!(:email_block) { Fabricate(:canonical_email_block, reference_account: account) } + + context 'when user is admin' do + let(:admin) { true } + + it 'succeeds in removing email blocks' do + is_expected.to change { CanonicalEmailBlock.where(reference_account: account).count }.from(1).to(0) + end + + it 'redirects to admin account path' do + subject.call + expect(response).to redirect_to admin_account_path(account.id) + end + end + + context 'when user is not admin' do + let(:admin) { false } + + it 'fails to remove avatar' do + subject.call + expect(response).to have_http_status :forbidden + end + end + end end diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb index 1347ca4a03..8a5e62c06e 100644 --- a/spec/policies/account_policy_spec.rb +++ b/spec/policies/account_policy_spec.rb @@ -37,7 +37,7 @@ RSpec.describe AccountPolicy do end end - permissions :unsuspend? do + permissions :unsuspend?, :unblock_email? do before do alice.suspend! end