@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||
## [Unreleased] | |||
### Added | |||
- Refreshing poll results for remote polls | |||
- Admin API: Add ability to force user's password reset | |||
### Changed | |||
- **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) | |||
@@ -310,6 +310,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||
- Params: none | |||
- 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` | |||
### Get a list of reports | |||
- Method `GET` | |||
@@ -269,6 +269,7 @@ defmodule Pleroma.User do | |||
|> validate_required([:password, :password_confirmation]) | |||
|> validate_confirmation(:password) | |||
|> put_password_hash | |||
|> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false)) | |||
end | |||
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} | |||
@@ -285,6 +286,20 @@ defmodule Pleroma.User do | |||
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 | |||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) | |||
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}) | |||
end | |||
def perform(:force_password_reset, user), do: force_password_reset(user) | |||
@spec perform(atom(), User.t()) :: {:ok, User.t()} | |||
def perform(:delete, %User{} = user) do | |||
{:ok, _user} = ActivityPub.delete(user) | |||
@@ -20,6 +20,7 @@ defmodule Pleroma.User.Info do | |||
field(:following_count, :integer, default: nil) | |||
field(:locked, :boolean, default: false) | |||
field(:confirmation_pending, :boolean, default: false) | |||
field(:password_reset_pending, :boolean, default: false) | |||
field(:confirmation_token, :string, default: nil) | |||
field(:default_scope, :string, default: "public") | |||
field(:blocks, {:array, :string}, default: []) | |||
@@ -82,6 +83,14 @@ defmodule Pleroma.User.Info do | |||
|> validate_required([:deactivated]) | |||
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 | |||
settings = | |||
settings | |||
@@ -333,9 +342,7 @@ defmodule Pleroma.User.Info do | |||
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) | |||
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) | |||
is_binary(name) && | |||
is_binary(value) && | |||
String.length(name) <= name_limit && | |||
is_binary(name) && is_binary(value) && String.length(name) <= name_limit && | |||
String.length(value) <= value_limit | |||
end | |||
@@ -447,6 +447,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
|> json(token.token) | |||
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 | |||
params = | |||
params | |||
@@ -202,6 +202,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
{:ok, app} <- Token.Utils.fetch_app(conn), | |||
{:auth_active, true} <- {:auth_active, User.auth_active?(user)}, | |||
{: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, auth} <- Authorization.create_authorization(app, user, scopes), | |||
{:ok, token} <- Token.exchange_token(app, auth) do | |||
@@ -215,6 +217,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
{:user_active, false} -> | |||
render_error(conn, :forbidden, "Your account is currently disabled") | |||
{:password_reset_pending, true} -> | |||
render_error(conn, :forbidden, "Password reset is required") | |||
_error -> | |||
render_invalid_credentials_error(conn) | |||
end | |||
@@ -186,6 +186,7 @@ defmodule Pleroma.Web.Router do | |||
post("/users/email_invite", AdminAPIController, :email_invite) | |||
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/:nickname", AdminAPIController, :user_show) | |||
@@ -26,6 +26,11 @@ defmodule Pleroma.Workers.BackgroundWorker do | |||
User.perform(:delete, user) | |||
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( | |||
%{ | |||
"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") | |||
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 |
@@ -4,11 +4,13 @@ | |||
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do | |||
use Pleroma.Web.ConnCase | |||
use Oban.Testing, repo: Pleroma.Repo | |||
alias Pleroma.Activity | |||
alias Pleroma.HTML | |||
alias Pleroma.ModerationLog | |||
alias Pleroma.Repo | |||
alias Pleroma.Tests.ObanHelpers | |||
alias Pleroma.User | |||
alias Pleroma.UserInviteToken | |||
alias Pleroma.Web.CommonAPI | |||
@@ -2351,6 +2353,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do | |||
"@#{admin.nickname} followed relay: https://example.org/relay" | |||
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 | |||
# Needed for testing | |||
@@ -831,6 +831,33 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
refute Map.has_key?(resp, "access_token") | |||
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 | |||
app = insert(:oauth_app) | |||
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do | |||
use Pleroma.Web.ConnCase | |||
alias Pleroma.PasswordResetToken | |||
alias Pleroma.User | |||
alias Pleroma.Web.OAuth.Token | |||
import Pleroma.Factory | |||
@@ -56,5 +57,25 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do | |||
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash) | |||
assert length(Token.get_user_tokens(user)) == 0 | |||
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 |