Skip to content
Permalink
Browse files

Change account suspensions to be reversible by default (#14726)

  • Loading branch information
Gargron committed Sep 15, 2020
1 parent bbcbf12 commit ed099d8bdc5b3d9e7df7ce5358441887e6bb7e48
Showing with 529 additions and 282 deletions.
  1. +20 −11 app/controllers/admin/accounts_controller.rb
  2. +2 −2 app/controllers/api/base_controller.rb
  3. +8 −1 app/controllers/api/v1/admin/accounts_controller.rb
  4. +1 −1 app/controllers/settings/deletes_controller.rb
  5. +1 −1 app/lib/activitypub/activity/delete.rb
  6. +9 −7 app/mailers/notification_mailer.rb
  7. +14 −14 app/mailers/user_mailer.rb
  8. +3 −6 app/models/account.rb
  9. +20 −0 app/models/account_deletion_request.rb
  10. +1 −1 app/models/admin/account_action.rb
  11. +3 −0 app/models/concerns/account_associations.rb
  12. +1 −1 app/models/form/account_batch.rb
  13. +1 −1 app/models/invite.rb
  14. +2 −2 app/models/user.rb
  15. +4 −0 app/policies/account_policy.rb
  16. +1 −1 app/services/after_unallow_domain_service.rb
  17. +1 −1 app/services/block_domain_service.rb
  18. +180 −0 app/services/delete_account_service.rb
  19. +30 −153 app/services/suspend_account_service.rb
  20. +52 −0 app/services/unsuspend_account_service.rb
  21. +58 −56 app/views/admin/accounts/show.html.haml
  22. +13 −0 app/workers/account_deletion_worker.rb
  23. +13 −0 app/workers/admin/account_deletion_worker.rb
  24. +4 −2 app/workers/admin/suspension_worker.rb
  25. +13 −0 app/workers/admin/unsuspension_worker.rb
  26. +13 −0 app/workers/scheduler/user_cleanup_scheduler.rb
  27. +23 −8 config/locales/en.yml
  28. +4 −4 config/locales/simple_form.en.yml
  29. +2 −2 config/routes.rb
  30. +8 −0 db/migrate/20200908193330_create_account_deletion_requests.rb
  31. +9 −1 db/schema.rb
  32. +2 −2 lib/mastodon/accounts_cli.rb
  33. +1 −1 lib/mastodon/domains_cli.rb
  34. +2 −1 spec/controllers/auth/registrations_controller_spec.rb
  35. +1 −0 spec/controllers/concerns/export_controller_concern_spec.rb
  36. +3 −0 spec/fabricators/account_deletion_request_fabricator.rb
  37. +4 −0 spec/models/account_deletion_request_spec.rb
  38. +1 −1 spec/models/invite_spec.rb
  39. +1 −1 spec/services/{suspend_account_service_spec.rb → delete_account_service_spec.rb}
@@ -2,7 +2,7 @@

module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :set_account, except: [:index]
before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]

@@ -14,49 +14,58 @@ def index
def show
authorize @account, :show?

@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom
@domain_block = DomainBlock.rule_for(@account.domain)
end

def memorialize
authorize @account, :memorialize?
@account.memorialize!
log_action :memorialize, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct)
end

def enable
authorize @account.user, :enable?
@account.user.enable!
log_action :enable, @account.user
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct)
end

def approve
authorize @account.user, :approve?
@account.user.approve!
redirect_to admin_pending_accounts_path
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
end

def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
end

def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
end

def unsilence
authorize @account, :unsilence?
@account.unsilence!
log_action :unsilence, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct)
end

def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct)
end

def redownload
@@ -65,7 +74,7 @@ def redownload
@account.update!(last_webfingered_at: nil)
ResolveAccountService.new.call(@account)

redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct)
end

def remove_avatar
@@ -76,7 +85,7 @@ def remove_avatar

log_action :remove_avatar, @account.user

redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct)
end

def remove_header
@@ -87,7 +96,7 @@ def remove_header

log_action :remove_header, @account.user

redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
end

private
@@ -96,12 +96,12 @@ def require_authenticated_user!
def require_user!
if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
elsif !current_user.functional?
render json: { error: 'Your login is currently disabled' }, status: 403
else
set_user_activity
end
@@ -58,7 +58,13 @@ def approve

def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
render json: @account, serializer: REST::Admin::AccountSerializer
end

def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
render json: @account, serializer: REST::Admin::AccountSerializer
end

