mirror of
https://github.com/plausible/analytics.git
synced 2025-01-05 17:16:44 +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,
|
config :ua_inspector,
|
||||||
init: {Plausible.Release, :configure_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,
|
config :kaffy,
|
||||||
otp_app: :plausible,
|
otp_app: :plausible,
|
||||||
ecto_repo: Plausible.Repo,
|
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
|
TOTP can be disabled with `disable/2`. User is expected to provide their
|
||||||
current password for safety. Once disabled, all TOTP user settings are
|
current password for safety. Once disabled, all TOTP user settings are
|
||||||
cleared and any remaining generated recovery codes are removed. The function
|
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,
|
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
|
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}
|
@spec disable(Auth.User.t(), String.t()) :: {:ok, Auth.User.t()} | {:error, :invalid_password}
|
||||||
def disable(user, password) do
|
def disable(user, password) do
|
||||||
if Auth.Password.match?(password, user.password_hash) do
|
if Auth.Password.match?(password, user.password_hash) do
|
||||||
{:ok, user} =
|
{:ok, user} = disable_for(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)
|
|
||||||
|
|
||||||
user
|
user
|
||||||
|> Email.two_factor_disabled_email()
|
|> Email.two_factor_disabled_email()
|
||||||
@ -196,6 +184,11 @@ defmodule Plausible.Auth.TOTP do
|
|||||||
end
|
end
|
||||||
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()
|
@spec reset_token(Auth.User.t()) :: Auth.User.t()
|
||||||
def reset_token(user) do
|
def reset_token(user) do
|
||||||
new_token =
|
new_token =
|
||||||
@ -286,6 +279,24 @@ defmodule Plausible.Auth.TOTP do
|
|||||||
end
|
end
|
||||||
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
|
defp totp_uri(user) do
|
||||||
NimbleTOTP.otpauth_uri("#{@issuer_name}:#{user.email}", user.totp_secret,
|
NimbleTOTP.otpauth_uri("#{@issuer_name}:#{user.email}", user.totp_secret,
|
||||||
issuer: @issuer_name
|
issuer: @issuer_name
|
||||||
|
@ -54,6 +54,10 @@ defmodule Plausible.Auth.UserAdmin do
|
|||||||
lock: %{
|
lock: %{
|
||||||
name: "Lock",
|
name: "Lock",
|
||||||
action: fn _, user -> lock(user) end
|
action: fn _, user -> lock(user) end
|
||||||
|
},
|
||||||
|
reset_2fa: %{
|
||||||
|
name: "Reset 2FA",
|
||||||
|
action: fn _, user -> disable_2fa(user) end
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@ -77,6 +81,10 @@ defmodule Plausible.Auth.UserAdmin do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def disable_2fa(user) do
|
||||||
|
Plausible.Auth.TOTP.force_disable(user)
|
||||||
|
end
|
||||||
|
|
||||||
defp grace_period_status(%{grace_period: grace_period}) do
|
defp grace_period_status(%{grace_period: grace_period}) do
|
||||||
case grace_period do
|
case grace_period do
|
||||||
nil ->
|
nil ->
|
||||||
|
@ -109,7 +109,7 @@ defmodule Plausible.Site do
|
|||||||
|> cast(attrs, [
|
|> cast(attrs, [
|
||||||
:timezone,
|
:timezone,
|
||||||
:public,
|
:public,
|
||||||
:stats_start_date,
|
:native_stats_start_at,
|
||||||
:ingest_rate_limit_threshold,
|
:ingest_rate_limit_threshold,
|
||||||
:ingest_rate_limit_scale_seconds
|
:ingest_rate_limit_scale_seconds
|
||||||
])
|
])
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
defmodule Plausible.SiteAdmin do
|
defmodule Plausible.SiteAdmin do
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
def ordering(_schema) do
|
def ordering(_schema) do
|
||||||
@ -21,12 +22,21 @@ defmodule Plausible.SiteAdmin do
|
|||||||
)
|
)
|
||||||
end
|
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
|
def form_fields(_) do
|
||||||
[
|
[
|
||||||
domain: %{update: :readonly},
|
domain: %{update: :readonly},
|
||||||
timezone: %{choices: Plausible.Timezones.options()},
|
timezone: %{choices: Plausible.Timezones.options()},
|
||||||
public: nil,
|
public: nil,
|
||||||
stats_start_date: nil,
|
stats_start_date: %{update: :readonly},
|
||||||
|
native_stats_start_at: nil,
|
||||||
ingest_rate_limit_scale_seconds: %{
|
ingest_rate_limit_scale_seconds: %{
|
||||||
help_text: "Time scale for which events rate-limiting is calculated. Default: 60"
|
help_text: "Time scale for which events rate-limiting is calculated. Default: 60"
|
||||||
},
|
},
|
||||||
|
@ -206,6 +206,40 @@ defmodule Plausible.Auth.TOTPTest do
|
|||||||
end
|
end
|
||||||
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
|
describe "reset_token/1" do
|
||||||
test "generates new token when TOTP enabled" do
|
test "generates new token when TOTP enabled" do
|
||||||
user = insert(:user, password: "VeryStrongVerySecret")
|
user = insert(:user, password: "VeryStrongVerySecret")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
defmodule PlausibleWeb.AdminControllerTest do
|
defmodule PlausibleWeb.AdminControllerTest do
|
||||||
use PlausibleWeb.ConnCase
|
use PlausibleWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
alias Plausible.Repo
|
||||||
|
|
||||||
describe "GET /crm/auth/user/:user_id/usage" do
|
describe "GET /crm/auth/user/:user_id/usage" do
|
||||||
setup [:create_user, :log_in]
|
setup [:create_user, :log_in]
|
||||||
@ -10,4 +12,40 @@ defmodule PlausibleWeb.AdminControllerTest do
|
|||||||
assert response(conn, 403) == "Not allowed"
|
assert response(conn, 403) == "Not allowed"
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user