Merge pull request #334 from philomena-dev/user-eraser

Add more thorough user change eraser
This commit is contained in:
liamwhite 2024-07-19 17:36:47 -04:00 committed by GitHub
commit 7e92d5a734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 251 additions and 0 deletions

View file

@ -18,6 +18,7 @@ defmodule Philomena.Users do
alias Philomena.Galleries
alias Philomena.Reports
alias Philomena.Filters
alias Philomena.UserEraseWorker
alias Philomena.UserRenameWorker
## Database getters
@ -683,6 +684,20 @@ defmodule Philomena.Users do
|> Repo.update()
end
def erase_user(%User{} = user, %User{} = moderator) do
# Deactivate to prevent the user from racing these changes
{:ok, user} = deactivate_user(moderator, user)
# Rename to prevent usage for brand recognition SEO
random_hex = Base.encode16(:crypto.strong_rand_bytes(16), case: :lower)
{:ok, user} = update_user(user, %{name: "deactivated_#{random_hex}"})
# Enqueue a background job to perform the rest of the deletion
Exq.enqueue(Exq, "indexing", UserEraseWorker, [user.id, moderator.id])
{:ok, user}
end
defp setup_roles(nil), do: nil
defp setup_roles(user) do

View file

@ -0,0 +1,125 @@
defmodule Philomena.Users.Eraser do
import Ecto.Query
alias Philomena.Repo
alias Philomena.Bans
alias Philomena.Comments.Comment
alias Philomena.Comments
alias Philomena.Galleries.Gallery
alias Philomena.Galleries
alias Philomena.Posts.Post
alias Philomena.Posts
alias Philomena.Topics.Topic
alias Philomena.Topics
alias Philomena.Images
alias Philomena.SourceChanges.SourceChange
alias Philomena.Users
@reason "Site abuse"
@wipe_ip %Postgrex.INET{address: {127, 0, 1, 1}, netmask: 32}
@wipe_fp "ffff"
def erase_permanently!(user, moderator) do
# Erase avatar
{:ok, user} = Users.remove_avatar(user)
# Erase "about me" and personal title
{:ok, user} = Users.update_description(user, %{description: "", personal_title: ""})
# Delete all forum posts
Post
|> where(user_id: ^user.id)
|> Repo.all()
|> Enum.each(fn post ->
{:ok, post} = Posts.hide_post(post, %{deletion_reason: @reason}, moderator)
{:ok, _post} = Posts.destroy_post(post)
end)
# Delete all comments
Comment
|> where(user_id: ^user.id)
|> Repo.all()
|> Enum.each(fn comment ->
{:ok, comment} = Comments.hide_comment(comment, %{deletion_reason: @reason}, moderator)
{:ok, _comment} = Comments.destroy_comment(comment)
end)
# Delete all galleries
Gallery
|> where(creator_id: ^user.id)
|> Repo.all()
|> Enum.each(fn gallery ->
{:ok, _gallery} = Galleries.delete_gallery(gallery)
end)
# Delete all posted topics
Topic
|> where(user_id: ^user.id)
|> Repo.all()
|> Enum.each(fn topic ->
{:ok, _topic} = Topics.hide_topic(topic, @reason, moderator)
end)
# Revert all source changes
SourceChange
|> where(user_id: ^user.id)
|> order_by(desc: :created_at)
|> preload(:image)
|> Repo.all()
|> Enum.each(fn source_change ->
if source_change.added do
revert_added_source_change(source_change, user)
else
revert_removed_source_change(source_change, user)
end
end)
# Delete all source changes
SourceChange
|> where(user_id: ^user.id)
|> Repo.delete_all()
# Ban the user
{:ok, _ban} =
Bans.create_user(
moderator,
%{
"user_id" => user.id,
"reason" => @reason,
"valid_until" => "permanent"
}
)
# We succeeded
:ok
end
defp revert_removed_source_change(source_change, user) do
old_sources = %{}
new_sources = %{"0" => %{"source" => source_change.source_url}}
revert_source_change(source_change, user, old_sources, new_sources)
end
defp revert_added_source_change(source_change, user) do
old_sources = %{"0" => %{"source" => source_change.source_url}}
new_sources = %{}
revert_source_change(source_change, user, old_sources, new_sources)
end
defp revert_source_change(source_change, user, old_sources, new_sources) do
attrs = %{"old_sources" => old_sources, "sources" => new_sources}
attribution = [
user: user,
ip: @wipe_ip,
fingerprint: @wipe_fp,
user_agent: "",
referrer: ""
]
{:ok, _} = Images.update_sources(source_change.image, attribution, attrs)
end
end

