# Pleroma: A lightweight social networking server # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, only: [json_response: 3] alias Pleroma.Activity alias Pleroma.Config alias Pleroma.ConfigDB alias Pleroma.MFA alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.ReportNote alias Pleroma.Stats alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint alias Pleroma.Web.MastodonAPI alias Pleroma.Web.MastodonAPI.AppView alias Pleroma.Web.OAuth.App alias Pleroma.Web.Router require Logger @descriptions Pleroma.Docs.JSON.compile() @users_page_size 50 plug( OAuthScopesPlug, %{scopes: ["read:accounts"], admin: true} when action in [:list_users, :user_show, :right_get, :show_user_credentials] ) plug( OAuthScopesPlug, %{scopes: ["write:accounts"], admin: true} when action in [ :get_password_reset, :force_password_reset, :user_delete, :users_create, :user_toggle_activation, :user_activate, :user_deactivate, :tag_users, :untag_users, :right_add, :right_add_multiple, :right_delete, :disable_mfa, :right_delete_multiple, :update_user_credentials ] ) plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites) plug( OAuthScopesPlug, %{scopes: ["write:invites"], admin: true} when action in [:create_invite_token, :revoke_invite, :email_invite] ) plug( OAuthScopesPlug, %{scopes: ["write:follows"], admin: true} when action in [:user_follow, :user_unfollow] ) plug( OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} when action in [:list_reports, :report_show] ) plug( OAuthScopesPlug, %{scopes: ["write:reports"], admin: true} when action in [:reports_update, :report_notes_create, :report_notes_delete] ) plug( OAuthScopesPlug, %{scopes: ["read:statuses"], admin: true} when action in [:list_user_statuses, :list_instance_statuses] ) plug( OAuthScopesPlug, %{scopes: ["read"], admin: true} when action in [ :config_show, :list_log, :stats, :config_descriptions, :need_reboot ] ) plug( OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ :restart, :config_update, :resend_confirmation_email, :confirm_email, :oauth_app_create, :oauth_app_list, :oauth_app_update, :oauth_app_delete, :reload_emoji ] ) action_fallback(AdminAPI.FallbackController) def user_delete(conn, %{"nickname" => nickname}) do user_delete(conn, %{"nicknames" => [nickname]}) end def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) users |> Enum.each(fn user -> {:ok, delete_data, _} = Builder.delete(admin, user.ap_id) Pipeline.common_pipeline(delete_data, local: true) end) ModerationLog.insert_log(%{ actor: admin, subject: users, action: "delete" }) conn |> json(nicknames) end def user_follow(%{assigns: %{user: admin}} = conn, %{ "follower" => follower_nick, "followed" => followed_nick }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.follow(follower, followed) ModerationLog.insert_log(%{ actor: admin, followed: followed, follower: follower, action: "follow" }) end conn |> json("ok") end def user_unfollow(%{assigns: %{user: admin}} = conn, %{ "follower" => follower_nick, "followed" => followed_nick }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.unfollow(follower, followed) ModerationLog.insert_log(%{ actor: admin, followed: followed, follower: follower, action: "unfollow" }) end conn |> json("ok") end def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do changesets = Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} -> user_data = %{ nickname: nickname, name: nickname, email: email, password: password, password_confirmation: password, bio: "." } User.register_changeset(%User{}, user_data, need_confirmation: false) end) |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi -> Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset) end) case Pleroma.Repo.transaction(changesets) do {:ok, users} -> res = users |> Map.values() |> Enum.map(fn user -> {:ok, user} = User.post_register_action(user) user end) |> Enum.map(&AccountView.render("created.json", %{user: &1})) ModerationLog.insert_log(%{ actor: admin, subjects: Map.values(users), action: "create" }) conn |> json(res) {:error, id, changeset, _} -> res = Enum.map(changesets.operations, fn {current_id, {:changeset, _current_changeset, _}} when current_id == id -> AccountView.render("create-error.json", %{changeset: changeset}) {_, {:changeset, current_changeset, _}} -> AccountView.render("create-error.json", %{changeset: current_changeset}) end) conn |> put_status(:conflict) |> json(res) end end def user_show(conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do conn |> put_view(AccountView) |> render("show.json", %{user: user}) else _ -> {:error, :not_found} end end def list_instance_statuses(conn, %{"instance" => instance} = params) do with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true {page, page_size} = page_params(params) activities = ActivityPub.fetch_statuses(nil, %{ "instance" => instance, "limit" => page_size, "offset" => (page - 1) * page_size, "exclude_reblogs" => !with_reblogs && "true" }) conn |> put_view(AdminAPI.StatusView) |> render("index.json", %{activities: activities, as: :activity}) end def list_user_statuses(conn, %{"nickname" => nickname} = params) do with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true godmode = params["godmode"] == "true" || params["godmode"] == true with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do {_, page_size} = page_params(params) activities = ActivityPub.fetch_user_activities(user, nil, %{ "limit" => page_size, "godmode" => godmode, "exclude_reblogs" => !with_reblogs && "true" }) conn |> put_view(MastodonAPI.StatusView) |> render("index.json", %{activities: activities, as: :activity}) else _ -> {:error, :not_found} end end def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) {:ok, updated_user} = User.deactivate(user, !user.deactivated) action = if user.deactivated, do: "activate", else: "deactivate" ModerationLog.insert_log(%{ actor: admin, subject: [user], action: action }) conn |> put_view(AccountView) |> render("show.json", %{user: updated_user}) end def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do users = Enum.map(nicknames, &User.get_cached_by_nickname/1) {:ok, updated_users} = User.deactivate(users, false) ModerationLog.insert_log(%{ actor: admin, subject: users, action: "activate" }) conn |> put_view(AccountView) |> render("index.json", %{users: Keyword.values(updated_users)}) end def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do users = Enum.map(nicknames, &User.get_cached_by_nickname/1) {:ok, updated_users} = User.deactivate(users, true) ModerationLog.insert_log(%{ actor: admin, subject: users, action: "deactivate" }) conn |> put_view(AccountView) |> render("index.json", %{users: Keyword.values(updated_users)}) end def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do with {:ok, _} <- User.tag(nicknames, tags) do ModerationLog.insert_log(%{ actor: admin, nicknames: nicknames, tags: tags, action: "tag" }) json_response(conn, :no_content, "") end end def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do with {:ok, _} <- User.untag(nicknames, tags) do ModerationLog.insert_log(%{ actor: admin, nicknames: nicknames, tags: tags, action: "untag" }) json_response(conn, :no_content, "") end end def list_users(conn, params) do {page, page_size} = page_params(params) filters = maybe_parse_filters(params["filters"]) search_params = %{ query: params["query"], page: page, page_size: page_size, tags: params["tags"], name: params["name"], email: params["email"] } with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do json( conn, AccountView.render("index.json", users: users, count: count, page_size: page_size) ) end end @filters ~w(local external active deactivated is_admin is_moderator) @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{} defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{} defp maybe_parse_filters(filters) do filters |> String.split(",") |> Enum.filter(&Enum.member?(@filters, &1)) |> Enum.map(&String.to_atom(&1)) |> Enum.into(%{}, &{&1, true}) end def right_add_multiple(%{assigns: %{user: admin}} = conn, %{ "permission_group" => permission_group, "nicknames" => nicknames }) when permission_group in ["moderator", "admin"] do update = %{:"is_#{permission_group}" => true} users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) for u <- users, do: User.admin_api_update(u, update) ModerationLog.insert_log(%{ action: "grant", actor: admin, subject: users, permission: permission_group }) json(conn, update) end def right_add_multiple(conn, _) do render_error(conn, :not_found, "No such permission_group") end def right_add(%{assigns: %{user: admin}} = conn, %{ "permission_group" => permission_group, "nickname" => nickname }) when permission_group in ["moderator", "admin"] do fields = %{:"is_#{permission_group}" => true} {:ok, user} = nickname |> User.get_cached_by_nickname() |> User.admin_api_update(fields) ModerationLog.insert_log(%{ action: "grant", actor: admin, subject: [user], permission: permission_group }) json(conn, fields) end def right_add(conn, _) do render_error(conn, :not_found, "No such permission_group") end def right_get(conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) conn |> json(%{ is_moderator: user.is_moderator, is_admin: user.is_admin }) end def right_delete_multiple( %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn, %{ "permission_group" => permission_group, "nicknames" => nicknames } ) when permission_group in ["moderator", "admin"] do with false <- Enum.member?(nicknames, admin_nickname) do update = %{:"is_#{permission_group}" => false} users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) for u <- users, do: User.admin_api_update(u, update) ModerationLog.insert_log(%{ action: "revoke", actor: admin, subject: users, permission: permission_group }) json(conn, update) else _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.") end end def right_delete_multiple(conn, _) do render_error(conn, :not_found, "No such permission_group") end def right_delete( %{assigns: %{user: admin}} = conn, %{ "permission_group" => permission_group, "nickname" => nickname } ) when permission_group in ["moderator", "admin"] do fields = %{:"is_#{permission_group}" => false} {:ok, user} = nickname |> User.get_cached_by_nickname() |> User.admin_api_update(fields) ModerationLog.insert_log(%{ action: "revoke", actor: admin, subject: [user], permission: permission_group }) json(conn, fields) end def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do render_error(conn, :forbidden, "You can't revoke your own admin status.") end @doc "Sends registration invite via email" def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, {:ok, invite_token} <- UserInviteToken.create_invite(), email <- Pleroma.Emails.UserEmail.user_invitation_email( user, invite_token, email, params["name"] ), {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do json_response(conn, :no_content, "") else {:registrations_open, _} -> {:error, "To send invites you need to set the `registrations_open` option to false."} {:invites_enabled, _} -> {:error, "To send invites you need to set the `invites_enabled` option to true."} end end @doc "Create an account registration invite token" def create_invite_token(conn, params) do opts = %{} opts = if params["max_use"], do: Map.put(opts, :max_use, params["max_use"]), else: opts opts = if params["expires_at"], do: Map.put(opts, :expires_at, params["expires_at"]), else: opts {:ok, invite} = UserInviteToken.create_invite(opts) json(conn, AccountView.render("invite.json", %{invite: invite})) end @doc "Get list of created invites" def invites(conn, _params) do invites = UserInviteToken.list_invites() conn |> put_view(AccountView) |> render("invites.json", %{invites: invites}) end @doc "Revokes invite by token" def revoke_invite(conn, %{"token" => token}) do with {:ok, invite} <- UserInviteToken.find_by_token(token), {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do conn |> put_view(AccountView) |> render("invite.json", %{invite: updated_invite}) else nil -> {:error, :not_found} end end @doc "Get a password reset token (base64 string) for given nickname" def get_password_reset(conn, %{"nickname" => nickname}) do (%User{local: true} = user) = User.get_cached_by_nickname(nickname) {:ok, token} = Pleroma.PasswordResetToken.create_token(user) conn |> json(%{ token: token.token, link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token) }) end @doc "Force password reset for a given user" def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) Enum.each(users, &User.force_password_reset_async/1) ModerationLog.insert_log(%{ actor: admin, subject: users, action: "force_password_reset" }) json_response(conn, :no_content, "") end @doc "Disable mfa for user's account." def disable_mfa(conn, %{"nickname" => nickname}) do case User.get_by_nickname(nickname) do %User{} = user -> MFA.disable(user) json(conn, nickname) _ -> {:error, :not_found} end end @doc "Show a given user's credentials" def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do conn |> put_view(AccountView) |> render("credentials.json", %{user: user, for: admin}) else _ -> {:error, :not_found} end end @doc "Updates a given user" def update_user_credentials( %{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params ) do with {_, user} <- {:user, User.get_cached_by_nickname(nickname)}, {:ok, _user} <- User.update_as_admin(user, params) do ModerationLog.insert_log(%{ actor: admin, subject: [user], action: "updated_users" }) if params["password"] do User.force_password_reset_async(user) end ModerationLog.insert_log(%{ actor: admin, subject: [user], action: "force_password_reset" }) json(conn, %{status: "success"}) else {:error, changeset} -> {_, {error, _}} = Enum.at(changeset.errors, 0) json(conn, %{error: "New password #{error}."}) _ -> json(conn, %{error: "Unable to change password."}) end end def list_reports(conn, params) do {page, page_size} = page_params(params) reports = Utils.get_reports(params, page, page_size) conn |> put_view(ReportView) |> render("index.json", %{reports: reports}) end def report_show(conn, %{"id" => id}) do with %Activity{} = report <- Activity.get_by_id(id) do conn |> put_view(ReportView) |> render("show.json", Report.extract_report_info(report)) else _ -> {:error, :not_found} end end def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do result = reports |> Enum.map(fn report -> with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do ModerationLog.insert_log(%{ action: "report_update", actor: admin, subject: activity }) activity else {:error, message} -> %{id: report["id"], error: message} end end) case Enum.any?(result, &Map.has_key?(&1, :error)) do true -> json_response(conn, :bad_request, result) false -> json_response(conn, :no_content, "") end end def report_notes_create(%{assigns: %{user: user}} = conn, %{ "id" => report_id, "content" => content }) do with {:ok, _} <- ReportNote.create(user.id, report_id, content) do ModerationLog.insert_log(%{ action: "report_note", actor: user, subject: Activity.get_by_id(report_id), text: content }) json_response(conn, :no_content, "") else _ -> json_response(conn, :bad_request, "") end end def report_notes_delete(%{assigns: %{user: user}} = conn, %{ "id" => note_id, "report_id" => report_id }) do with {:ok, note} <- ReportNote.destroy(note_id) do ModerationLog.insert_log(%{ action: "report_note_delete", actor: user, subject: Activity.get_by_id(report_id), text: note.content }) json_response(conn, :no_content, "") else _ -> json_response(conn, :bad_request, "") end end def list_log(conn, params) do {page, page_size} = page_params(params) log = ModerationLog.get_all(%{ page: page, page_size: page_size, start_date: params["start_date"], end_date: params["end_date"], user_id: params["user_id"], search: params["search"] }) conn |> put_view(ModerationLogView) |> render("index.json", %{log: log}) end def config_descriptions(conn, _params) do descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) json(conn, descriptions) end def config_show(conn, %{"only_db" => true}) do with :ok <- configurable_from_database() do configs = Pleroma.Repo.all(ConfigDB) conn |> put_view(ConfigView) |> render("index.json", %{configs: configs}) end end def config_show(conn, _params) do with :ok <- configurable_from_database() do configs = ConfigDB.get_all_as_keyword() merged = Config.Holder.default_config() |> ConfigDB.merge(configs) |> Enum.map(fn {group, values} -> Enum.map(values, fn {key, value} -> db = if configs[group][key] do ConfigDB.get_db_keys(configs[group][key], key) end db_value = configs[group][key] merged_value = if !is_nil(db_value) and Keyword.keyword?(db_value) and ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do ConfigDB.merge_group(group, key, value, db_value) else value end setting = %{ group: ConfigDB.convert(group), key: ConfigDB.convert(key), value: ConfigDB.convert(merged_value) } if db, do: Map.put(setting, :db, db), else: setting end) end) |> List.flatten() json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) end end def config_update(conn, %{"configs" => configs}) do with :ok <- configurable_from_database() do {_errors, results} = configs |> Enum.filter(&whitelisted_config?/1) |> Enum.map(fn %{"group" => group, "key" => key, "delete" => true} = params -> ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) %{"group" => group, "key" => key, "value" => value} -> ConfigDB.update_or_create(%{group: group, key: key, value: value}) end) |> Enum.split_with(fn result -> elem(result, 0) == :error end) {deleted, updated} = results |> Enum.map(fn {:ok, config} -> Map.put(config, :db, ConfigDB.get_db_keys(config)) end) |> Enum.split_with(fn config -> Ecto.get_meta(config, :state) == :deleted end) Config.TransferTask.load_and_update_env(deleted, false) if !Restarter.Pleroma.need_reboot?() do changed_reboot_settings? = (updated ++ deleted) |> Enum.any?(fn config -> group = ConfigDB.from_string(config.group) key = ConfigDB.from_string(config.key) value = ConfigDB.from_binary(config.value) Config.TransferTask.pleroma_need_restart?(group, key, value) end) if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() end conn |> put_view(ConfigView) |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()}) end end def restart(conn, _params) do with :ok <- configurable_from_database() do Restarter.Pleroma.restart(Config.get(:env), 50) json(conn, %{}) end end def need_reboot(conn, _params) do json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()}) end defp configurable_from_database do if Config.get(:configurable_from_database) do :ok else {:error, "To use this endpoint you need to enable configuration from database."} end end defp whitelisted_config?(group, key) do if whitelisted_configs = Config.get(:database_config_whitelist) do Enum.any?(whitelisted_configs, fn {whitelisted_group} -> group == inspect(whitelisted_group) {whitelisted_group, whitelisted_key} -> group == inspect(whitelisted_group) && key == inspect(whitelisted_key) end) else true end end defp whitelisted_config?(%{"group" => group, "key" => key}) do whitelisted_config?(group, key) end defp whitelisted_config?(%{:group => group} = config) do whitelisted_config?(group, config[:key]) end def reload_emoji(conn, _params) do Pleroma.Emoji.reload() conn |> json("ok") end def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) User.toggle_confirmation(users) ModerationLog.insert_log(%{ actor: admin, subject: users, action: "confirm_email" }) conn |> json("") end def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) User.try_send_confirmation_email(users) ModerationLog.insert_log(%{ actor: admin, subject: users, action: "resend_confirmation_email" }) conn |> json("") end def oauth_app_create(conn, params) do params = if params["name"] do Map.put(params, "client_name", params["name"]) else params end result = case App.create(params) do {:ok, app} -> AppView.render("show.json", %{app: app, admin: true}) {:error, changeset} -> App.errors(changeset) end json(conn, result) end def oauth_app_update(conn, params) do params = if params["name"] do Map.put(params, "client_name", params["name"]) else params end with {:ok, app} <- App.update(params) do json(conn, AppView.render("show.json", %{app: app, admin: true})) else {:error, changeset} -> json(conn, App.errors(changeset)) nil -> json_response(conn, :bad_request, "") end end def oauth_app_list(conn, params) do {page, page_size} = page_params(params) search_params = %{ client_name: params["name"], client_id: params["client_id"], page: page, page_size: page_size } search_params = if Map.has_key?(params, "trusted") do Map.put(search_params, :trusted, params["trusted"]) else search_params end with {:ok, apps, count} <- App.search(search_params) do json( conn, AppView.render("index.json", apps: apps, count: count, page_size: page_size, admin: true ) ) end end def oauth_app_delete(conn, params) do with {:ok, _app} <- App.destroy(params["id"]) do json_response(conn, :no_content, "") else _ -> json_response(conn, :bad_request, "") end end def stats(conn, _) do count = Stats.get_status_visibility_count() conn |> json(%{"status_visibility" => count}) end defp page_params(params) do {get_page(params["page"]), get_page_size(params["page_size"])} end defp get_page(page_string) when is_nil(page_string), do: 1 defp get_page(page_string) do case Integer.parse(page_string) do {page, _} -> page :error -> 1 end end defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size defp get_page_size(page_size_string) do case Integer.parse(page_size_string) do {page_size, _} -> page_size :error -> @users_page_size end end end