@@ -72,6 +78,7 @@ def unsilence
def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account
render json: @account, serializer: REST::Admin::AccountSerializer
end
@@ -43,7 +43,7 @@ def challenge_passed?

def destroy_account!
current_account.suspend!
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
AccountDeletionWorker.perform_async(current_user.account_id)
sign_out
end
end
@@ -13,7 +13,7 @@ def perform

def delete_person
lock_or_return("delete_in_progress:#{@account.id}") do
SuspendAccountService.new.call(@account, reserve_username: false)
DeleteAccountService.new.call(@account, reserve_username: false)
end
end

@@ -10,7 +10,7 @@ def mention(recipient, notification)
@me = recipient
@status = notification.target_status

return if @me.user.disabled? || @status.nil?
return unless @me.user.functional? && @status.present?

locale_for_account(@me) do
thread_by_conversation(@status.conversation)
@@ -22,7 +22,7 @@ def follow(recipient, notification)
@me = recipient
@account = notification.from_account

return if @me.user.disabled?
return unless @me.user.functional?

locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
@@ -34,7 +34,7 @@ def favourite(recipient, notification)
@account = notification.from_account
@status = notification.target_status

return if @me.user.disabled? || @status.nil?
return unless @me.user.functional? && @status.present?

locale_for_account(@me) do
thread_by_conversation(@status.conversation)
@@ -47,7 +47,7 @@ def reblog(recipient, notification)
@account = notification.from_account
@status = notification.target_status

return if @me.user.disabled? || @status.nil?
return unless @me.user.functional? && @status.present?

locale_for_account(@me) do
thread_by_conversation(@status.conversation)
@@ -59,15 +59,15 @@ def follow_request(recipient, notification)
@me = recipient
@account = notification.from_account

return if @me.user.disabled?
return unless @me.user.functional?

locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
end
end

def digest(recipient, **opts)
return if recipient.user.disabled?
return unless recipient.user.functional?

@me = recipient
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
@@ -88,8 +88,10 @@ def digest(recipient, **opts)

def thread_by_conversation(conversation)
return if conversation.nil?

msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>"

headers['In-Reply-To'] = msg_id
headers['References'] = msg_id
headers['References'] = msg_id
end
end
@@ -15,7 +15,7 @@ def confirmation_instructions(user, token, **)
@token = token
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.unconfirmed_email.presence || @resource.email,
@@ -29,7 +29,7 @@ def reset_password_instructions(user, token, **)
@token = token
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
@@ -40,7 +40,7 @@ def password_change(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
@@ -51,7 +51,7 @@ def email_changed(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject')
@@ -62,7 +62,7 @@ def two_factor_enabled(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
@@ -73,7 +73,7 @@ def two_factor_disabled(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
@@ -84,7 +84,7 @@ def two_factor_recovery_codes_changed(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
@@ -95,7 +95,7 @@ def webauthn_enabled(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject')
@@ -106,7 +106,7 @@ def webauthn_disabled(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject')
@@ -118,7 +118,7 @@ def webauthn_credential_added(user, webauthn_credential)
@instance = Rails.configuration.x.local_domain
@webauthn_credential = webauthn_credential

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
@@ -130,7 +130,7 @@ def webauthn_credential_deleted(user, webauthn_credential)
@instance = Rails.configuration.x.local_domain
@webauthn_credential = webauthn_credential

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
@@ -141,7 +141,7 @@ def welcome(user)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
@@ -153,7 +153,7 @@ def backup_ready(user, backup)
@instance = Rails.configuration.x.local_domain
@backup = backup

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject')
@@ -181,7 +181,7 @@ def sign_in_token(user, remote_ip, user_agent, timestamp)
@detection = Browser.new(user_agent)
@timestamp = timestamp.to_time.utc

return if @resource.disabled?
return unless @resource.active_for_authentication?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email,
@@ -222,23 +222,20 @@ def suspended?

def suspend!(date = Time.now.utc)
transaction do
user&.disable! if local?
create_deletion_request!
update!(suspended_at: date)
end
end

def unsuspend!
transaction do
user&.enable! if local?
deletion_request&.destroy!
update!(suspended_at: nil)
end
end

def memorialize!
transaction do
user&.disable! if local?
update!(memorial: true)
end
update!(memorial: true)
end

def sign?
@@ -0,0 +1,20 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: account_deletion_requests
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountDeletionRequest < ApplicationRecord
DELAY_TO_DELETION = 30.days.freeze

belongs_to :account

def due_at
created_at + DELAY_TO_DELETION
end
end

0 comments on commit ed099d8

Please sign in to comment.
You can’t perform that action at this time.