Merge branch 'develop' into openapi/admin/relay
[akkoma] / lib / pleroma / web / admin_api / controllers / admin_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.AdminAPI.AdminAPIController do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
9
10 alias Pleroma.Config
11 alias Pleroma.ConfigDB
12 alias Pleroma.MFA
13 alias Pleroma.ModerationLog
14 alias Pleroma.Plugs.OAuthScopesPlug
15 alias Pleroma.Stats
16 alias Pleroma.User
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Builder
19 alias Pleroma.Web.ActivityPub.Pipeline
20 alias Pleroma.Web.AdminAPI
21 alias Pleroma.Web.AdminAPI.AccountView
22 alias Pleroma.Web.AdminAPI.ConfigView
23 alias Pleroma.Web.AdminAPI.ModerationLogView
24 alias Pleroma.Web.AdminAPI.Search
25 alias Pleroma.Web.Endpoint
26 alias Pleroma.Web.Router
27
28 require Logger
29
30 @descriptions Pleroma.Docs.JSON.compile()
31 @users_page_size 50
32
33 plug(
34 OAuthScopesPlug,
35 %{scopes: ["read:accounts"], admin: true}
36 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
37 )
38
39 plug(
40 OAuthScopesPlug,
41 %{scopes: ["write:accounts"], admin: true}
42 when action in [
43 :get_password_reset,
44 :force_password_reset,
45 :user_delete,
46 :users_create,
47 :user_toggle_activation,
48 :user_activate,
49 :user_deactivate,
50 :tag_users,
51 :untag_users,
52 :right_add,
53 :right_add_multiple,
54 :right_delete,
55 :disable_mfa,
56 :right_delete_multiple,
57 :update_user_credentials
58 ]
59 )
60
61 plug(
62 OAuthScopesPlug,
63 %{scopes: ["write:follows"], admin: true}
64 when action in [:user_follow, :user_unfollow]
65 )
66
67 plug(
68 OAuthScopesPlug,
69 %{scopes: ["read:statuses"], admin: true}
70 when action in [:list_user_statuses, :list_instance_statuses]
71 )
72
73 plug(
74 OAuthScopesPlug,
75 %{scopes: ["read"], admin: true}
76 when action in [
77 :config_show,
78 :list_log,
79 :stats,
80 :config_descriptions,
81 :need_reboot
82 ]
83 )
84
85 plug(
86 OAuthScopesPlug,
87 %{scopes: ["write"], admin: true}
88 when action in [
89 :restart,
90 :config_update,
91 :resend_confirmation_email,
92 :confirm_email,
93 :reload_emoji
94 ]
95 )
96
97 action_fallback(AdminAPI.FallbackController)
98
99 def user_delete(conn, %{"nickname" => nickname}) do
100 user_delete(conn, %{"nicknames" => [nickname]})
101 end
102
103 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
104 users =
105 nicknames
106 |> Enum.map(&User.get_cached_by_nickname/1)
107
108 users
109 |> Enum.each(fn user ->
110 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
111 Pipeline.common_pipeline(delete_data, local: true)
112 end)
113
114 ModerationLog.insert_log(%{
115 actor: admin,
116 subject: users,
117 action: "delete"
118 })
119
120 conn
121 |> json(nicknames)
122 end
123
124 def user_follow(%{assigns: %{user: admin}} = conn, %{
125 "follower" => follower_nick,
126 "followed" => followed_nick
127 }) do
128 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
129 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
130 User.follow(follower, followed)
131
132 ModerationLog.insert_log(%{
133 actor: admin,
134 followed: followed,
135 follower: follower,
136 action: "follow"
137 })
138 end
139
140 conn
141 |> json("ok")
142 end
143
144 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
145 "follower" => follower_nick,
146 "followed" => followed_nick
147 }) do
148 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
149 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
150 User.unfollow(follower, followed)
151
152 ModerationLog.insert_log(%{
153 actor: admin,
154 followed: followed,
155 follower: follower,
156 action: "unfollow"
157 })
158 end
159
160 conn
161 |> json("ok")
162 end
163
164 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
165 changesets =
166 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
167 user_data = %{
168 nickname: nickname,
169 name: nickname,
170 email: email,
171 password: password,
172 password_confirmation: password,
173 bio: "."
174 }
175
176 User.register_changeset(%User{}, user_data, need_confirmation: false)
177 end)
178 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
179 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
180 end)
181
182 case Pleroma.Repo.transaction(changesets) do
183 {:ok, users} ->
184 res =
185 users
186 |> Map.values()
187 |> Enum.map(fn user ->
188 {:ok, user} = User.post_register_action(user)
189
190 user
191 end)
192 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
193
194 ModerationLog.insert_log(%{
195 actor: admin,
196 subjects: Map.values(users),
197 action: "create"
198 })
199
200 conn
201 |> json(res)
202
203 {:error, id, changeset, _} ->
204 res =
205 Enum.map(changesets.operations, fn
206 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
207 AccountView.render("create-error.json", %{changeset: changeset})
208
209 {_, {:changeset, current_changeset, _}} ->
210 AccountView.render("create-error.json", %{changeset: current_changeset})
211 end)
212
213 conn
214 |> put_status(:conflict)
215 |> json(res)
216 end
217 end
218
219 def user_show(conn, %{"nickname" => nickname}) do
220 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
221 conn
222 |> put_view(AccountView)
223 |> render("show.json", %{user: user})
224 else
225 _ -> {:error, :not_found}
226 end
227 end
228
229 def list_instance_statuses(conn, %{"instance" => instance} = params) do
230 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
231 {page, page_size} = page_params(params)
232
233 activities =
234 ActivityPub.fetch_statuses(nil, %{
235 "instance" => instance,
236 "limit" => page_size,
237 "offset" => (page - 1) * page_size,
238 "exclude_reblogs" => !with_reblogs && "true"
239 })
240
241 conn
242 |> put_view(AdminAPI.StatusView)
243 |> render("index.json", %{activities: activities, as: :activity})
244 end
245
246 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
247 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
248 godmode = params["godmode"] == "true" || params["godmode"] == true
249
250 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
251 {_, page_size} = page_params(params)
252
253 activities =
254 ActivityPub.fetch_user_activities(user, nil, %{
255 "limit" => page_size,
256 "godmode" => godmode,
257 "exclude_reblogs" => !with_reblogs && "true"
258 })
259
260 conn
261 |> put_view(AdminAPI.StatusView)
262 |> render("index.json", %{activities: activities, as: :activity})
263 else
264 _ -> {:error, :not_found}
265 end
266 end
267
268 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
269 user = User.get_cached_by_nickname(nickname)
270
271 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
272
273 action = if user.deactivated, do: "activate", else: "deactivate"
274
275 ModerationLog.insert_log(%{
276 actor: admin,
277 subject: [user],
278 action: action
279 })
280
281 conn
282 |> put_view(AccountView)
283 |> render("show.json", %{user: updated_user})
284 end
285
286 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
287 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
288 {:ok, updated_users} = User.deactivate(users, false)
289
290 ModerationLog.insert_log(%{
291 actor: admin,
292 subject: users,
293 action: "activate"
294 })
295
296 conn
297 |> put_view(AccountView)
298 |> render("index.json", %{users: Keyword.values(updated_users)})
299 end
300
301 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
302 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
303 {:ok, updated_users} = User.deactivate(users, true)
304
305 ModerationLog.insert_log(%{
306 actor: admin,
307 subject: users,
308 action: "deactivate"
309 })
310
311 conn
312 |> put_view(AccountView)
313 |> render("index.json", %{users: Keyword.values(updated_users)})
314 end
315
316 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
317 with {:ok, _} <- User.tag(nicknames, tags) do
318 ModerationLog.insert_log(%{
319 actor: admin,
320 nicknames: nicknames,
321 tags: tags,
322 action: "tag"
323 })
324
325 json_response(conn, :no_content, "")
326 end
327 end
328
329 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
330 with {:ok, _} <- User.untag(nicknames, tags) do
331 ModerationLog.insert_log(%{
332 actor: admin,
333 nicknames: nicknames,
334 tags: tags,
335 action: "untag"
336 })
337
338 json_response(conn, :no_content, "")
339 end
340 end
341
342 def list_users(conn, params) do
343 {page, page_size} = page_params(params)
344 filters = maybe_parse_filters(params["filters"])
345
346 search_params = %{
347 query: params["query"],
348 page: page,
349 page_size: page_size,
350 tags: params["tags"],
351 name: params["name"],
352 email: params["email"]
353 }
354
355 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
356 json(
357 conn,
358 AccountView.render("index.json", users: users, count: count, page_size: page_size)
359 )
360 end
361 end
362
363 @filters ~w(local external active deactivated is_admin is_moderator)
364
365 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
366 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
367
368 defp maybe_parse_filters(filters) do
369 filters
370 |> String.split(",")
371 |> Enum.filter(&Enum.member?(@filters, &1))
372 |> Enum.map(&String.to_atom(&1))
373 |> Enum.into(%{}, &{&1, true})
374 end
375
376 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
377 "permission_group" => permission_group,
378 "nicknames" => nicknames
379 })
380 when permission_group in ["moderator", "admin"] do
381 update = %{:"is_#{permission_group}" => true}
382
383 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
384
385 for u <- users, do: User.admin_api_update(u, update)
386
387 ModerationLog.insert_log(%{
388 action: "grant",
389 actor: admin,
390 subject: users,
391 permission: permission_group
392 })
393
394 json(conn, update)
395 end
396
397 def right_add_multiple(conn, _) do
398 render_error(conn, :not_found, "No such permission_group")
399 end
400
401 def right_add(%{assigns: %{user: admin}} = conn, %{
402 "permission_group" => permission_group,
403 "nickname" => nickname
404 })
405 when permission_group in ["moderator", "admin"] do
406 fields = %{:"is_#{permission_group}" => true}
407
408 {:ok, user} =
409 nickname
410 |> User.get_cached_by_nickname()
411 |> User.admin_api_update(fields)
412
413 ModerationLog.insert_log(%{
414 action: "grant",
415 actor: admin,
416 subject: [user],
417 permission: permission_group
418 })
419
420 json(conn, fields)
421 end
422
423 def right_add(conn, _) do
424 render_error(conn, :not_found, "No such permission_group")
425 end
426
427 def right_get(conn, %{"nickname" => nickname}) do
428 user = User.get_cached_by_nickname(nickname)
429
430 conn
431 |> json(%{
432 is_moderator: user.is_moderator,
433 is_admin: user.is_admin
434 })
435 end
436
437 def right_delete_multiple(
438 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
439 %{
440 "permission_group" => permission_group,
441 "nicknames" => nicknames
442 }
443 )
444 when permission_group in ["moderator", "admin"] do
445 with false <- Enum.member?(nicknames, admin_nickname) do
446 update = %{:"is_#{permission_group}" => false}
447
448 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
449
450 for u <- users, do: User.admin_api_update(u, update)
451
452 ModerationLog.insert_log(%{
453 action: "revoke",
454 actor: admin,
455 subject: users,
456 permission: permission_group
457 })
458
459 json(conn, update)
460 else
461 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
462 end
463 end
464
465 def right_delete_multiple(conn, _) do
466 render_error(conn, :not_found, "No such permission_group")
467 end
468
469 def right_delete(
470 %{assigns: %{user: admin}} = conn,
471 %{
472 "permission_group" => permission_group,
473 "nickname" => nickname
474 }
475 )
476 when permission_group in ["moderator", "admin"] do
477 fields = %{:"is_#{permission_group}" => false}
478
479 {:ok, user} =
480 nickname
481 |> User.get_cached_by_nickname()
482 |> User.admin_api_update(fields)
483
484 ModerationLog.insert_log(%{
485 action: "revoke",
486 actor: admin,
487 subject: [user],
488 permission: permission_group
489 })
490
491 json(conn, fields)
492 end
493
494 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
495 render_error(conn, :forbidden, "You can't revoke your own admin status.")
496 end
497
498 @doc "Get a password reset token (base64 string) for given nickname"
499 def get_password_reset(conn, %{"nickname" => nickname}) do
500 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
501 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
502
503 conn
504 |> json(%{
505 token: token.token,
506 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
507 })
508 end
509
510 @doc "Force password reset for a given user"
511 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
512 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
513
514 Enum.each(users, &User.force_password_reset_async/1)
515
516 ModerationLog.insert_log(%{
517 actor: admin,
518 subject: users,
519 action: "force_password_reset"
520 })
521
522 json_response(conn, :no_content, "")
523 end
524
525 @doc "Disable mfa for user's account."
526 def disable_mfa(conn, %{"nickname" => nickname}) do
527 case User.get_by_nickname(nickname) do
528 %User{} = user ->
529 MFA.disable(user)
530 json(conn, nickname)
531
532 _ ->
533 {:error, :not_found}
534 end
535 end
536
537 @doc "Show a given user's credentials"
538 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
539 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
540 conn
541 |> put_view(AccountView)
542 |> render("credentials.json", %{user: user, for: admin})
543 else
544 _ -> {:error, :not_found}
545 end
546 end
547
548 @doc "Updates a given user"
549 def update_user_credentials(
550 %{assigns: %{user: admin}} = conn,
551 %{"nickname" => nickname} = params
552 ) do
553 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
554 {:ok, _user} <-
555 User.update_as_admin(user, params) do
556 ModerationLog.insert_log(%{
557 actor: admin,
558 subject: [user],
559 action: "updated_users"
560 })
561
562 if params["password"] do
563 User.force_password_reset_async(user)
564 end
565
566 ModerationLog.insert_log(%{
567 actor: admin,
568 subject: [user],
569 action: "force_password_reset"
570 })
571
572 json(conn, %{status: "success"})
573 else
574 {:error, changeset} ->
575 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
576
577 json(conn, %{errors: errors})
578
579 _ ->
580 json(conn, %{error: "Unable to update user."})
581 end
582 end
583
584 def list_log(conn, params) do
585 {page, page_size} = page_params(params)
586
587 log =
588 ModerationLog.get_all(%{
589 page: page,
590 page_size: page_size,
591 start_date: params["start_date"],
592 end_date: params["end_date"],
593 user_id: params["user_id"],
594 search: params["search"]
595 })
596
597 conn
598 |> put_view(ModerationLogView)
599 |> render("index.json", %{log: log})
600 end
601
602 def config_descriptions(conn, _params) do
603 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
604
605 json(conn, descriptions)
606 end
607
608 def config_show(conn, %{"only_db" => true}) do
609 with :ok <- configurable_from_database() do
610 configs = Pleroma.Repo.all(ConfigDB)
611
612 conn
613 |> put_view(ConfigView)
614 |> render("index.json", %{configs: configs})
615 end
616 end
617
618 def config_show(conn, _params) do
619 with :ok <- configurable_from_database() do
620 configs = ConfigDB.get_all_as_keyword()
621
622 merged =
623 Config.Holder.default_config()
624 |> ConfigDB.merge(configs)
625 |> Enum.map(fn {group, values} ->
626 Enum.map(values, fn {key, value} ->
627 db =
628 if configs[group][key] do
629 ConfigDB.get_db_keys(configs[group][key], key)
630 end
631
632 db_value = configs[group][key]
633
634 merged_value =
635 if !is_nil(db_value) and Keyword.keyword?(db_value) and
636 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
637 ConfigDB.merge_group(group, key, value, db_value)
638 else
639 value
640 end
641
642 setting = %{
643 group: ConfigDB.convert(group),
644 key: ConfigDB.convert(key),
645 value: ConfigDB.convert(merged_value)
646 }
647
648 if db, do: Map.put(setting, :db, db), else: setting
649 end)
650 end)
651 |> List.flatten()
652
653 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
654 end
655 end
656
657 def config_update(conn, %{"configs" => configs}) do
658 with :ok <- configurable_from_database() do
659 {_errors, results} =
660 configs
661 |> Enum.filter(&whitelisted_config?/1)
662 |> Enum.map(fn
663 %{"group" => group, "key" => key, "delete" => true} = params ->
664 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
665
666 %{"group" => group, "key" => key, "value" => value} ->
667 ConfigDB.update_or_create(%{group: group, key: key, value: value})
668 end)
669 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
670
671 {deleted, updated} =
672 results
673 |> Enum.map(fn {:ok, config} ->
674 Map.put(config, :db, ConfigDB.get_db_keys(config))
675 end)
676 |> Enum.split_with(fn config ->
677 Ecto.get_meta(config, :state) == :deleted
678 end)
679
680 Config.TransferTask.load_and_update_env(deleted, false)
681
682 if !Restarter.Pleroma.need_reboot?() do
683 changed_reboot_settings? =
684 (updated ++ deleted)
685 |> Enum.any?(fn config ->
686 group = ConfigDB.from_string(config.group)
687 key = ConfigDB.from_string(config.key)
688 value = ConfigDB.from_binary(config.value)
689 Config.TransferTask.pleroma_need_restart?(group, key, value)
690 end)
691
692 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
693 end
694
695 conn
696 |> put_view(ConfigView)
697 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
698 end
699 end
700
701 def restart(conn, _params) do
702 with :ok <- configurable_from_database() do
703 Restarter.Pleroma.restart(Config.get(:env), 50)
704
705 json(conn, %{})
706 end
707 end
708
709 def need_reboot(conn, _params) do
710 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
711 end
712
713 defp configurable_from_database do
714 if Config.get(:configurable_from_database) do
715 :ok
716 else
717 {:error, "To use this endpoint you need to enable configuration from database."}
718 end
719 end
720
721 defp whitelisted_config?(group, key) do
722 if whitelisted_configs = Config.get(:database_config_whitelist) do
723 Enum.any?(whitelisted_configs, fn
724 {whitelisted_group} ->
725 group == inspect(whitelisted_group)
726
727 {whitelisted_group, whitelisted_key} ->
728 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
729 end)
730 else
731 true
732 end
733 end
734
735 defp whitelisted_config?(%{"group" => group, "key" => key}) do
736 whitelisted_config?(group, key)
737 end
738
739 defp whitelisted_config?(%{:group => group} = config) do
740 whitelisted_config?(group, config[:key])
741 end
742
743 def reload_emoji(conn, _params) do
744 Pleroma.Emoji.reload()
745
746 conn |> json("ok")
747 end
748
749 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
750 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
751
752 User.toggle_confirmation(users)
753
754 ModerationLog.insert_log(%{
755 actor: admin,
756 subject: users,
757 action: "confirm_email"
758 })
759
760 conn |> json("")
761 end
762
763 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
764 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
765
766 User.try_send_confirmation_email(users)
767
768 ModerationLog.insert_log(%{
769 actor: admin,
770 subject: users,
771 action: "resend_confirmation_email"
772 })
773
774 conn |> json("")
775 end
776
777 def stats(conn, _) do
778 count = Stats.get_status_visibility_count()
779
780 conn
781 |> json(%{"status_visibility" => count})
782 end
783
784 defp page_params(params) do
785 {get_page(params["page"]), get_page_size(params["page_size"])}
786 end
787
788 defp get_page(page_string) when is_nil(page_string), do: 1
789
790 defp get_page(page_string) do
791 case Integer.parse(page_string) do
792 {page, _} -> page
793 :error -> 1
794 end
795 end
796
797 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
798
799 defp get_page_size(page_size_string) do
800 case Integer.parse(page_size_string) do
801 {page_size, _} -> page_size
802 :error -> @users_page_size
803 end
804 end
805 end