Admin API: Add ability to force user's password reset
This commit is contained in:
parent
c4da7499a3
commit
6f25668215
12 changed files with 148 additions and 3 deletions
|
@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Refreshing poll results for remote polls
|
- Refreshing poll results for remote polls
|
||||||
|
- Admin API: Add ability to force user's password reset
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
||||||
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
|
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
|
||||||
|
|
|
@ -310,6 +310,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
- Params: none
|
- Params: none
|
||||||
- Response: password reset token (base64 string)
|
- Response: password reset token (base64 string)
|
||||||
|
|
||||||
|
## `/api/pleroma/admin/users/:nickname/force_password_reset`
|
||||||
|
|
||||||
|
### Force passord reset for a user with a given nickname
|
||||||
|
|
||||||
|
- Methods: `PATCH`
|
||||||
|
- Params: none
|
||||||
|
- Response: none (code `204`)
|
||||||
|
|
||||||
## `/api/pleroma/admin/reports`
|
## `/api/pleroma/admin/reports`
|
||||||
### Get a list of reports
|
### Get a list of reports
|
||||||
- Method `GET`
|
- Method `GET`
|
||||||
|
|
|
@ -269,6 +269,7 @@ def password_update_changeset(struct, params) do
|
||||||
|> validate_required([:password, :password_confirmation])
|
|> validate_required([:password, :password_confirmation])
|
||||||
|> validate_confirmation(:password)
|
|> validate_confirmation(:password)
|
||||||
|> put_password_hash
|
|> put_password_hash
|
||||||
|
|> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
@ -285,6 +286,20 @@ def reset_password(%User{id: user_id} = user, data) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def force_password_reset_async(user) do
|
||||||
|
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def force_password_reset(user) do
|
||||||
|
info_cng = User.Info.set_password_reset_pending(user.info, true)
|
||||||
|
|
||||||
|
user
|
||||||
|
|> change()
|
||||||
|
|> put_embed(:info, info_cng)
|
||||||
|
|> update_and_set_cache()
|
||||||
|
end
|
||||||
|
|
||||||
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||||
|
@ -1115,6 +1130,8 @@ def delete(%User{} = user) do
|
||||||
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def perform(:force_password_reset, user), do: force_password_reset(user)
|
||||||
|
|
||||||
@spec perform(atom(), User.t()) :: {:ok, User.t()}
|
@spec perform(atom(), User.t()) :: {:ok, User.t()}
|
||||||
def perform(:delete, %User{} = user) do
|
def perform(:delete, %User{} = user) do
|
||||||
{:ok, _user} = ActivityPub.delete(user)
|
{:ok, _user} = ActivityPub.delete(user)
|
||||||
|
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.User.Info do
|
||||||
field(:following_count, :integer, default: nil)
|
field(:following_count, :integer, default: nil)
|
||||||
field(:locked, :boolean, default: false)
|
field(:locked, :boolean, default: false)
|
||||||
field(:confirmation_pending, :boolean, default: false)
|
field(:confirmation_pending, :boolean, default: false)
|
||||||
|
field(:password_reset_pending, :boolean, default: false)
|
||||||
field(:confirmation_token, :string, default: nil)
|
field(:confirmation_token, :string, default: nil)
|
||||||
field(:default_scope, :string, default: "public")
|
field(:default_scope, :string, default: "public")
|
||||||
field(:blocks, {:array, :string}, default: [])
|
field(:blocks, {:array, :string}, default: [])
|
||||||
|
@ -82,6 +83,14 @@ def set_activation_status(info, deactivated) do
|
||||||
|> validate_required([:deactivated])
|
|> validate_required([:deactivated])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_password_reset_pending(info, pending) do
|
||||||
|
params = %{password_reset_pending: pending}
|
||||||
|
|
||||||
|
info
|
||||||
|
|> cast(params, [:password_reset_pending])
|
||||||
|
|> validate_required([:password_reset_pending])
|
||||||
|
end
|
||||||
|
|
||||||
def update_notification_settings(info, settings) do
|
def update_notification_settings(info, settings) do
|
||||||
settings =
|
settings =
|
||||||
settings
|
settings
|
||||||
|
@ -333,9 +342,7 @@ defp valid_field?(%{"name" => name, "value" => value}) do
|
||||||
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
|
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
|
||||||
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
|
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
|
||||||
|
|
||||||
is_binary(name) &&
|
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
|
||||||
is_binary(value) &&
|
|
||||||
String.length(name) <= name_limit &&
|
|
||||||
String.length(value) <= value_limit
|
String.length(value) <= value_limit
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -447,6 +447,15 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|
||||||
|> json(token.token)
|
|> json(token.token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Force password reset for a given user"
|
||||||
|
def force_password_reset(conn, %{"nickname" => nickname}) do
|
||||||
|
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
|
||||||
|
|
||||||
|
User.force_password_reset_async(user)
|
||||||
|
|
||||||
|
json_response(conn, :no_content, "")
|
||||||
|
end
|
||||||
|
|
||||||
def list_reports(conn, params) do
|
def list_reports(conn, params) do
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|
|
|
@ -202,6 +202,8 @@ def token_exchange(
|
||||||
{:ok, app} <- Token.Utils.fetch_app(conn),
|
{:ok, app} <- Token.Utils.fetch_app(conn),
|
||||||
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
|
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
|
||||||
{:user_active, true} <- {:user_active, !user.info.deactivated},
|
{:user_active, true} <- {:user_active, !user.info.deactivated},
|
||||||
|
{:password_reset_pending, false} <-
|
||||||
|
{:password_reset_pending, user.info.password_reset_pending},
|
||||||
{:ok, scopes} <- validate_scopes(app, params),
|
{:ok, scopes} <- validate_scopes(app, params),
|
||||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
|
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
|
||||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||||
|
@ -215,6 +217,9 @@ def token_exchange(
|
||||||
{:user_active, false} ->
|
{:user_active, false} ->
|
||||||
render_error(conn, :forbidden, "Your account is currently disabled")
|
render_error(conn, :forbidden, "Your account is currently disabled")
|
||||||
|
|
||||||
|
{:password_reset_pending, true} ->
|
||||||
|
render_error(conn, :forbidden, "Password reset is required")
|
||||||
|
|
||||||
_error ->
|
_error ->
|
||||||
render_invalid_credentials_error(conn)
|
render_invalid_credentials_error(conn)
|
||||||
end
|
end
|
||||||
|
|
|
@ -186,6 +186,7 @@ defmodule Pleroma.Web.Router do
|
||||||
post("/users/email_invite", AdminAPIController, :email_invite)
|
post("/users/email_invite", AdminAPIController, :email_invite)
|
||||||
|
|
||||||
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
|
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
|
||||||
|
patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
|
||||||
|
|
||||||
get("/users", AdminAPIController, :list_users)
|
get("/users", AdminAPIController, :list_users)
|
||||||
get("/users/:nickname", AdminAPIController, :user_show)
|
get("/users/:nickname", AdminAPIController, :user_show)
|
||||||
|
|
|
@ -26,6 +26,11 @@ def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do
|
||||||
User.perform(:delete, user)
|
User.perform(:delete, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do
|
||||||
|
user = User.get_cached_by_id(user_id)
|
||||||
|
User.perform(:force_password_reset, user)
|
||||||
|
end
|
||||||
|
|
||||||
def perform(
|
def perform(
|
||||||
%{
|
%{
|
||||||
"op" => "blocks_import",
|
"op" => "blocks_import",
|
||||||
|
|
|
@ -1690,4 +1690,21 @@ test "changes email", %{user: user} do
|
||||||
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
|
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "set_password_reset_pending/2" do
|
||||||
|
setup do
|
||||||
|
[user: insert(:user)]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets password_reset_pending to true", %{user: user} do
|
||||||
|
%{password_reset_pending: password_reset_pending} = user.info
|
||||||
|
|
||||||
|
refute password_reset_pending
|
||||||
|
|
||||||
|
{:ok, %{info: %{password_reset_pending: password_reset_pending}}} =
|
||||||
|
User.force_password_reset(user)
|
||||||
|
|
||||||
|
assert password_reset_pending
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|
||||||
use Pleroma.Web.ConnCase
|
use Pleroma.Web.ConnCase
|
||||||
|
use Oban.Testing, repo: Pleroma.Repo
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.HTML
|
alias Pleroma.HTML
|
||||||
alias Pleroma.ModerationLog
|
alias Pleroma.ModerationLog
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.Tests.ObanHelpers
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.UserInviteToken
|
alias Pleroma.UserInviteToken
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
@ -2351,6 +2353,30 @@ test "returns the log with pagination", %{conn: conn, admin: admin} do
|
||||||
"@#{admin.nickname} followed relay: https://example.org/relay"
|
"@#{admin.nickname} followed relay: https://example.org/relay"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "PATCH /users/:nickname/force_password_reset" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
admin = insert(:user, info: %{is_admin: true})
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
%{conn: assign(conn, :user, admin), admin: admin, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets password_reset_pending to true", %{admin: admin, user: user} do
|
||||||
|
assert user.info.password_reset_pending == false
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, admin)
|
||||||
|
|> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset")
|
||||||
|
|
||||||
|
assert json_response(conn, 204) == ""
|
||||||
|
|
||||||
|
ObanHelpers.perform_all()
|
||||||
|
|
||||||
|
assert User.get_by_id(user.id).info.password_reset_pending == true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Needed for testing
|
# Needed for testing
|
||||||
|
|
|
@ -831,6 +831,33 @@ test "rejects token exchange for valid credentials belonging to deactivated user
|
||||||
refute Map.has_key?(resp, "access_token")
|
refute Map.has_key?(resp, "access_token")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rejects token exchange for user with password_reset_pending set to true" do
|
||||||
|
password = "testpassword"
|
||||||
|
|
||||||
|
user =
|
||||||
|
insert(:user,
|
||||||
|
password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
|
||||||
|
info: %{password_reset_pending: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
app = insert(:oauth_app, scopes: ["read", "write"])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> post("/oauth/token", %{
|
||||||
|
"grant_type" => "password",
|
||||||
|
"username" => user.nickname,
|
||||||
|
"password" => password,
|
||||||
|
"client_id" => app.client_id,
|
||||||
|
"client_secret" => app.client_secret
|
||||||
|
})
|
||||||
|
|
||||||
|
assert resp = json_response(conn, 403)
|
||||||
|
|
||||||
|
assert resp["error"] == "Password reset is required"
|
||||||
|
refute Map.has_key?(resp, "access_token")
|
||||||
|
end
|
||||||
|
|
||||||
test "rejects an invalid authorization code" do
|
test "rejects an invalid authorization code" do
|
||||||
app = insert(:oauth_app)
|
app = insert(:oauth_app)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
|
||||||
use Pleroma.Web.ConnCase
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
alias Pleroma.PasswordResetToken
|
alias Pleroma.PasswordResetToken
|
||||||
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
@ -56,5 +57,25 @@ test "it returns HTTP 200", %{conn: conn} do
|
||||||
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
|
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
|
||||||
assert length(Token.get_user_tokens(user)) == 0
|
assert length(Token.get_user_tokens(user)) == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it sets password_reset_pending to false", %{conn: conn} do
|
||||||
|
user = insert(:user, info: %{password_reset_pending: true})
|
||||||
|
|
||||||
|
{:ok, token} = PasswordResetToken.create_token(user)
|
||||||
|
{:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{})
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
"password" => "test",
|
||||||
|
password_confirmation: "test",
|
||||||
|
token: token.token
|
||||||
|
}
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/pleroma/password_reset", %{data: params})
|
||||||
|
|> html_response(:ok)
|
||||||
|
|
||||||
|
assert User.get_by_id(user.id).info.password_reset_pending == false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue