mirror of
https://github.com/plausible/analytics.git
synced 2025-01-03 15:17:58 +03:00
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:
parent
87ae9d807f
commit
a8ea4ce54b
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 ->
|
||||
|
@ -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
|
||||
])
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user