Add new actions to CRM (#4030)

* Implement `Auth.TOTP.force_disable/1`

* Add "Reset 2FA" action to users CRM

* Add `Purge.reset!/2` variant allowing to set arbitrary cutoff time

* Add ability to set native stats start time from CRM

* Revert "Add `Purge.reset!/2` variant allowing to set arbitrary cutoff time"

This reverts commit 6f294d5d58.

* Add test for CRM site update action
This commit is contained in:
Adrian Gruntkowski 2024-04-23 10:29:49 +02:00 committed by GitHub
parent 87ae9d807f
commit a8ea4ce54b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 126 additions and 25 deletions

View File

@ -596,7 +596,7 @@ config :ref_inspector,
config :ua_inspector,
init: {Plausible.Release, :configure_ua_inspector}
if config_env() in [:dev, :staging, :prod] do
if config_env() in [:dev, :staging, :prod, :test] do
config :kaffy,
otp_app: :plausible,
ecto_repo: Plausible.Repo,

View File

@ -33,7 +33,10 @@ defmodule Plausible.Auth.TOTP do
TOTP can be disabled with `disable/2`. User is expected to provide their
current password for safety. Once disabled, all TOTP user settings are
cleared and any remaining generated recovery codes are removed. The function
can be safely run more than once.
can be safely run more than once. There's also alternative call for forced
disabling of TOTP for a given user without sending any notification,
`force_disable/1`. It's meant for use in situation where user lost both,
2FA device and recovery codes and their identity is verified independently.
If the user needs to regenerate the recovery codes outside of setup procedure,
they must do it via `generate_recovery_codes/2`, providing their current
@ -169,22 +172,7 @@ defmodule Plausible.Auth.TOTP do
@spec disable(Auth.User.t(), String.t()) :: {:ok, Auth.User.t()} | {:error, :invalid_password}
def disable(user, password) do
if Auth.Password.match?(password, user.password_hash) do
{:ok, user} =
Repo.transaction(fn ->
{_, _} =
user
|> recovery_codes_query()
|> Repo.delete_all()
user
|> change(
totp_enabled: false,
totp_token: nil,
totp_secret: nil,
totp_last_used_at: nil
)
|> Repo.update!()
end)
{:ok, user} = disable_for(user)
user
|> Email.two_factor_disabled_email()
@ -196,6 +184,11 @@ defmodule Plausible.Auth.TOTP do
end
end
@spec force_disable(Auth.User.t()) :: {:ok, Auth.User.t()}
def force_disable(user) do
disable_for(user)
end
@spec reset_token(Auth.User.t()) :: Auth.User.t()
def reset_token(user) do
new_token =
@ -286,6 +279,24 @@ defmodule Plausible.Auth.TOTP do
end
end
defp disable_for(user) do
Repo.transaction(fn ->
{_, _} =
user
|> recovery_codes_query()
|> Repo.delete_all()
user
|> change(
totp_enabled: false,
totp_token: nil,
totp_secret: nil,
totp_last_used_at: nil
)
|> Repo.update!()
end)
end
defp totp_uri(user) do
NimbleTOTP.otpauth_uri("#{@issuer_name}:#{user.email}", user.totp_secret,
issuer: @issuer_name

View File

@ -54,6 +54,10 @@ defmodule Plausible.Auth.UserAdmin do
lock: %{
name: "Lock",
action: fn _, user -> lock(user) end
},
reset_2fa: %{
name: "Reset 2FA",
action: fn _, user -> disable_2fa(user) end
}
]
end
@ -77,6 +81,10 @@ defmodule Plausible.Auth.UserAdmin do
end
end
def disable_2fa(user) do
Plausible.Auth.TOTP.force_disable(user)
end
defp grace_period_status(%{grace_period: grace_period}) do
case grace_period do
nil ->

View File

@ -109,7 +109,7 @@ defmodule Plausible.Site do
|> cast(attrs, [
:timezone,
:public,
:stats_start_date,
:native_stats_start_at,
:ingest_rate_limit_threshold,
:ingest_rate_limit_scale_seconds
])

View File

@ -1,5 +1,6 @@
defmodule Plausible.SiteAdmin do
use Plausible.Repo
import Ecto.Query
def ordering(_schema) do
@ -21,12 +22,21 @@ defmodule Plausible.SiteAdmin do
)
end
def before_update(_conn, changeset) do
if Ecto.Changeset.get_change(changeset, :native_stats_start_at) do
{:ok, Ecto.Changeset.put_change(changeset, :stats_start_date, nil)}
else
{:ok, changeset}
end
end
def form_fields(_) do
[
domain: %{update: :readonly},
timezone: %{choices: Plausible.Timezones.options()},
public: nil,
stats_start_date: nil,
stats_start_date: %{update: :readonly},
native_stats_start_at: nil,
ingest_rate_limit_scale_seconds: %{
help_text: "Time scale for which events rate-limiting is calculated. Default: 60"
},

View File

@ -206,6 +206,40 @@ defmodule Plausible.Auth.TOTPTest do
end
end
describe "force_disable/1" do
test "disables TOTP for user who has it enabled" do
user = insert(:user, password: "VeryStrongVerySecret")
{:ok, user, _} = TOTP.initiate(user)
code = NimbleTOTP.verification_code(user.totp_secret)
{:ok, user, _} = TOTP.enable(user, code)
assert_email_delivered_with(
to: [{user.name, user.email}],
subject: "Plausible Two-Factor Authentication enabled"
)
assert {:ok, updated_user} = TOTP.force_disable(user)
assert updated_user.id == user.id
refute updated_user.totp_enabled
assert is_nil(updated_user.totp_token)
assert is_nil(updated_user.totp_secret)
assert Repo.all(RecoveryCode) == []
end
test "succeeds for user who does not have TOTP enabled" do
user = insert(:user, password: "VeryStrongVerySecret")
assert {:ok, updated_user} = TOTP.force_disable(user)
assert updated_user.id == user.id
refute updated_user.totp_enabled
assert is_nil(updated_user.totp_token)
assert is_nil(updated_user.totp_secret)
end
end
describe "reset_token/1" do
test "generates new token when TOTP enabled" do
user = insert(:user, password: "VeryStrongVerySecret")

View File

@ -1,5 +1,7 @@
defmodule PlausibleWeb.AdminControllerTest do
use PlausibleWeb.ConnCase
use PlausibleWeb.ConnCase, async: false
alias Plausible.Repo
describe "GET /crm/auth/user/:user_id/usage" do
setup [:create_user, :log_in]
@ -10,4 +12,40 @@ defmodule PlausibleWeb.AdminControllerTest do
assert response(conn, 403) == "Not allowed"
end
end
describe "POST /crm/sites/site/:site_id" do
setup [:create_user, :log_in]
@tag :full_build_only
test "resets stats start date on native stats start time change", %{conn: conn, user: user} do
patch_env(:super_admin_user_ids, [user.id])
site =
insert(:site,
public: false,
stats_start_date: ~D[2022-03-14],
native_stats_start_at: ~N[2024-01-22 14:28:00]
)
params = %{
"site" => %{
"domain" => site.domain,
"timezone" => site.timezone,
"public" => "false",
"native_stats_start_at" => "2024-02-12 12:00:00",
"ingest_rate_limit_scale_seconds" => site.ingest_rate_limit_scale_seconds,
"ingest_rate_limit_threshold" => site.ingest_rate_limit_threshold
}
}
conn = put(conn, "/crm/sites/site/#{site.id}", params)
assert redirected_to(conn, 302) == "/crm/sites/site"
site = Repo.reload!(site)
refute site.public
assert site.native_stats_start_at == ~N[2024-02-12 12:00:00]
assert site.stats_start_date == nil
end
end
end