mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
Bans logic cleanup
This commit is contained in:
parent
dbbc067679
commit
318ef681de
6 changed files with 157 additions and 130 deletions
|
@ -4,13 +4,17 @@ defmodule Philomena.Bans do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
|
alias Ecto.Multi
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
|
|
||||||
alias Philomena.UserIps
|
alias Philomena.Bans.Finder
|
||||||
alias Philomena.Bans.Fingerprint
|
alias Philomena.Bans.Fingerprint
|
||||||
|
alias Philomena.Bans.SubnetCreator
|
||||||
|
alias Philomena.Bans.Subnet
|
||||||
|
alias Philomena.Bans.User
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of fingerprint_bans.
|
Returns the list of fingerprint bans.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -23,9 +27,9 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single fingerprint.
|
Gets a single fingerprint ban.
|
||||||
|
|
||||||
Raises `Ecto.NoResultsError` if the Fingerprint does not exist.
|
Raises `Ecto.NoResultsError` if the fingerprint ban does not exist.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -39,7 +43,7 @@ defmodule Philomena.Bans do
|
||||||
def get_fingerprint!(id), do: Repo.get!(Fingerprint, id)
|
def get_fingerprint!(id), do: Repo.get!(Fingerprint, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a fingerprint.
|
Creates a fingerprint ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -57,7 +61,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates a fingerprint.
|
Updates a fingerprint ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -75,7 +79,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a Fingerprint.
|
Deletes a fingerprint ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -91,7 +95,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking fingerprint changes.
|
Returns an `%Ecto.Changeset{}` for tracking fingerprint ban changes.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -103,10 +107,8 @@ defmodule Philomena.Bans do
|
||||||
Fingerprint.changeset(fingerprint, %{})
|
Fingerprint.changeset(fingerprint, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
alias Philomena.Bans.Subnet
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of subnet_bans.
|
Returns the list of subnet bans.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -119,9 +121,9 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single subnet.
|
Gets a single subnet ban.
|
||||||
|
|
||||||
Raises `Ecto.NoResultsError` if the Subnet does not exist.
|
Raises `Ecto.NoResultsError` if the subnet ban does not exist.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -135,7 +137,7 @@ defmodule Philomena.Bans do
|
||||||
def get_subnet!(id), do: Repo.get!(Subnet, id)
|
def get_subnet!(id), do: Repo.get!(Subnet, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a subnet.
|
Creates a subnet ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -153,7 +155,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates a subnet.
|
Updates a subnet ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -171,7 +173,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a Subnet.
|
Deletes a subnet ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -187,7 +189,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking subnet changes.
|
Returns an `%Ecto.Changeset{}` for tracking subnet ban changes.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -199,10 +201,8 @@ defmodule Philomena.Bans do
|
||||||
Subnet.changeset(subnet, %{})
|
Subnet.changeset(subnet, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
alias Philomena.Bans.User
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of user_bans.
|
Returns the list of user bans.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -215,9 +215,9 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single user.
|
Gets a single user ban.
|
||||||
|
|
||||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
Raises `Ecto.NoResultsError` if the user ban does not exist.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -231,7 +231,7 @@ defmodule Philomena.Bans do
|
||||||
def get_user!(id), do: Repo.get!(User, id)
|
def get_user!(id), do: Repo.get!(User, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a user.
|
Creates a user ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -243,31 +243,27 @@ defmodule Philomena.Bans do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def create_user(creator, attrs \\ %{}) do
|
def create_user(creator, attrs \\ %{}) do
|
||||||
%User{banning_user_id: creator.id}
|
changeset =
|
||||||
|> User.save_changeset(attrs)
|
%User{banning_user_id: creator.id}
|
||||||
|> Repo.insert()
|
|> User.save_changeset(attrs)
|
||||||
|
|
||||||
|
Multi.new()
|
||||||
|
|> Multi.insert(:user_ban, changeset)
|
||||||
|
|> Multi.run(:subnet_ban, fn _repo, %{user_ban: %{user_id: user_id}} ->
|
||||||
|
SubnetCreator.create_for_user(creator, user_id, attrs)
|
||||||
|
end)
|
||||||
|
|> Repo.transaction()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, user_ban} ->
|
{:ok, %{user_ban: user_ban}} ->
|
||||||
ip = UserIps.get_ip_for_user(user_ban.user_id)
|
|
||||||
|
|
||||||
if ip do
|
|
||||||
# Automatically create associated IP ban.
|
|
||||||
ip = UserIps.masked_ip(ip)
|
|
||||||
|
|
||||||
%Subnet{banning_user_id: creator.id, specification: ip}
|
|
||||||
|> Subnet.save_changeset(attrs)
|
|
||||||
|> Repo.insert()
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, user_ban}
|
{:ok, user_ban}
|
||||||
|
|
||||||
error ->
|
{:error, :user_ban, changeset, _changes} ->
|
||||||
error
|
{:error, changeset}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates a user.
|
Updates a user ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -285,7 +281,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a User.
|
Deletes a user ban.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -301,7 +297,7 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
Returns an `%Ecto.Changeset{}` for tracking user ban changes.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -314,88 +310,9 @@ defmodule Philomena.Bans do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the first ban, if any, that matches the specified request
|
Returns the first ban, if any, that matches the specified request attributes.
|
||||||
attributes.
|
|
||||||
"""
|
"""
|
||||||
def exists_for?(user, ip, fingerprint) do
|
def find(user, ip, fingerprint) do
|
||||||
now = DateTime.utc_now()
|
Finder.find(user, ip, fingerprint)
|
||||||
|
|
||||||
queries =
|
|
||||||
subnet_query(ip, now) ++
|
|
||||||
fingerprint_query(fingerprint, now) ++
|
|
||||||
user_query(user, now)
|
|
||||||
|
|
||||||
bans =
|
|
||||||
queries
|
|
||||||
|> union_all_queries()
|
|
||||||
|> Repo.all()
|
|
||||||
|
|
||||||
# Don't return a ban if the user is currently signed in.
|
|
||||||
case is_nil(user) do
|
|
||||||
true -> Enum.at(bans, 0)
|
|
||||||
false -> user_ban(bans)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fingerprint_query(nil, _now), do: []
|
|
||||||
|
|
||||||
defp fingerprint_query(fingerprint, now) do
|
|
||||||
[
|
|
||||||
Fingerprint
|
|
||||||
|> select([f], %{
|
|
||||||
reason: f.reason,
|
|
||||||
valid_until: f.valid_until,
|
|
||||||
generated_ban_id: f.generated_ban_id,
|
|
||||||
type: ^"FingerprintBan"
|
|
||||||
})
|
|
||||||
|> where([f], f.enabled and f.valid_until > ^now)
|
|
||||||
|> where([f], f.fingerprint == ^fingerprint)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp subnet_query(nil, _now), do: []
|
|
||||||
|
|
||||||
defp subnet_query(ip, now) do
|
|
||||||
{:ok, inet} = EctoNetwork.INET.cast(ip)
|
|
||||||
|
|
||||||
[
|
|
||||||
Subnet
|
|
||||||
|> select([s], %{
|
|
||||||
reason: s.reason,
|
|
||||||
valid_until: s.valid_until,
|
|
||||||
generated_ban_id: s.generated_ban_id,
|
|
||||||
type: ^"SubnetBan"
|
|
||||||
})
|
|
||||||
|> where([s], s.enabled and s.valid_until > ^now)
|
|
||||||
|> where(fragment("specification >>= ?", ^inet))
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp user_query(nil, _now), do: []
|
|
||||||
|
|
||||||
defp user_query(user, now) do
|
|
||||||
[
|
|
||||||
User
|
|
||||||
|> select([u], %{
|
|
||||||
reason: u.reason,
|
|
||||||
valid_until: u.valid_until,
|
|
||||||
generated_ban_id: u.generated_ban_id,
|
|
||||||
type: ^"UserBan"
|
|
||||||
})
|
|
||||||
|> where([u], u.enabled and u.valid_until > ^now)
|
|
||||||
|> where([u], u.user_id == ^user.id)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp union_all_queries([query]),
|
|
||||||
do: query
|
|
||||||
|
|
||||||
defp union_all_queries([query | rest]),
|
|
||||||
do: query |> union_all(^union_all_queries(rest))
|
|
||||||
|
|
||||||
defp user_ban(bans) do
|
|
||||||
bans
|
|
||||||
|> Enum.filter(&(&1.type == "UserBan"))
|
|
||||||
|> Enum.at(0)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
86
lib/philomena/bans/finder.ex
Normal file
86
lib/philomena/bans/finder.ex
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
defmodule Philomena.Bans.Finder do
|
||||||
|
@moduledoc """
|
||||||
|
Helper to find a bans associated with a set of request attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
alias Philomena.Repo
|
||||||
|
|
||||||
|
alias Philomena.Bans.Fingerprint
|
||||||
|
alias Philomena.Bans.Subnet
|
||||||
|
alias Philomena.Bans.User
|
||||||
|
|
||||||
|
@fingerprint "Fingerprint"
|
||||||
|
@subnet "Subnet"
|
||||||
|
@user "User"
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the first ban, if any, that matches the specified request attributes.
|
||||||
|
"""
|
||||||
|
def find(user, ip, fingerprint) do
|
||||||
|
bans =
|
||||||
|
generate_valid_queries([
|
||||||
|
{ip, &subnet_query/2},
|
||||||
|
{fingerprint, &fingerprint_query/2},
|
||||||
|
{user, &user_query/2}
|
||||||
|
])
|
||||||
|
|> union_all_queries()
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
|
# Don't return a fingerprint or subnet ban if the user is currently signed in.
|
||||||
|
case is_nil(user) do
|
||||||
|
true -> Enum.at(bans, 0)
|
||||||
|
false -> user_ban(bans)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp query_base(schema, name, now) do
|
||||||
|
from b in schema,
|
||||||
|
where: b.enabled and b.valid_until > ^now,
|
||||||
|
select: %{
|
||||||
|
reason: b.reason,
|
||||||
|
valid_until: b.valid_until,
|
||||||
|
generated_ban_id: b.generated_ban_id,
|
||||||
|
type: type(^name, :string)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fingerprint_query(fingerprint, now) do
|
||||||
|
Fingerprint
|
||||||
|
|> query_base(@fingerprint, now)
|
||||||
|
|> where([f], f.fingerprint == ^fingerprint)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp subnet_query(ip, now) do
|
||||||
|
{:ok, inet} = EctoNetwork.INET.cast(ip)
|
||||||
|
|
||||||
|
Subnet
|
||||||
|
|> query_base(@subnet, now)
|
||||||
|
|> where(fragment("specification >>= ?", ^inet))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp user_query(user, now) do
|
||||||
|
User
|
||||||
|
|> query_base(@user, now)
|
||||||
|
|> where([u], u.user_id == ^user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp generate_valid_queries(sources) do
|
||||||
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
|
Enum.flat_map(sources, fn
|
||||||
|
{nil, _cb} -> []
|
||||||
|
{source, cb} -> [cb.(source, now)]
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp union_all_queries([query | rest]) do
|
||||||
|
Enum.reduce(rest, query, fn q, acc -> union_all(acc, ^q) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp user_ban(bans) do
|
||||||
|
bans
|
||||||
|
|> Enum.filter(&(&1.type == @user))
|
||||||
|
|> Enum.at(0)
|
||||||
|
end
|
||||||
|
end
|
27
lib/philomena/bans/subnet_creator.ex
Normal file
27
lib/philomena/bans/subnet_creator.ex
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Philomena.Bans.SubnetCreator do
|
||||||
|
@moduledoc """
|
||||||
|
Handles automatic creation of subnet bans for an input user ban.
|
||||||
|
|
||||||
|
This prevents trivial ban evasion with the creation of a new account from the same address.
|
||||||
|
The user must work around or wait out the subnet ban first.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Philomena.UserIps
|
||||||
|
alias Philomena.Bans
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a subnet ban for the given user's last known IP address.
|
||||||
|
|
||||||
|
Returns `{:ok, ban}`, `{:ok, nil}`, or `{:error, changeset}`. The return value is
|
||||||
|
suitable for use as the return value to an `Ecto.Multi.run/3` callback.
|
||||||
|
"""
|
||||||
|
def create_for_user(creator, user_id, attrs) do
|
||||||
|
ip = UserIps.get_ip_for_user(user_id)
|
||||||
|
|
||||||
|
if ip do
|
||||||
|
Bans.create_subnet(creator, Map.put(attrs, "specification", UserIps.masked_ip(ip)))
|
||||||
|
else
|
||||||
|
{:ok, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -53,9 +53,6 @@ defmodule PhilomenaWeb.Admin.UserBanController do
|
||||||
|> moderation_log(details: &log_details/2, data: user_ban)
|
|> moderation_log(details: &log_details/2, data: user_ban)
|
||||||
|> redirect(to: ~p"/admin/user_bans")
|
|> redirect(to: ~p"/admin/user_bans")
|
||||||
|
|
||||||
{:error, :user_ban, changeset, _changes} ->
|
|
||||||
render(conn, "new.html", changeset: changeset)
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
render(conn, "new.html", changeset: changeset)
|
render(conn, "new.html", changeset: changeset)
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,7 @@ defmodule PhilomenaWeb.ApiRequireAuthorizationPlug do
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> maybe_unauthorized(user)
|
|> maybe_unauthorized(user)
|
||||||
|> maybe_forbidden(Bans.exists_for?(user, conn.remote_ip, "NOTAPI"))
|
|> maybe_forbidden(Bans.find(user, conn.remote_ip, "NOTAPI"))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_unauthorized(conn, nil) do
|
defp maybe_unauthorized(conn, nil) do
|
||||||
|
|
|
@ -20,7 +20,7 @@ defmodule PhilomenaWeb.CurrentBanPlug do
|
||||||
user = conn.assigns.current_user
|
user = conn.assigns.current_user
|
||||||
ip = conn.remote_ip
|
ip = conn.remote_ip
|
||||||
|
|
||||||
ban = Bans.exists_for?(user, ip, fingerprint)
|
ban = Bans.find(user, ip, fingerprint)
|
||||||
|
|
||||||
Conn.assign(conn, :current_ban, ban)
|
Conn.assign(conn, :current_ban, ban)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue