[#1234] Permissions-related fixes / new functionality (Masto 2.4.3 scopes).
[akkoma] / lib / pleroma / web / admin_api / admin_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 alias Pleroma.Activity
8 alias Pleroma.ModerationLog
9 alias Pleroma.Plugs.OAuthScopesPlug
10 alias Pleroma.User
11 alias Pleroma.UserInviteToken
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Relay
14 alias Pleroma.Web.AdminAPI.AccountView
15 alias Pleroma.Web.AdminAPI.Config
16 alias Pleroma.Web.AdminAPI.ConfigView
17 alias Pleroma.Web.AdminAPI.ModerationLogView
18 alias Pleroma.Web.AdminAPI.ReportView
19 alias Pleroma.Web.AdminAPI.Search
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.StatusView
22
23 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
24
25 require Logger
26
27 plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :list_user_statuses)
28
29 plug(
30 OAuthScopesPlug,
31 %{scopes: ["write:statuses"]} when action in [:status_update, :status_delete]
32 )
33
34 plug(
35 OAuthScopesPlug,
36 %{scopes: ["read"]}
37 when action in [
38 :list_reports,
39 :report_show,
40 :right_get,
41 :get_invite_token,
42 :invites,
43 :get_password_reset,
44 :list_users,
45 :user_show,
46 :config_show,
47 :migrate_to_db,
48 :migrate_from_db,
49 :list_log
50 ]
51 )
52
53 plug(
54 OAuthScopesPlug,
55 %{scopes: ["write"]}
56 when action in [
57 :report_update_state,
58 :report_respond,
59 :user_follow,
60 :user_unfollow,
61 :user_delete,
62 :users_create,
63 :user_toggle_activation,
64 :tag_users,
65 :untag_users,
66 :right_add,
67 :right_delete,
68 :set_activation_status,
69 :relay_follow,
70 :relay_unfollow,
71 :revoke_invite,
72 :email_invite,
73 :config_update
74 ]
75 )
76
77 @users_page_size 50
78
79 action_fallback(:errors)
80
81 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
82 user = User.get_cached_by_nickname(nickname)
83 User.delete(user)
84
85 ModerationLog.insert_log(%{
86 actor: admin,
87 subject: user,
88 action: "delete"
89 })
90
91 conn
92 |> json(nickname)
93 end
94
95 def user_follow(%{assigns: %{user: admin}} = conn, %{
96 "follower" => follower_nick,
97 "followed" => followed_nick
98 }) do
99 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
100 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
101 User.follow(follower, followed)
102
103 ModerationLog.insert_log(%{
104 actor: admin,
105 followed: followed,
106 follower: follower,
107 action: "follow"
108 })
109 end
110
111 conn
112 |> json("ok")
113 end
114
115 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
116 "follower" => follower_nick,
117 "followed" => followed_nick
118 }) do
119 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
120 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
121 User.unfollow(follower, followed)
122
123 ModerationLog.insert_log(%{
124 actor: admin,
125 followed: followed,
126 follower: follower,
127 action: "unfollow"
128 })
129 end
130
131 conn
132 |> json("ok")
133 end
134
135 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
136 changesets =
137 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
138 user_data = %{
139 nickname: nickname,
140 name: nickname,
141 email: email,
142 password: password,
143 password_confirmation: password,
144 bio: "."
145 }
146
147 User.register_changeset(%User{}, user_data, need_confirmation: false)
148 end)
149 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
150 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
151 end)
152
153 case Pleroma.Repo.transaction(changesets) do
154 {:ok, users} ->
155 res =
156 users
157 |> Map.values()
158 |> Enum.map(fn user ->
159 {:ok, user} = User.post_register_action(user)
160
161 user
162 end)
163 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
164
165 ModerationLog.insert_log(%{
166 actor: admin,
167 subjects: Map.values(users),
168 action: "create"
169 })
170
171 conn
172 |> json(res)
173
174 {:error, id, changeset, _} ->
175 res =
176 Enum.map(changesets.operations, fn
177 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
178 AccountView.render("create-error.json", %{changeset: changeset})
179
180 {_, {:changeset, current_changeset, _}} ->
181 AccountView.render("create-error.json", %{changeset: current_changeset})
182 end)
183
184 conn
185 |> put_status(:conflict)
186 |> json(res)
187 end
188 end
189
190 def user_show(conn, %{"nickname" => nickname}) do
191 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
192 conn
193 |> json(AccountView.render("show.json", %{user: user}))
194 else
195 _ -> {:error, :not_found}
196 end
197 end
198
199 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
200 godmode = params["godmode"] == "true" || params["godmode"] == true
201
202 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
203 {_, page_size} = page_params(params)
204
205 activities =
206 ActivityPub.fetch_user_activities(user, nil, %{
207 "limit" => page_size,
208 "godmode" => godmode
209 })
210
211 conn
212 |> json(StatusView.render("index.json", %{activities: activities, as: :activity}))
213 else
214 _ -> {:error, :not_found}
215 end
216 end
217
218 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
219 user = User.get_cached_by_nickname(nickname)
220
221 {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
222
223 action = if user.info.deactivated, do: "activate", else: "deactivate"
224
225 ModerationLog.insert_log(%{
226 actor: admin,
227 subject: user,
228 action: action
229 })
230
231 conn
232 |> json(AccountView.render("show.json", %{user: updated_user}))
233 end
234
235 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
236 with {:ok, _} <- User.tag(nicknames, tags) do
237 ModerationLog.insert_log(%{
238 actor: admin,
239 nicknames: nicknames,
240 tags: tags,
241 action: "tag"
242 })
243
244 json_response(conn, :no_content, "")
245 end
246 end
247
248 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
249 with {:ok, _} <- User.untag(nicknames, tags) do
250 ModerationLog.insert_log(%{
251 actor: admin,
252 nicknames: nicknames,
253 tags: tags,
254 action: "untag"
255 })
256
257 json_response(conn, :no_content, "")
258 end
259 end
260
261 def list_users(conn, params) do
262 {page, page_size} = page_params(params)
263 filters = maybe_parse_filters(params["filters"])
264
265 search_params = %{
266 query: params["query"],
267 page: page,
268 page_size: page_size,
269 tags: params["tags"],
270 name: params["name"],
271 email: params["email"]
272 }
273
274 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
275 do:
276 conn
277 |> json(
278 AccountView.render("index.json",
279 users: users,
280 count: count,
281 page_size: page_size
282 )
283 )
284 end
285
286 @filters ~w(local external active deactivated is_admin is_moderator)
287
288 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
289 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
290
291 defp maybe_parse_filters(filters) do
292 filters
293 |> String.split(",")
294 |> Enum.filter(&Enum.member?(@filters, &1))
295 |> Enum.map(&String.to_atom(&1))
296 |> Enum.into(%{}, &{&1, true})
297 end
298
299 def right_add(%{assigns: %{user: admin}} = conn, %{
300 "permission_group" => permission_group,
301 "nickname" => nickname
302 })
303 when permission_group in ["moderator", "admin"] do
304 user = User.get_cached_by_nickname(nickname)
305
306 info =
307 %{}
308 |> Map.put("is_" <> permission_group, true)
309
310 info_cng = User.Info.admin_api_update(user.info, info)
311
312 cng =
313 user
314 |> Ecto.Changeset.change()
315 |> Ecto.Changeset.put_embed(:info, info_cng)
316
317 ModerationLog.insert_log(%{
318 action: "grant",
319 actor: admin,
320 subject: user,
321 permission: permission_group
322 })
323
324 {:ok, _user} = User.update_and_set_cache(cng)
325
326 json(conn, info)
327 end
328
329 def right_add(conn, _) do
330 render_error(conn, :not_found, "No such permission_group")
331 end
332
333 def right_get(conn, %{"nickname" => nickname}) do
334 user = User.get_cached_by_nickname(nickname)
335
336 conn
337 |> json(%{
338 is_moderator: user.info.is_moderator,
339 is_admin: user.info.is_admin
340 })
341 end
342
343 def right_delete(
344 %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
345 %{
346 "permission_group" => permission_group,
347 "nickname" => nickname
348 }
349 )
350 when permission_group in ["moderator", "admin"] do
351 if admin_nickname == nickname do
352 render_error(conn, :forbidden, "You can't revoke your own admin status.")
353 else
354 user = User.get_cached_by_nickname(nickname)
355
356 info =
357 %{}
358 |> Map.put("is_" <> permission_group, false)
359
360 info_cng = User.Info.admin_api_update(user.info, info)
361
362 cng =
363 Ecto.Changeset.change(user)
364 |> Ecto.Changeset.put_embed(:info, info_cng)
365
366 {:ok, _user} = User.update_and_set_cache(cng)
367
368 ModerationLog.insert_log(%{
369 action: "revoke",
370 actor: admin,
371 subject: user,
372 permission: permission_group
373 })
374
375 json(conn, info)
376 end
377 end
378
379 def right_delete(conn, _) do
380 render_error(conn, :not_found, "No such permission_group")
381 end
382
383 def set_activation_status(%{assigns: %{user: admin}} = conn, %{
384 "nickname" => nickname,
385 "status" => status
386 }) do
387 with {:ok, status} <- Ecto.Type.cast(:boolean, status),
388 %User{} = user <- User.get_cached_by_nickname(nickname),
389 {:ok, _} <- User.deactivate(user, !status) do
390 action = if(user.info.deactivated, do: "activate", else: "deactivate")
391
392 ModerationLog.insert_log(%{
393 actor: admin,
394 subject: user,
395 action: action
396 })
397
398 json_response(conn, :no_content, "")
399 end
400 end
401
402 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
403 with {:ok, _message} <- Relay.follow(target) do
404 ModerationLog.insert_log(%{
405 action: "relay_follow",
406 actor: admin,
407 target: target
408 })
409
410 json(conn, target)
411 else
412 _ ->
413 conn
414 |> put_status(500)
415 |> json(target)
416 end
417 end
418
419 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
420 with {:ok, _message} <- Relay.unfollow(target) do
421 ModerationLog.insert_log(%{
422 action: "relay_unfollow",
423 actor: admin,
424 target: target
425 })
426
427 json(conn, target)
428 else
429 _ ->
430 conn
431 |> put_status(500)
432 |> json(target)
433 end
434 end
435
436 @doc "Sends registration invite via email"
437 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
438 with true <-
439 Pleroma.Config.get([:instance, :invites_enabled]) &&
440 !Pleroma.Config.get([:instance, :registrations_open]),
441 {:ok, invite_token} <- UserInviteToken.create_invite(),
442 email <-
443 Pleroma.Emails.UserEmail.user_invitation_email(
444 user,
445 invite_token,
446 email,
447 params["name"]
448 ),
449 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
450 json_response(conn, :no_content, "")
451 end
452 end
453
454 @doc "Get a account registeration invite token (base64 string)"
455 def get_invite_token(conn, params) do
456 options = params["invite"] || %{}
457 {:ok, invite} = UserInviteToken.create_invite(options)
458
459 conn
460 |> json(invite.token)
461 end
462
463 @doc "Get list of created invites"
464 def invites(conn, _params) do
465 invites = UserInviteToken.list_invites()
466
467 conn
468 |> json(AccountView.render("invites.json", %{invites: invites}))
469 end
470
471 @doc "Revokes invite by token"
472 def revoke_invite(conn, %{"token" => token}) do
473 with {:ok, invite} <- UserInviteToken.find_by_token(token),
474 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
475 conn
476 |> json(AccountView.render("invite.json", %{invite: updated_invite}))
477 else
478 nil -> {:error, :not_found}
479 end
480 end
481
482 @doc "Get a password reset token (base64 string) for given nickname"
483 def get_password_reset(conn, %{"nickname" => nickname}) do
484 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
485 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
486
487 conn
488 |> json(token.token)
489 end
490
491 def list_reports(conn, params) do
492 params =
493 params
494 |> Map.put("type", "Flag")
495 |> Map.put("skip_preload", true)
496
497 reports =
498 []
499 |> ActivityPub.fetch_activities(params)
500 |> Enum.reverse()
501
502 conn
503 |> put_view(ReportView)
504 |> render("index.json", %{reports: reports})
505 end
506
507 def report_show(conn, %{"id" => id}) do
508 with %Activity{} = report <- Activity.get_by_id(id) do
509 conn
510 |> put_view(ReportView)
511 |> render("show.json", %{report: report})
512 else
513 _ -> {:error, :not_found}
514 end
515 end
516
517 def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
518 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
519 ModerationLog.insert_log(%{
520 action: "report_update",
521 actor: admin,
522 subject: report
523 })
524
525 conn
526 |> put_view(ReportView)
527 |> render("show.json", %{report: report})
528 end
529 end
530
531 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
532 with false <- is_nil(params["status"]),
533 %Activity{} <- Activity.get_by_id(id) do
534 params =
535 params
536 |> Map.put("in_reply_to_status_id", id)
537 |> Map.put("visibility", "direct")
538
539 {:ok, activity} = CommonAPI.post(user, params)
540
541 ModerationLog.insert_log(%{
542 action: "report_response",
543 actor: user,
544 subject: activity,
545 text: params["status"]
546 })
547
548 conn
549 |> put_view(StatusView)
550 |> render("status.json", %{activity: activity})
551 else
552 true ->
553 {:param_cast, nil}
554
555 nil ->
556 {:error, :not_found}
557 end
558 end
559
560 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
561 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
562 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
563
564 ModerationLog.insert_log(%{
565 action: "status_update",
566 actor: admin,
567 subject: activity,
568 sensitive: sensitive,
569 visibility: params["visibility"]
570 })
571
572 conn
573 |> put_view(StatusView)
574 |> render("status.json", %{activity: activity})
575 end
576 end
577
578 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
579 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
580 ModerationLog.insert_log(%{
581 action: "status_delete",
582 actor: user,
583 subject_id: id
584 })
585
586 json(conn, %{})
587 end
588 end
589
590 def list_log(conn, params) do
591 {page, page_size} = page_params(params)
592
593 log = ModerationLog.get_all(page, page_size)
594
595 conn
596 |> put_view(ModerationLogView)
597 |> render("index.json", %{log: log})
598 end
599
600 def migrate_to_db(conn, _params) do
601 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
602 json(conn, %{})
603 end
604
605 def migrate_from_db(conn, _params) do
606 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
607 json(conn, %{})
608 end
609
610 def config_show(conn, _params) do
611 configs = Pleroma.Repo.all(Config)
612
613 conn
614 |> put_view(ConfigView)
615 |> render("index.json", %{configs: configs})
616 end
617
618 def config_update(conn, %{"configs" => configs}) do
619 updated =
620 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
621 updated =
622 Enum.map(configs, fn
623 %{"group" => group, "key" => key, "delete" => "true"} = params ->
624 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
625 config
626
627 %{"group" => group, "key" => key, "value" => value} ->
628 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
629 config
630 end)
631 |> Enum.reject(&is_nil(&1))
632
633 Pleroma.Config.TransferTask.load_and_update_env()
634 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
635 updated
636 else
637 []
638 end
639
640 conn
641 |> put_view(ConfigView)
642 |> render("index.json", %{configs: updated})
643 end
644
645 def errors(conn, {:error, :not_found}) do
646 conn
647 |> put_status(:not_found)
648 |> json(dgettext("errors", "Not found"))
649 end
650
651 def errors(conn, {:error, reason}) do
652 conn
653 |> put_status(:bad_request)
654 |> json(reason)
655 end
656
657 def errors(conn, {:param_cast, _}) do
658 conn
659 |> put_status(:bad_request)
660 |> json(dgettext("errors", "Invalid parameters"))
661 end
662
663 def errors(conn, _) do
664 conn
665 |> put_status(:internal_server_error)
666 |> json(dgettext("errors", "Something went wrong"))
667 end
668
669 defp page_params(params) do
670 {get_page(params["page"]), get_page_size(params["page_size"])}
671 end
672
673 defp get_page(page_string) when is_nil(page_string), do: 1
674
675 defp get_page(page_string) do
676 case Integer.parse(page_string) do
677 {page, _} -> page
678 :error -> 1
679 end
680 end
681
682 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
683
684 defp get_page_size(page_size_string) do
685 case Integer.parse(page_size_string) do
686 {page_size, _} -> page_size
687 :error -> @users_page_size
688 end
689 end
690 end