From 36bc7b116d7b66a47f19e53bba3631b010030876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 20 Jul 2026 19:01:02 +0200 Subject: [PATCH] Allow creating accounts without a password in the AdminAPI, update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../admin-api-passwordless-accounts.add | 1 + docs/development/API/admin_api.md | 18 +++++++- .../admin_api/controllers/user_controller.ex | 43 ++++++++++++++++--- .../web/admin_api/views/account_view.ex | 30 ++++++++++--- .../operations/admin/user_operation.ex | 13 ++++-- .../controllers/user_controller_test.exs | 28 ++++++++++++ 6 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 changelog.d/admin-api-passwordless-accounts.add diff --git a/changelog.d/admin-api-passwordless-accounts.add b/changelog.d/admin-api-passwordless-accounts.add new file mode 100644 index 000000000..f5ea2626f --- /dev/null +++ b/changelog.d/admin-api-passwordless-accounts.add @@ -0,0 +1 @@ +Allow creating accounts without a password in the AdminAPI diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 3719ceeb9..ef04944af 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -80,10 +80,24 @@ The `/api/v1/pleroma/admin/*` path is backwards compatible with `/api/pleroma/ad { `nickname`, `email`, - `password` + `password` (optional) } ] -- Response: Array of user objects +- Response: Array of user objects. When a user is created without a `password`, `data` + object contains a `password_reset_link` the user can follow to set their own password. + +```json +[ + { + "data": { + "nickname": string, + "email": string, + "password_reset_link": string (optional) + }, + "type": "success" + } +] +``` ## `POST /api/v1/pleroma/admin/users/follow` diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex index 9ac275396..23af5b53c 100644 --- a/lib/pleroma/web/admin_api/controllers/user_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex @@ -8,13 +8,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 3] + alias Ecto.Multi alias Pleroma.ModerationLog alias Pleroma.User alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Search + alias Pleroma.Web.Endpoint alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Router @users_page_size 50 @@ -148,9 +151,24 @@ defmodule Pleroma.Web.AdminAPI.UserController do } = conn, _ ) do - changesets = + # Nicknames of users for which a password reset link will be generated. + passwordless_nicknames = users - |> Enum.map(fn %{nickname: nickname, email: email, password: password} -> + |> Enum.filter(fn user -> Map.get(user, :password) in ["", nil] end) + |> Enum.map(& &1.nickname) + + multi = + users + |> Enum.map(fn %{nickname: nickname, email: email} = attrs -> + password = + case Map.get(attrs, :password) do + password when is_binary(password) and password != "" -> + password + + _ -> + :crypto.strong_rand_bytes(16) |> Base.encode64() + end + user_data = %{ nickname: nickname, name: nickname, @@ -162,11 +180,11 @@ defmodule Pleroma.Web.AdminAPI.UserController do 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) + |> Enum.reduce(Multi.new(), fn changeset, multi -> + Multi.insert(multi, "user-" <> changeset.changes.nickname, changeset) end) - case Pleroma.Repo.transaction(changesets) do + case Pleroma.Repo.transaction(multi) do {:ok, users_map} -> users = users_map @@ -177,17 +195,28 @@ defmodule Pleroma.Web.AdminAPI.UserController do user end) + password_reset_links = + users + |> Enum.filter(fn user -> Enum.member?(passwordless_nicknames, user.nickname) end) + |> Map.new(fn user -> + {:ok, token} = Pleroma.PasswordResetToken.create_token(user) + {user.id, Router.Helpers.reset_password_url(Endpoint, :reset, token.token)} + end) + ModerationLog.insert_log(%{ actor: admin, subjects: users, action: "create" }) - render(conn, "created_many.json", users: users) + render(conn, "created_many.json", + users: users, + password_reset_links: password_reset_links + ) {:error, id, changeset, _} -> changesets = - Enum.map(changesets.operations, fn + Enum.map(multi.operations, fn {^id, {:changeset, _current_changeset, _}} -> changeset diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 280152241..cc715c2a5 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -88,18 +88,29 @@ defmodule Pleroma.Web.AdminAPI.AccountView do } end - def render("created_many.json", %{users: users}) do - render_many(users, AccountView, "created.json", as: :user) + def render("created_many.json", %{users: users} = assigns) do + password_reset_links = Map.get(assigns, :password_reset_links, %{}) + + render_many(users, AccountView, "created.json", + as: :user, + password_reset_links: password_reset_links + ) end - def render("created.json", %{user: user}) do - %{ - type: "success", - code: 200, - data: %{ + def render("created.json", %{user: user} = assigns) do + password_reset_links = Map.get(assigns, :password_reset_links, %{}) + + data = + %{ nickname: user.nickname, email: user.email } + |> maybe_put_password_reset_link(Map.get(password_reset_links, user.id)) + + %{ + type: "success", + code: 200, + data: data } end @@ -119,6 +130,11 @@ defmodule Pleroma.Web.AdminAPI.AccountView do } end + defp maybe_put_password_reset_link(data, nil), do: data + + defp maybe_put_password_reset_link(data, link), + do: Map.put(data, :password_reset_link, link) + def merge_account_views(%User{} = user) do MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}) |> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user})) diff --git a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex index a5179ac39..84cf789d8 100644 --- a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex @@ -82,8 +82,14 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do properties: %{ nickname: %Schema{type: :string}, email: %Schema{type: :string}, - password: %Schema{type: :string} - } + password: %Schema{ + type: :string, + description: + "Optional. When omitted, a `password_reset_link` is returned, " <> + "allowing the user to set their own password." + } + }, + required: [:nickname, :email] } } } @@ -102,7 +108,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do type: :object, properties: %{ email: %Schema{type: :string, format: :email}, - nickname: %Schema{type: :string} + nickname: %Schema{type: :string}, + password_reset_link: %Schema{type: :string} } } } diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs index 0e5650285..9808f9d0d 100644 --- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs @@ -176,6 +176,34 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] end + test "Creating without a password returns a password reset link", %{conn: conn} do + [account1, account2] = + conn + |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "lain", + "email" => "lain@example.org" + }, + %{ + "nickname" => "lain2", + "email" => "lain2@example.org", + "password" => "dupa.8" + } + ] + }) + |> json_response_and_validate_schema(200) + + assert account1["type"] == "success" + assert %{"password_reset_link" => link} = account1["data"] + assert link =~ "/api/v1/pleroma/password_reset/" + + assert account2["type"] == "success" + refute Map.has_key?(account2["data"], "password_reset_link") + end + test "Cannot create user with existing email", %{conn: conn} do user = insert(:user)