View file

@ -0,0 +1,11 @@
defmodule Philomena.UserEraseWorker do
alias Philomena.Users.Eraser
alias Philomena.Users
def perform(user_id, moderator_id) do
moderator = Users.get_user!(moderator_id)
user = Users.get_user!(user_id)
Eraser.erase_permanently!(user, moderator)
end
end

View file

@ -0,0 +1,74 @@
defmodule PhilomenaWeb.Admin.User.EraseController do
use PhilomenaWeb, :controller
alias Philomena.Users.User
alias Philomena.Users
plug :verify_authorized
plug :load_resource,
model: User,
id_name: "user_id",
id_field: "slug",
persisted: true,
preload: [:roles]
plug :prevent_deleting_privileged_users
plug :prevent_deleting_verified_users
plug :prevent_deleting_old_users
def new(conn, _params) do
render(conn, "new.html", title: "Erase user")
end
def create(conn, _params) do
{:ok, user} = Users.erase_user(conn.assigns.user, conn.assigns.current_user)
conn
|> put_flash(:info, "User erase started")
|> redirect(to: ~p"/profiles/#{user}")
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, User) do
true -> conn
_false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
defp prevent_deleting_privileged_users(conn, _opts) do
if conn.assigns.user.role != "user" do
conn
|> put_flash(:error, "Cannot erase a privileged user")
|> redirect(to: ~p"/profiles/#{conn.assigns.user}")
|> Plug.Conn.halt()
else
conn
end
end
defp prevent_deleting_verified_users(conn, _opts) do
if conn.assigns.user.verified do
conn
|> put_flash(:error, "Cannot erase a verified user")
|> redirect(to: ~p"/profiles/#{conn.assigns.user}")
|> Plug.Conn.halt()
else
conn
end
end
defp prevent_deleting_old_users(conn, _opts) do
now = DateTime.utc_now(:second)
two_weeks = 1_209_600
if DateTime.compare(now, DateTime.add(conn.assigns.user.created_at, two_weeks)) == :gt do
conn
|> put_flash(:error, "Cannot erase a user older than two weeks")
|> redirect(to: ~p"/profiles/#{conn.assigns.user}")
|> Plug.Conn.halt()
else
conn
end
end
end

View file

@ -398,6 +398,7 @@ defmodule PhilomenaWeb.Router do
singleton: true
resources "/unlock", User.UnlockController, only: [:create], singleton: true
resources "/erase", User.EraseController, only: [:new, :create], singleton: true
resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true
resources "/downvotes", User.DownvoteController, only: [:delete], singleton: true
resources "/votes", User.VoteController, only: [:delete], singleton: true

View file

@ -0,0 +1,16 @@
h1
' Deleting all changes for user
= @user.name
.block.block--fixed.block--warning
p This is IRREVERSIBLE.
p All user details will be destroyed.
p Are you really sure?
.field
=> button_to "Abort", ~p"/profiles/#{@user}", class: "button"
=> button_to "Erase user", ~p"/admin/users/#{@user}/erase", method: "post", class: "button button--state-danger", data: [confirm: "Are you really, really sure?"]
p
' This automatically creates user and IP bans but does not create a fingerprint ban.
' Check to see if one is necessary after erasing.

View file

@ -171,6 +171,12 @@ a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__optio
i.fa.fa-fw.fa-arrow-down
span.admin__button Remove All Downvotes
= if @user.role == "user" do
li
= link to: ~p"/admin/users/#{@user}/erase/new", data: [confirm: "Are you really, really sure?"] do
i.fa.fa-fw.fa-warning
span.admin__button Erase for spam
= if @user.role == "user" and can?(@conn, :revert, Philomena.TagChanges.TagChange) do
li
= link to: ~p"/tag_changes/full_revert?#{[user_id: @user.id]}", data: [confirm: "Are you really, really sure?", method: "create"] do

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.Admin.User.EraseView do
use PhilomenaWeb, :view
end