Admin API: Add ability to require password reset See merge request pleroma/pleroma!1705object-id-column
@@ -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 require 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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.User.Info 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 | ||||
@@ -453,6 +453,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
{: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 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
{: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 @@ defmodule Pleroma.Workers.BackgroundWorker 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 @@ defmodule Pleroma.UserTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||||
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 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest 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 |