Merge pull request 'Allow creating accounts without a password in the AdminAPI, update docs' (#7937) from mkljczk/pleroma:admin-api-passwordless-user-creation into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7937
This commit is contained in:
commit
8ab0e046b1
8 changed files with 478 additions and 39 deletions
1
changelog.d/admin-api-passwordless-accounts.add
Normal file
1
changelog.d/admin-api-passwordless-accounts.add
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Allow creating accounts without a password in the AdminAPI
|
||||||
|
|
@ -80,10 +80,24 @@ The `/api/v1/pleroma/admin/*` path is backwards compatible with `/api/pleroma/ad
|
||||||
{
|
{
|
||||||
`nickname`,
|
`nickname`,
|
||||||
`email`,
|
`email`,
|
||||||
`password`
|
`password` (optional; when provided, must not be empty)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
- 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`
|
## `POST /api/v1/pleroma/admin/users/follow`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,17 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
||||||
import Pleroma.Web.ControllerHelper,
|
import Pleroma.Web.ControllerHelper,
|
||||||
only: [fetch_integer_param: 3]
|
only: [fetch_integer_param: 3]
|
||||||
|
|
||||||
|
alias Ecto.Multi
|
||||||
alias Pleroma.ModerationLog
|
alias Pleroma.ModerationLog
|
||||||
|
alias Pleroma.PasswordResetToken
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.Builder
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
alias Pleroma.Web.ActivityPub.Pipeline
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
alias Pleroma.Web.AdminAPI
|
alias Pleroma.Web.AdminAPI
|
||||||
alias Pleroma.Web.AdminAPI.Search
|
alias Pleroma.Web.AdminAPI.Search
|
||||||
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.Router
|
||||||
|
|
||||||
@users_page_size 50
|
@users_page_size 50
|
||||||
|
|
||||||
|
|
@ -148,9 +152,25 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
||||||
} = conn,
|
} = conn,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
changesets =
|
conn =
|
||||||
|
if Enum.any?(users, fn user -> not Map.has_key?(user, :password) end) do
|
||||||
|
put_private(conn, :skip_idempotency_cache, true)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
|
||||||
|
multi =
|
||||||
users
|
users
|
||||||
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
|
|> Enum.map(fn %{nickname: nickname, email: email} = attrs ->
|
||||||
|
{password, passwordless?} =
|
||||||
|
case Map.fetch(attrs, :password) do
|
||||||
|
{:ok, password} ->
|
||||||
|
{password, false}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
|
||||||
|
end
|
||||||
|
|
||||||
user_data = %{
|
user_data = %{
|
||||||
nickname: nickname,
|
nickname: nickname,
|
||||||
name: nickname,
|
name: nickname,
|
||||||
|
|
@ -160,44 +180,80 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
||||||
bio: "."
|
bio: "."
|
||||||
}
|
}
|
||||||
|
|
||||||
User.register_changeset(%User{}, user_data, need_confirmation: false)
|
{User.register_changeset(%User{}, user_data, confirmed: true), passwordless?}
|
||||||
end)
|
end)
|
||||||
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
|
|> Enum.reduce(Multi.new(), fn {changeset, passwordless?}, multi ->
|
||||||
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
|
user_operation = {:user, Ecto.UUID.generate()}
|
||||||
|
multi = Multi.insert(multi, user_operation, changeset)
|
||||||
|
|
||||||
|
if passwordless? do
|
||||||
|
Multi.run(
|
||||||
|
multi,
|
||||||
|
{:password_reset_token, Ecto.UUID.generate()},
|
||||||
|
fn _repo, changes ->
|
||||||
|
changes
|
||||||
|
|> Map.fetch!(user_operation)
|
||||||
|
|> PasswordResetToken.create_token()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
else
|
||||||
|
multi
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
case Pleroma.Repo.transaction(changesets) do
|
case Pleroma.Repo.transaction(multi) do
|
||||||
{:ok, users_map} ->
|
{:ok, results} ->
|
||||||
users =
|
{users, password_reset_tokens} =
|
||||||
users_map
|
results
|
||||||
|> Map.values()
|
|> Map.values()
|
||||||
|
|> Enum.split_with(&match?(%User{}, &1))
|
||||||
|
|
||||||
|
users =
|
||||||
|
users
|
||||||
|> Enum.map(fn user ->
|
|> Enum.map(fn user ->
|
||||||
{:ok, user} = User.post_register_action(user)
|
{:ok, user} = User.post_register_action(user)
|
||||||
|
|
||||||
user
|
user
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
password_reset_links =
|
||||||
|
Map.new(password_reset_tokens, fn token ->
|
||||||
|
{token.user_id, Router.Helpers.reset_password_url(Endpoint, :reset, token.token)}
|
||||||
|
end)
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
actor: admin,
|
actor: admin,
|
||||||
subjects: users,
|
subjects: users,
|
||||||
action: "create"
|
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, _} ->
|
{:error, {:user, _} = id, %Ecto.Changeset{} = changeset, _} ->
|
||||||
changesets =
|
changesets =
|
||||||
Enum.map(changesets.operations, fn
|
Enum.flat_map(multi.operations, fn
|
||||||
{^id, {:changeset, _current_changeset, _}} ->
|
{^id, {:changeset, _current_changeset, _}} ->
|
||||||
changeset
|
[changeset]
|
||||||
|
|
||||||
{_, {:changeset, current_changeset, _}} ->
|
{{:user, _}, {:changeset, current_changeset, _}} ->
|
||||||
current_changeset
|
[current_changeset]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
end)
|
end)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_status(:conflict)
|
|> put_status(:conflict)
|
||||||
|> render("create_errors.json", changesets: changesets)
|
|> render("create_errors.json", changesets: changesets)
|
||||||
|
|
||||||
|
{:error, {:password_reset_token, _}, _reason, _} ->
|
||||||
|
Pleroma.Web.AdminAPI.FallbackController.call(
|
||||||
|
conn,
|
||||||
|
{:password_reset_token_error, :creation_failed}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,18 +88,29 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("created_many.json", %{users: users}) do
|
def render("created_many.json", %{users: users} = assigns) do
|
||||||
render_many(users, AccountView, "created.json", as: :user)
|
password_reset_links = Map.get(assigns, :password_reset_links, %{})
|
||||||
|
|
||||||
|
render_many(users, AccountView, "created.json",
|
||||||
|
as: :user,
|
||||||
|
password_reset_links: password_reset_links
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("created.json", %{user: user}) do
|
def render("created.json", %{user: user} = assigns) do
|
||||||
%{
|
password_reset_links = Map.get(assigns, :password_reset_links, %{})
|
||||||
type: "success",
|
|
||||||
code: 200,
|
data =
|
||||||
data: %{
|
%{
|
||||||
nickname: user.nickname,
|
nickname: user.nickname,
|
||||||
email: user.email
|
email: user.email
|
||||||
}
|
}
|
||||||
|
|> maybe_put_password_reset_link(Map.get(password_reset_links, user.id))
|
||||||
|
|
||||||
|
%{
|
||||||
|
type: "success",
|
||||||
|
code: 200,
|
||||||
|
data: data
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -119,6 +130,11 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
||||||
}
|
}
|
||||||
end
|
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
|
def merge_account_views(%User{} = user) do
|
||||||
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
||||||
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,15 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
|
||||||
properties: %{
|
properties: %{
|
||||||
nickname: %Schema{type: :string},
|
nickname: %Schema{type: :string},
|
||||||
email: %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. When provided, " <>
|
||||||
|
"it must not be empty."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: [:nickname, :email]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +109,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
email: %Schema{type: :string, format: :email},
|
email: %Schema{type: :string, format: :email},
|
||||||
nickname: %Schema{type: :string}
|
nickname: %Schema{type: :string},
|
||||||
|
password_reset_link: %Schema{type: :string}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
|
||||||
import Phoenix.Controller, only: [json: 2]
|
import Phoenix.Controller, only: [json: 2]
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
|
||||||
@behaviour Plug
|
@behaviour Plug
|
||||||
|
|
||||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
|
|
@ -27,9 +29,11 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
|
||||||
def call(conn, _), do: conn
|
def call(conn, _), do: conn
|
||||||
|
|
||||||
def process_request(conn, key) do
|
def process_request(conn, key) do
|
||||||
case @cachex.get(:idempotency_cache, key) do
|
cache_key = {conn.method, conn.request_path, actor_key(conn), key}
|
||||||
|
|
||||||
|
case @cachex.get(:idempotency_cache, cache_key) do
|
||||||
{:ok, nil} ->
|
{:ok, nil} ->
|
||||||
cache_resposnse(conn, key)
|
cache_resposnse(conn, key, cache_key)
|
||||||
|
|
||||||
{:ok, record} ->
|
{:ok, record} ->
|
||||||
send_cached(conn, key, record)
|
send_cached(conn, key, record)
|
||||||
|
|
@ -39,18 +43,48 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp cache_resposnse(conn, key) do
|
defp cache_resposnse(conn, key, cache_key) do
|
||||||
register_before_send(conn, fn conn ->
|
register_before_send(conn, &maybe_cache_response(&1, key, cache_key))
|
||||||
[request_id] = get_resp_header(conn, "x-request-id")
|
end
|
||||||
content_type = get_content_type(conn)
|
|
||||||
|
|
||||||
record = {request_id, content_type, conn.status, conn.resp_body}
|
defp maybe_cache_response(
|
||||||
{:ok, _} = @cachex.put(:idempotency_cache, key, record)
|
%{private: %{skip_idempotency_cache: true}} = conn,
|
||||||
|
_key,
|
||||||
|
_cache_key
|
||||||
|
),
|
||||||
|
do: conn
|
||||||
|
|
||||||
conn
|
defp maybe_cache_response(conn, key, cache_key) do
|
||||||
|> put_resp_header("idempotency-key", key)
|
[request_id] = get_resp_header(conn, "x-request-id")
|
||||||
|> put_resp_header("x-original-request-id", request_id)
|
content_type = get_content_type(conn)
|
||||||
end)
|
|
||||||
|
record = {request_id, content_type, conn.status, conn.resp_body}
|
||||||
|
{:ok, _} = @cachex.put(:idempotency_cache, cache_key, record)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_header("idempotency-key", key)
|
||||||
|
|> put_resp_header("x-original-request-id", request_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_key(%{assigns: assigns}) do
|
||||||
|
{principal_key(assigns), token_key(assigns[:token]), staff_privileges()}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp principal_key(%{user: %{id: id} = user}) do
|
||||||
|
{:user, id, Map.get(user, :is_admin), Map.get(user, :is_moderator)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp principal_key(%{app: %{id: id}}), do: {:app, id}
|
||||||
|
defp principal_key(_assigns), do: nil
|
||||||
|
|
||||||
|
defp token_key(%{id: id} = token), do: {id, token |> Map.get(:scopes, []) |> Enum.sort()}
|
||||||
|
defp token_key(_token), do: nil
|
||||||
|
|
||||||
|
defp staff_privileges do
|
||||||
|
{
|
||||||
|
Config.get([:instance, :admin_privileges], []) |> Enum.sort(),
|
||||||
|
Config.get([:instance, :moderator_privileges], []) |> Enum.sort()
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp send_cached(conn, key, record) do
|
defp send_cached(conn, key, record) do
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,183 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do
|
||||||
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
|
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "Admin-created users bypass account confirmation", %{conn: conn} do
|
||||||
|
clear_config([:instance, :account_activation_required], true)
|
||||||
|
|
||||||
|
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", "password" => "test"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert User.get_by_nickname("lain").is_confirmed
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Creating without a password returns a password reset link", %{conn: conn} do
|
||||||
|
accounts =
|
||||||
|
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)
|
||||||
|
|> Map.new(fn account -> {account["data"]["nickname"], account} end)
|
||||||
|
|
||||||
|
account1 = accounts["lain"]
|
||||||
|
account2 = accounts["lain2"]
|
||||||
|
|
||||||
|
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 "Explicit empty passwords return a conflict", %{conn: conn} do
|
||||||
|
conn =
|
||||||
|
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", "password" => ""}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert [%{"error" => "password can't be blank"}] = json_response(conn, 409)
|
||||||
|
refute User.get_by_nickname("lain")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Password reset token failures roll back the user batch", %{conn: conn} do
|
||||||
|
idempotency_key = Ecto.UUID.generate()
|
||||||
|
|
||||||
|
request = fn conn ->
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/json")
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put_req_header("idempotency-key", idempotency_key)
|
||||||
|
|> post("/api/pleroma/admin/users", %{
|
||||||
|
"users" => [
|
||||||
|
%{"nickname" => "lain", "email" => "lain@example.org"},
|
||||||
|
%{"nickname" => "lain2", "email" => "lain2@example.org"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
with_mock Pleroma.PasswordResetToken, [:passthrough],
|
||||||
|
create_token: fn user ->
|
||||||
|
if user.nickname == "lain2" do
|
||||||
|
{:error, :forced}
|
||||||
|
else
|
||||||
|
result = passthrough([user])
|
||||||
|
send(self(), {:token_created, user.nickname})
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end do
|
||||||
|
conn = request.(conn)
|
||||||
|
|
||||||
|
assert json_response(conn, 500) == %{"error" => "Something went wrong"}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_received {:token_created, "lain"}
|
||||||
|
refute User.get_by_nickname("lain")
|
||||||
|
refute User.get_by_nickname("lain2")
|
||||||
|
assert Repo.aggregate(Pleroma.PasswordResetToken, :count) == 0
|
||||||
|
|
||||||
|
assert [_account1, _account2] = conn |> request.() |> json_response(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Password reset links are not cached by idempotency key", %{conn: conn} do
|
||||||
|
idempotency_key = Ecto.UUID.generate()
|
||||||
|
|
||||||
|
request = fn conn ->
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/json")
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put_req_header("idempotency-key", idempotency_key)
|
||||||
|
|> post("/api/pleroma/admin/users", %{
|
||||||
|
"users" => [%{"nickname" => "lain", "email" => "lain@example.org"}]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
assert [%{"data" => %{"password_reset_link" => _link}}] =
|
||||||
|
conn |> request.() |> json_response(200)
|
||||||
|
|
||||||
|
second_conn = request.(conn)
|
||||||
|
|
||||||
|
assert [_error] = json_response(second_conn, 409)
|
||||||
|
refute get_resp_header(second_conn, "idempotent-replayed") == ["true"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Explicit-password responses remain idempotent", %{conn: conn} do
|
||||||
|
idempotency_key = Ecto.UUID.generate()
|
||||||
|
|
||||||
|
request = fn conn ->
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/json")
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put_req_header("idempotency-key", idempotency_key)
|
||||||
|
|> post("/api/pleroma/admin/users", %{
|
||||||
|
"users" => [
|
||||||
|
%{"nickname" => "lain", "email" => "lain@example.org", "password" => "test"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
assert [_account] = conn |> request.() |> json_response(200)
|
||||||
|
|
||||||
|
second_conn = request.(conn)
|
||||||
|
|
||||||
|
assert [_account] = json_response(second_conn, 200)
|
||||||
|
assert get_resp_header(second_conn, "idempotent-replayed") == ["true"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Duplicate nicknames in a batch return a conflict", %{conn: conn} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/json")
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> post("/api/pleroma/admin/users", %{
|
||||||
|
"users" => [
|
||||||
|
%{"nickname" => "lain", "email" => "lain1@example.org", "password" => "test"},
|
||||||
|
%{"nickname" => "lain", "email" => "lain2@example.org", "password" => "test"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert [_first, _second] = json_response(conn, 409)
|
||||||
|
refute User.get_by_nickname("lain")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Blank nicknames return a conflict", %{conn: conn} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/json")
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> post("/api/pleroma/admin/users", %{
|
||||||
|
"users" => [
|
||||||
|
%{"nickname" => "", "email" => "lain@example.org", "password" => "test"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert [%{"error" => "nickname can't be blank"}] = json_response(conn, 409)
|
||||||
|
end
|
||||||
|
|
||||||
test "Cannot create user with existing email", %{conn: conn} do
|
test "Cannot create user with existing email", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,139 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do
|
||||||
assert [^key] = Conn.get_resp_header(conn, "idempotency-key")
|
assert [^key] = Conn.get_resp_header(conn, "idempotency-key")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "does not cache responses marked as sensitive" do
|
||||||
|
key = Ecto.UUID.generate()
|
||||||
|
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", "sensitive-request")
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|> Conn.put_private(:skip_idempotency_cache, true)
|
||||||
|
|> Conn.send_resp(200, "secret")
|
||||||
|
|
||||||
|
conn =
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", "second-request")
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|
||||||
|
refute conn.halted
|
||||||
|
refute conn.resp_body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not replay legacy unscoped cache entries" do
|
||||||
|
key = Ecto.UUID.generate()
|
||||||
|
record = {"legacy-request", "application/json", 200, "secret"}
|
||||||
|
assert {:ok, true} = Cachex.put(:idempotency_cache, key, record)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", "new-request")
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|
||||||
|
refute conn.halted
|
||||||
|
refute conn.resp_body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "scopes cached responses to the route and authenticated actor" do
|
||||||
|
key = Ecto.UUID.generate()
|
||||||
|
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> Conn.assign(:user, %{id: "user-one", is_admin: false, is_moderator: false})
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", "original-request")
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|> Conn.send_resp(200, "one")
|
||||||
|
|
||||||
|
for {path, user} <- [
|
||||||
|
{"/tea", %{id: "user-one", is_admin: false, is_moderator: false}},
|
||||||
|
{"/cofe", %{id: "user-two", is_admin: false, is_moderator: false}},
|
||||||
|
{"/cofe", %{id: "user-one", is_admin: true, is_moderator: false}}
|
||||||
|
] do
|
||||||
|
conn =
|
||||||
|
:post
|
||||||
|
|> conn(path)
|
||||||
|
|> Conn.assign(:user, user)
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", Ecto.UUID.generate())
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|
||||||
|
refute conn.halted
|
||||||
|
refute conn.resp_body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "scopes cached responses to the OAuth application and token" do
|
||||||
|
key = Ecto.UUID.generate()
|
||||||
|
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> Conn.assign(:app, %{id: "app-one"})
|
||||||
|
|> Conn.assign(:token, %{id: "token-one", scopes: ["write"]})
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", "original-request")
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|> Conn.send_resp(200, "one")
|
||||||
|
|
||||||
|
for {app, token} <- [
|
||||||
|
{%{id: "app-two"}, %{id: "token-two", scopes: ["write"]}},
|
||||||
|
{%{id: "app-one"}, %{id: "token-two", scopes: ["write"]}},
|
||||||
|
{%{id: "app-one"}, %{id: "token-one", scopes: ["read"]}}
|
||||||
|
] do
|
||||||
|
conn =
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> Conn.assign(:app, app)
|
||||||
|
|> Conn.assign(:token, token)
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", Ecto.UUID.generate())
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|
||||||
|
refute conn.halted
|
||||||
|
refute conn.resp_body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "scopes cached responses to configured staff privileges" do
|
||||||
|
key = Ecto.UUID.generate()
|
||||||
|
clear_config([:instance, :admin_privileges], [:users_delete])
|
||||||
|
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> Conn.assign(:user, %{id: "admin", is_admin: true, is_moderator: false})
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", "original-request")
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|> Conn.send_resp(200, "one")
|
||||||
|
|
||||||
|
clear_config([:instance, :admin_privileges], [])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
:post
|
||||||
|
|> conn("/cofe")
|
||||||
|
|> Conn.assign(:user, %{id: "admin", is_admin: true, is_moderator: false})
|
||||||
|
|> put_req_header("idempotency-key", key)
|
||||||
|
|> Conn.put_resp_header("x-request-id", "second-request")
|
||||||
|
|> Conn.put_resp_content_type("application/json")
|
||||||
|
|> IdempotencyPlug.call([])
|
||||||
|
|
||||||
|
refute conn.halted
|
||||||
|
refute conn.resp_body
|
||||||
|
end
|
||||||
|
|
||||||
test "passes conn downstream if idempotency is not present in headers" do
|
test "passes conn downstream if idempotency is not present in headers" do
|
||||||
orig_request_id = "test4"
|
orig_request_id = "test4"
|
||||||
body = "testing"
|
body = "testing"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue