Browse Source

Merge branch 'feature/force-password-reset' into 'develop'

Admin API: Add ability to require password reset

See merge request pleroma/pleroma!1705
object-id-column
kaniini 4 years ago
parent
commit
29dd8ab9c0
12 changed files with 147 additions and 0 deletions
  1. +2
    -0
      CHANGELOG.md
  2. +8
    -0
      docs/api/admin_api.md
  3. +17
    -0
      lib/pleroma/user.ex
  4. +9
    -0
      lib/pleroma/user/info.ex
  5. +9
    -0
      lib/pleroma/web/admin_api/admin_api_controller.ex
  6. +5
    -0
      lib/pleroma/web/oauth/oauth_controller.ex
  7. +1
    -0
      lib/pleroma/web/router.ex
  8. +5
    -0
      lib/pleroma/workers/background_worker.ex
  9. +17
    -0
      test/user_test.exs
  10. +26
    -0
      test/web/admin_api/admin_api_controller_test.exs
  11. +27
    -0
      test/web/oauth/oauth_controller_test.exs
  12. +21
    -0
      test/web/twitter_api/password_controller_test.exs

+ 2
- 0
CHANGELOG.md View File

@@ -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)


+ 8
- 0
docs/api/admin_api.md View File

@@ -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`


+ 17
- 0
lib/pleroma/user.ex View File

@@ -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)


+ 9
- 0
lib/pleroma/user/info.ex View File

@@ -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


+ 9
- 0
lib/pleroma/web/admin_api/admin_api_controller.ex View File

@@ -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


+ 5
- 0
lib/pleroma/web/oauth/oauth_controller.ex View File

@@ -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


+ 1
- 0
lib/pleroma/web/router.ex View File

@@ -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)


+ 5
- 0
lib/pleroma/workers/background_worker.ex View File

@@ -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",


+ 17
- 0
test/user_test.exs View File

@@ -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

+ 26
- 0
test/web/admin_api/admin_api_controller_test.exs View File

@@ -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


+ 27
- 0
test/web/oauth/oauth_controller_test.exs View File

@@ -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)




+ 21
- 0
test/web/twitter_api/password_controller_test.exs View File

@@ -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

Loading…
Cancel
Save