Files
parrhesia/lib/parrhesia/storage/adapters/postgres/admin.ex

298 lines
8.9 KiB
Elixir

defmodule Parrhesia.Storage.Adapters.Postgres.Admin do
@moduledoc """
PostgreSQL-backed implementation for `Parrhesia.Storage.Admin`.
"""
import Ecto.Query
alias Parrhesia.PostgresRepos
alias Parrhesia.Repo
@behaviour Parrhesia.Storage.Admin
@default_limit 100
@max_limit 1_000
@impl true
def execute(_context, method, params) when is_map(params) do
moderation = Parrhesia.Storage.moderation()
method_name = normalize_method_name(method)
case method_name do
"ping" -> {:ok, %{"status" => "ok"}}
"stats" -> {:ok, relay_stats()}
"supportedmethods" -> {:ok, %{"methods" => supported_methods()}}
"list_audit_logs" -> list_audit_logs(%{}, audit_list_opts(params))
_other -> execute_moderation_method(moderation, method_name, params)
end
end
def execute(_context, method, _params),
do: {:error, {:unsupported_method, normalize_method_name(method)}}
@impl true
def append_audit_log(_context, audit_entry) when is_map(audit_entry) do
with {:ok, method} <- fetch_required_method(audit_entry),
{:ok, actor_pubkey} <- fetch_optional_pubkey(audit_entry),
{:ok, params} <- fetch_optional_map(audit_entry, :params),
{:ok, result} <- fetch_optional_map(audit_entry, :result, true) do
now = DateTime.utc_now() |> DateTime.truncate(:microsecond)
{inserted, _result} =
Repo.insert_all("management_audit_logs", [
audit_log_row(method, actor_pubkey, params, result, now)
])
if inserted == 1 do
:ok
else
{:error, :audit_log_insert_failed}
end
end
end
def append_audit_log(_context, _audit_entry), do: {:error, :invalid_audit_entry}
@impl true
def list_audit_logs(_context, opts) when is_list(opts) do
limit = normalize_limit(Keyword.get(opts, :limit, @default_limit))
query =
from(log in "management_audit_logs",
order_by: [desc: log.inserted_at, desc: log.id],
limit: ^limit,
select: %{
id: log.id,
actor_pubkey: log.actor_pubkey,
method: log.method,
params: log.params,
result: log.result,
inserted_at: log.inserted_at
}
)
|> maybe_filter_method(Keyword.get(opts, :method))
|> maybe_filter_actor_pubkey(Keyword.get(opts, :actor_pubkey))
logs =
read_repo()
|> then(fn repo -> repo.all(query) end)
|> Enum.map(&to_audit_log_map/1)
{:ok, logs}
end
def list_audit_logs(_context, _opts), do: {:error, :invalid_opts}
defp relay_stats do
repo = read_repo()
events_count = repo.aggregate("events", :count, :id)
banned_pubkeys = repo.aggregate("banned_pubkeys", :count, :pubkey)
allowed_pubkeys = repo.aggregate("allowed_pubkeys", :count, :pubkey)
blocked_ips = repo.aggregate("blocked_ips", :count, :ip)
acl_rules = repo.aggregate("acl_rules", :count, :id)
%{
"events" => events_count,
"banned_pubkeys" => banned_pubkeys,
"allowed_pubkeys" => allowed_pubkeys,
"acl_rules" => acl_rules,
"blocked_ips" => blocked_ips
}
end
defp supported_methods do
[
"allow_pubkey",
"ban_event",
"ban_pubkey",
"block_ip",
"disallow_pubkey",
"list_audit_logs",
"ping",
"stats",
"supportedmethods",
"unban_event",
"unban_pubkey",
"unblock_ip"
]
end
defp execute_moderation_method(moderation, "ban_pubkey", params),
do: execute_pubkey_method(fn ctx, value -> moderation.ban_pubkey(ctx, value) end, params)
defp execute_moderation_method(moderation, "unban_pubkey", params),
do: execute_pubkey_method(fn ctx, value -> moderation.unban_pubkey(ctx, value) end, params)
defp execute_moderation_method(moderation, "allow_pubkey", params),
do: execute_pubkey_method(fn ctx, value -> moderation.allow_pubkey(ctx, value) end, params)
defp execute_moderation_method(moderation, "disallow_pubkey", params),
do: execute_pubkey_method(fn ctx, value -> moderation.disallow_pubkey(ctx, value) end, params)
defp execute_moderation_method(moderation, "ban_event", params),
do: execute_event_method(fn ctx, value -> moderation.ban_event(ctx, value) end, params)
defp execute_moderation_method(moderation, "unban_event", params),
do: execute_event_method(fn ctx, value -> moderation.unban_event(ctx, value) end, params)
defp execute_moderation_method(moderation, "block_ip", params),
do: execute_ip_method(fn ctx, value -> moderation.block_ip(ctx, value) end, params)
defp execute_moderation_method(moderation, "unblock_ip", params),
do: execute_ip_method(fn ctx, value -> moderation.unblock_ip(ctx, value) end, params)
defp execute_moderation_method(_moderation, method_name, _params),
do: {:error, {:unsupported_method, method_name}}
defp audit_list_opts(params) do
[]
|> maybe_put_opt(:limit, Map.get(params, "limit"))
|> maybe_put_opt(:method, Map.get(params, "method"))
|> maybe_put_opt(:actor_pubkey, Map.get(params, "actor_pubkey"))
end
defp maybe_put_opt(opts, _key, nil), do: opts
defp maybe_put_opt(opts, key, value), do: Keyword.put(opts, key, value)
defp execute_pubkey_method(fun, params) do
case Map.get(params, "pubkey") do
pubkey when is_binary(pubkey) ->
with :ok <- fun.(%{}, pubkey) do
{:ok, %{"ok" => true}}
end
_other ->
{:error, :invalid_pubkey}
end
end
defp execute_event_method(fun, params) do
case Map.get(params, "event_id") do
event_id when is_binary(event_id) ->
with :ok <- fun.(%{}, event_id) do
{:ok, %{"ok" => true}}
end
_other ->
{:error, :invalid_event_id}
end
end
defp execute_ip_method(fun, params) do
case Map.get(params, "ip") do
ip when is_binary(ip) ->
with :ok <- fun.(%{}, ip) do
{:ok, %{"ok" => true}}
end
_other ->
{:error, :invalid_ip}
end
end
defp fetch_required_method(audit_entry) do
audit_entry
|> fetch_value(:method)
|> normalize_non_empty_string(:invalid_method)
end
defp fetch_optional_pubkey(audit_entry) do
case fetch_value(audit_entry, :actor_pubkey) do
nil -> {:ok, nil}
value -> normalize_pubkey(value)
end
end
defp fetch_optional_map(audit_entry, key, allow_nil \\ false) do
case fetch_value(audit_entry, key) do
nil when allow_nil -> {:ok, nil}
nil -> {:ok, %{}}
value when is_map(value) -> {:ok, value}
_value -> {:error, invalid_key_reason(key)}
end
end
defp fetch_value(map, key) when is_map(map) do
Map.get(map, key) || Map.get(map, Atom.to_string(key))
end
defp normalize_method_name(method) when is_atom(method), do: Atom.to_string(method)
defp normalize_method_name(method) when is_binary(method), do: method
defp normalize_method_name(method), do: inspect(method)
defp normalize_non_empty_string(value, _reason) when is_binary(value) and value != "",
do: {:ok, value}
defp normalize_non_empty_string(_value, reason), do: {:error, reason}
defp normalize_pubkey(value) when is_binary(value) and byte_size(value) == 32, do: {:ok, value}
defp normalize_pubkey(value) when is_binary(value) and byte_size(value) == 64 do
case Base.decode16(value, case: :mixed) do
{:ok, pubkey} -> {:ok, pubkey}
:error -> {:error, :invalid_actor_pubkey}
end
end
defp normalize_pubkey(_value), do: {:error, :invalid_actor_pubkey}
defp read_repo, do: PostgresRepos.read()
defp invalid_key_reason(:params), do: :invalid_params
defp invalid_key_reason(:result), do: :invalid_result
defp audit_log_row(method, actor_pubkey, params, result, inserted_at) do
%{
method: method,
actor_pubkey: actor_pubkey,
params: params,
result: result,
inserted_at: inserted_at
}
end
defp normalize_limit(limit) when is_integer(limit) and limit > 0 do
min(limit, @max_limit)
end
defp normalize_limit(_limit), do: @default_limit
defp maybe_filter_method(query, nil), do: query
defp maybe_filter_method(query, method) when is_atom(method) do
maybe_filter_method(query, Atom.to_string(method))
end
defp maybe_filter_method(query, method) when is_binary(method) and method != "" do
where(query, [log], log.method == ^method)
end
defp maybe_filter_method(query, _method), do: query
defp maybe_filter_actor_pubkey(query, nil), do: query
defp maybe_filter_actor_pubkey(query, actor_pubkey) do
case normalize_pubkey(actor_pubkey) do
{:ok, normalized_actor_pubkey} ->
where(query, [log], log.actor_pubkey == ^normalized_actor_pubkey)
{:error, _reason} ->
where(query, [log], false)
end
end
defp to_audit_log_map(log) do
%{
id: log.id,
actor_pubkey: encode_optional_hex(log.actor_pubkey),
method: log.method,
params: log.params,
result: log.result,
inserted_at: log.inserted_at
}
end
defp encode_optional_hex(nil), do: nil
defp encode_optional_hex(value), do: Base.encode16(value, case: :lower)
end