Add SELF_DESTRUCT env variable to process self-destructions in the background (#26439)

This commit is contained in:
Claire 2023-10-23 17:46:21 +02:00 committed by GitHub
parent 26d2a2a0cc
commit 379115e601
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 193 additions and 57 deletions

View file

@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
include DomainControlHelper
include DatabaseHelper
include AuthorizedFetchHelper
include SelfDestructHelper
helper_method :current_account
helper_method :current_session
@ -39,6 +40,8 @@ class ApplicationController < ActionController::Base
service_unavailable
end
before_action :check_self_destruct!
before_action :store_referrer, except: :raise_not_found, if: :devise_controller?
before_action :require_functional!, if: :user_signed_in?
@ -170,6 +173,15 @@ class ApplicationController < ActionController::Base
end
end
def check_self_destruct!
return unless self_destruct?
respond_to do |format|
format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] }
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code }
end
end
def set_cache_control_defaults
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -7,6 +7,7 @@ class Auth::ChallengesController < ApplicationController
before_action :authenticate_user!
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
def create

View file

@ -12,6 +12,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
before_action :require_captcha_if_needed!, only: [:show]
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
def show

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :check_self_destruct!
skip_before_action :verify_authenticity_token
def self.provides_callback_for(provider)

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class Auth::PasswordsController < Devise::PasswordsController
skip_before_action :check_self_destruct!
before_action :check_validity_of_reset_password_token, only: :edit
before_action :set_body_classes

View file

@ -17,6 +17,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :require_rules_acceptance!, only: :new
before_action :set_registration_form_time, only: :new
skip_before_action :check_self_destruct!, only: [:edit, :update]
skip_before_action :require_functional!, only: [:edit, :update]
def new

View file

@ -3,6 +3,7 @@
class Auth::SessionsController < Devise::SessionsController
layout 'auth'
skip_before_action :check_self_destruct!
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :require_functional!
skip_before_action :update_user_sign_in

View file

@ -3,6 +3,7 @@
class BackupsController < ApplicationController
include RoutingHelper
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
before_action :authenticate_user!

View file

@ -7,6 +7,7 @@ module ExportControllerConcern
before_action :authenticate_user!
before_action :load_export
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
end

View file

@ -5,6 +5,7 @@ class Settings::ExportsController < Settings::BaseController
include Redisable
include Lockable
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
def show

View file

@ -1,6 +1,9 @@
# frozen_string_literal: true
class Settings::LoginActivitiesController < Settings::BaseController
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
def index
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
end

View file

@ -3,6 +3,7 @@
module Settings
module TwoFactorAuthentication
class WebauthnCredentialsController < BaseController
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
before_action :require_otp_enabled

View file

@ -4,6 +4,7 @@ module Settings
class TwoFactorAuthenticationMethodsController < BaseController
include ChallengableConcern
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
before_action :require_challenge!, only: :disable

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
module SelfDestructHelper
def self.self_destruct?
value = ENV.fetch('SELF_DESTRUCT', nil)
value.present? && Rails.application.message_verifier('self-destruct').verify(value) == ENV['LOCAL_DOMAIN']
rescue ActiveSupport::MessageVerifier::InvalidSignature
false
end
def self_destruct?
SelfDestructHelper.self_destruct?
end
end

View file

@ -1,7 +1,11 @@
- content_for :page_title do
= t('settings.account_settings')
= render partial: 'status', locals: { user: @user, strikes: @strikes }
- if self_destruct?
.flash-message.warning
= t('auth.status.self_destruct', domain: ENV['LOCAL_DOMAIN'])
- else
= render partial: 'status', locals: { user: @user, strikes: @strikes }
%h3= t('auth.security')
@ -32,7 +36,7 @@
= render partial: 'sessions', object: @sessions
- unless current_account.suspended?
- unless current_account.suspended? || self_destruct?
%hr.spacer/
%h3= t('auth.migrate_account')

View file

@ -0,0 +1,20 @@
- content_for :page_title do
= t('self_destruct.title')
.simple_form
%h1.title= t('self_destruct.title')
%p.lead= t('self_destruct.lead_html', domain: ENV['LOCAL_DOMAIN'])
.form-footer
%ul.no-list
- if user_signed_in?
%li= link_to t('settings.account_settings'), edit_user_registration_path
- else
- if controller_name != 'sessions'
%li= link_to_login t('auth.login')
- if controller_name != 'passwords' && controller_name != 'registrations'
%li= link_to t('auth.forgot_password'), new_user_password_path
- if user_signed_in?
%li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }

View file

@ -0,0 +1,72 @@
# frozen_string_literal: true
class Scheduler::SelfDestructScheduler
include Sidekiq::Worker
include SelfDestructHelper
MAX_ENQUEUED = 10_000
MAX_REDIS_MEM_USAGE = 0.5
MAX_ACCOUNT_DELETIONS_PER_JOB = 50
sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i
def perform
return unless self_destruct?
return if sidekiq_overwhelmed?
delete_accounts!
end
private
def sidekiq_overwhelmed?
redis_mem_info = Sidekiq.redis_info
Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > redis_mem_info['total_system_memory'].to_f * MAX_REDIS_MEM_USAGE
end
def delete_accounts!
# We currently do not distinguish between deleted accounts and suspended
# accounts, and we do not want to remove the records in this scheduler, as
# we still rely on it for account delivery and don't want to perform
# needless work when the database can be outright dropped after the
# self-destruct.
# Deleted accounts are suspended accounts that do not have a pending
# deletion request.
# This targets accounts that have not been deleted nor marked for deletion yet
Account.local.without_suspended.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
delete_account!(account)
end
return if sidekiq_overwhelmed?
# This targets accounts that have been marked for deletion but have not been
# deleted yet
Account.local.suspended.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
delete_account!(account)
account.deletion_request&.destroy
end
end
def inboxes
@inboxes ||= Account.inboxes
end
def delete_account!(account)
payload = ActiveModelSerializers::SerializableResource.new(
account,
serializer: ActivityPub::DeleteActorSerializer,
adapter: ActivityPub::Adapter
).as_json
json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url|
[json, account.id, inbox_url]
end
# Do not call `Account#suspend!` because we don't want to issue a deletion request
account.update!(suspended_at: Time.now.utc, suspension_origin: :local)
end
end