Add option to keep evidence when suspending accounts
Fix #547 When selected, before the account's data is removed, some of it is denormalized into a separate, symmetrically-encrypted table. In particular: - The e-mail - All IPs used to access the account - SHA256 fingerprints of all uploaded files - URIs of accounts followed by or following the account - URIs of accounts that were invited
This commit is contained in:
parent
3a6f9860fc
commit
7bf27db007
15 changed files with 234 additions and 9 deletions
142
app/services/summarize_account_service.rb
Normal file
142
app/services/summarize_account_service.rb
Normal file
|
@ -0,0 +1,142 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SummarizeAccountService < BaseService
|
||||
def call(account)
|
||||
raise ArgumentError, 'Must be a local account' unless account.user&.present?
|
||||
|
||||
@account = account
|
||||
@user = account.user
|
||||
|
||||
@sessions = []
|
||||
@following = []
|
||||
@followers = []
|
||||
@invited_by = nil
|
||||
@invitees = []
|
||||
@hashes = []
|
||||
|
||||
summarize_sessions!
|
||||
summarize_network!
|
||||
summarize_media!
|
||||
|
||||
SecureAccountSummary.create!(
|
||||
account_id: @account.id,
|
||||
summary: Oj.dump(summary_attributes)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def summary_attributes
|
||||
{
|
||||
access: {
|
||||
email: @user.email,
|
||||
sessions: @sessions.uniq,
|
||||
},
|
||||
|
||||
network: {
|
||||
following: @following,
|
||||
followers: @followers,
|
||||
inviter: @invited_by,
|
||||
invitees: @invitees,
|
||||
},
|
||||
|
||||
media: {
|
||||
fingerprints: @hashes.compact.uniq,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def summarize_sessions!
|
||||
remember_current_session!
|
||||
remember_last_session!
|
||||
remember_other_sessions!
|
||||
end
|
||||
|
||||
def summarize_network!
|
||||
remember_followers!
|
||||
remember_following!
|
||||
remember_invitees!
|
||||
remember_invited_by!
|
||||
end
|
||||
|
||||
def summarize_media!
|
||||
fingerprint_avatar!
|
||||
fingerprint_header!
|
||||
fingerprint_media_attachments!
|
||||
end
|
||||
|
||||
def remember_following!
|
||||
@account.following.find_each do |account|
|
||||
@following << account_uri(account)
|
||||
end
|
||||
end
|
||||
|
||||
def remember_followers!
|
||||
@account.followers.find_each do |account|
|
||||
@followers << account_uri(account)
|
||||
end
|
||||
end
|
||||
|
||||
def remember_invited_by!
|
||||
@invited_by = account_uri(@user.invite.user.account) if @user.invite&.user&.account&.present?
|
||||
end
|
||||
|
||||
def remember_invitees!
|
||||
@user.invites.find_each do |invite|
|
||||
invite.users.find_each do |user|
|
||||
@invitees << account_uri(user.account)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remember_current_session!
|
||||
@sessions << ip_and_timestamp(@user.current_sign_in_ip, @user.current_sign_in_at) if @user.current_sign_in_ip&.present?
|
||||
end
|
||||
|
||||
def remember_last_session!
|
||||
@sessions << ip_and_timestamp(@user.last_sign_in_ip, @user.last_sign_in_at) if @user.last_sign_in_ip&.present?
|
||||
end
|
||||
|
||||
def remember_other_sessions!
|
||||
@user.session_activations.find_each do |session_activation|
|
||||
@sessions << ip_and_timestamp(session_activation.ip, session_activation.updated_at)
|
||||
end
|
||||
end
|
||||
|
||||
def fingerprint_avatar!
|
||||
@hashes << fingerprint_attachment(@account.avatar) if @account.avatar.exists?
|
||||
end
|
||||
|
||||
def fingerprint_header!
|
||||
@hashes << fingerprint_attachment(@account.header) if @account.header.exists?
|
||||
end
|
||||
|
||||
def fingerprint_media_attachments!
|
||||
@account.media_attachments.find_each do |media_attachment|
|
||||
@hashes << fingerprint_attachment(media_attachment.file)
|
||||
end
|
||||
end
|
||||
|
||||
CHUNK_SIZE = 16.kilobytes
|
||||
|
||||
def fingerprint_attachment(attachment)
|
||||
adapter = Paperclip.io_adapters.for(attachment)
|
||||
digest = Digest::SHA256.new
|
||||
|
||||
while (buffer = adapter.read(CHUNK_SIZE))
|
||||
digest.update(buffer)
|
||||
end
|
||||
|
||||
digest.hexdigest
|
||||
rescue Errno::ENOENT, Seahorse::Client::NetworkingError
|
||||
nil
|
||||
end
|
||||
|
||||
def account_uri(account)
|
||||
ActivityPub::TagManager.instance.uri_for(account)
|
||||
end
|
||||
|
||||
def ip_and_timestamp(ip, timestamp)
|
||||
[ip&.to_s, timestamp&.iso8601]
|
||||
end
|
||||
end
|
|
@ -42,6 +42,7 @@ class SuspendAccountService < BaseService
|
|||
# @option [Boolean] :reserve_username Keep account record
|
||||
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
|
||||
# @option [Time] :suspended_at Only applicable when :reserve_username is true
|
||||
# @option [Boolean] :summarize_account Create a secure summary of access, network and media data
|
||||
def call(account, **options)
|
||||
@account = account
|
||||
@options = { reserve_username: true, reserve_email: true }.merge(options)
|
||||
|
@ -52,6 +53,7 @@ class SuspendAccountService < BaseService
|
|||
@options[:skip_side_effects] = true
|
||||
end
|
||||
|
||||
summarize_account!
|
||||
reject_follows!
|
||||
purge_user!
|
||||
purge_profile!
|
||||
|
@ -60,6 +62,12 @@ class SuspendAccountService < BaseService
|
|||
|
||||
private
|
||||
|
||||
def summarize_account!
|
||||
return unless @account.local? && @options[:summarize_account]
|
||||
|
||||
SummarizeAccountService.new.call(@account)
|
||||
end
|
||||
|
||||
def reject_follows!
|
||||
return if @account.local? || !@account.activitypub?
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue