Revise system behaviour for SSO users (#5506)

* Setup MFA properly in SSO tests

* Move `new_identity` test helper to common helpers

* Make standard login only allow Owner SSO users

* Implement `Plausible.Users.type/1` for determining user type

* Implement plug restricting action based on user type

* Restrict or adjust access to settings actions to SSO users

* Make a very small refactor to `Auth.SSO` helper

* Prevent SSO users from acceptig team invitations

* Prevent SSO users from adding websites under "My Presonal Sites"

* Prevent implicit team creation by SSO users

* Add workaround for compiler warning under CE

* Remove SSO user on removing membership

* Prevent changing role to owner when 2FA not enabled

* Prevent provisioning from standard user with active personal team

* Fix `Auth.lookup/1` to not break for standard users on multiple teams

* Use `Plausible.always/1` (h/t @aerosol)

* Revert "Use `Plausible.always/1` (h/t @aerosol)"

This reverts commit 0ee7dd84d3.

* Rename `RestrictType` -> `RestrictUserType`

* Make the configuration intent more explicit in `RestrictUserType` plug

* Rename plug file
This commit is contained in:
Adrian Gruntkowski 2025-06-23 10:19:12 +02:00 committed by GitHub
parent 6ade93bf86
commit a2ed1e04b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 901 additions and 102 deletions

View File

@ -8,11 +8,14 @@ defmodule Plausible.Auth.SSO do
alias Plausible.Auth
alias Plausible.Auth.SSO
alias Plausible.Billing.Subscription
alias Plausible.Repo
alias Plausible.Teams
use Plausible.Auth.SSO.Domain.Status
require Plausible.Billing.Subscription.Status
@type policy_attr() ::
{:sso_default_role, Teams.Policy.sso_member_role()}
| {:sso_session_timeout_minutes, non_neg_integer()}
@ -74,7 +77,7 @@ defmodule Plausible.Auth.SSO do
@spec provision_user(SSO.Identity.t()) ::
{:ok, :standard | :sso | :integration, Teams.Team.t(), Auth.User.t()}
| {:error, :integration_not_found | :over_limit}
| {:error, :multiple_memberships, Teams.Team.t(), Auth.User.t()}
| {:error, :multiple_memberships | :active_personal_team, Teams.Team.t(), Auth.User.t()}
def provision_user(identity) do
case find_user(identity) do
{:ok, :standard, user, integration, domain} ->
@ -394,6 +397,7 @@ defmodule Plausible.Auth.SSO do
with :ok <- ensure_team_member(integration.team, user),
:ok <- ensure_one_membership(user, integration.team),
:ok <- ensure_empty_personal_team(user, integration.team),
:ok <- Auth.UserSessions.revoke_all(user),
{:ok, user} <- Repo.update(changeset) do
{:ok, :standard, integration.team, user}
@ -467,12 +471,31 @@ defmodule Plausible.Auth.SSO do
end
defp ensure_one_membership(user, team) do
query = Teams.Users.teams_query(user)
if Repo.aggregate(query, :count) > 1 do
if Teams.Users.team_member?(user, except: [team.id], only_setup?: true) do
{:error, :multiple_memberships, team, user}
else
:ok
end
end
defp ensure_empty_personal_team(user, team) do
case Teams.get_by_owner(user, only_not_setup?: true) do
{:ok, personal_team} ->
subscription = Teams.Billing.get_subscription(personal_team)
no_subscription? =
is_nil(subscription) or subscription.status == Subscription.Status.deleted()
zero_sites? = Teams.owned_sites_count(personal_team) == 0
if no_subscription? and zero_sites? do
:ok
else
{:error, :active_personal_team, team, user}
end
{:error, :no_team} ->
:ok
end
end
end

View File

@ -57,9 +57,23 @@ defmodule Plausible.Auth do
Repo.get_by(Auth.User, opts)
end
@spec get_user_by(Keyword.t()) :: {:ok, Auth.User.t()} | {:error, :user_not_found}
def get_user_by(opts) do
case Repo.get_by(Auth.User, opts) do
@spec lookup(String.t()) :: {:ok, Auth.User.t()} | {:error, :user_not_found}
def lookup(email) do
query =
on_ee do
from(
u in Auth.User,
left_join: tm in assoc(u, :team_memberships),
on: u.type == :sso and tm.role == :owner,
left_join: t in assoc(tm, :team),
where: u.email == ^email,
where: u.type == :standard or (u.type == :sso and t.setup_complete == true)
)
else
from(u in Auth.User, where: u.email == ^email)
end
case Repo.one(query) do
%Auth.User{} = user -> {:ok, user}
nil -> {:error, :user_not_found}
end

View File

@ -141,13 +141,12 @@ defmodule Plausible.Teams do
If the user already has an owner membership in an existing team,
that team is returned.
If the user has a non-guest membership other than owner, `:no_team` error
is returned.
"""
@spec get_or_create(Auth.User.t()) :: {:ok, Teams.Team.t()} | {:error, :multiple_teams}
@spec get_or_create(Auth.User.t()) ::
{:ok, Teams.Team.t()} | {:error, :multiple_teams | :permission_denied}
def get_or_create(user) do
with {:error, :no_team} <- get_owned_team(user, only_not_setup?: true) do
with :ok <- check_user_type(user),
{:error, :no_team} <- get_owned_team(user, only_not_setup?: true) do
case create_my_team(user) do
{:ok, team} ->
{:ok, team}
@ -160,6 +159,14 @@ defmodule Plausible.Teams do
@spec force_create_my_team(Auth.User.t()) :: Teams.Team.t()
def force_create_my_team(user) do
# This is going to crash hard for SSO user. This shouldn't happen
# under normal circumstances except in case of a _very_ unlucky timing.
# Manual resolution is necessary anyway.
case check_user_type(user) do
:ok -> :pass
_ -> raise "SSO user tried to force create a personal team"
end
{:ok, team} =
Repo.transaction(fn ->
clear_autocreated(user)
@ -212,10 +219,10 @@ defmodule Plausible.Teams do
end
end
@spec get_by_owner(Auth.User.t()) ::
@spec get_by_owner(Auth.User.t(), Keyword.t()) ::
{:ok, Teams.Team.t()} | {:error, :no_team | :multiple_teams}
def get_by_owner(user) do
get_owned_team(user)
def get_by_owner(user, opts \\ []) do
get_owned_team(user, opts)
end
@spec update_accept_traffic_until(Teams.Team.t()) :: Teams.Team.t()
@ -344,6 +351,14 @@ defmodule Plausible.Teams do
end
end
defp check_user_type(user) do
if Plausible.Users.type(user) == :sso do
{:error, :permission_denied}
else
:ok
end
end
defp clear_autocreated(user) do
Repo.update_all(
from(tm in Teams.Membership,

View File

@ -252,7 +252,11 @@ defmodule Plausible.Teams.Invitations do
team_invitation = Repo.preload(team_invitation, [:team, :inviter])
now = NaiveDateTime.utc_now(:second)
do_accept(team_invitation, user, now, guest_invitations: [])
if Plausible.Users.type(user) == :sso do
{:error, :permission_denied}
else
do_accept(team_invitation, user, now, guest_invitations: [])
end
end
@doc false

View File

@ -46,13 +46,20 @@ defmodule Plausible.Teams.Memberships do
end
end
@spec can_add_site?(Teams.Team.t(), Auth.User.t()) :: boolean()
def can_add_site?(team, user) do
case team_role(team, user) do
{:ok, role} when role in [:owner, :admin, :editor] ->
true
user_type = Plausible.Users.type(user)
_ ->
false
role =
case team_role(team, user) do
{:ok, role} -> role
{:error, _} -> :not_a_member
end
case {user_type, role, team} do
{:sso, :owner, %{setup_complete: false}} -> false
{_, role, _} when role in [:owner, :admin, :editor] -> true
_ -> false
end
end

View File

@ -17,7 +17,7 @@ defmodule Plausible.Teams.Memberships.Remove do
{:ok, _} =
Repo.transaction(fn ->
Repo.delete!(team_membership)
delete_membership!(team_membership)
Plausible.Segments.after_user_removed_from_team(
team_membership.team,
@ -33,6 +33,18 @@ defmodule Plausible.Teams.Memberships.Remove do
end
end
defp delete_membership!(team_membership) do
user = team_membership.user
Repo.delete!(team_membership)
if Plausible.Users.type(user) == :sso do
{:ok, :deleted} = Plausible.Auth.delete_user(user)
end
:ok
end
defp check_can_remove_membership(:owner, _), do: :ok
defp check_can_remove_membership(:admin, role) when role != :owner, do: :ok
defp check_can_remove_membership(_, _), do: {:error, :permission_denied}

View File

@ -3,6 +3,8 @@ defmodule Plausible.Teams.Memberships.UpdateRole do
Service for updating role of a team member.
"""
use Plausible
alias Plausible.Repo
alias Plausible.Teams
alias Plausible.Teams.Memberships
@ -23,9 +25,9 @@ defmodule Plausible.Teams.Memberships.UpdateRole do
new_role,
granting_to_self?
),
:ok <- check_owner_can_get_demoted(team, team_membership.role, new_role) do
team_membership = Repo.preload(team_membership, :user)
:ok <- check_owner_can_get_demoted(team, team_membership.role, new_role),
team_membership = Repo.preload(team_membership, :user),
:ok <- check_can_promote_to_owner(team, team_membership.user, new_role) do
if team_membership.role == :guest and new_role != :guest do
team_membership.user.email
|> PlausibleWeb.Email.guest_to_team_member_promotion(
@ -46,6 +48,29 @@ defmodule Plausible.Teams.Memberships.UpdateRole do
end
end
on_ee do
defp check_can_promote_to_owner(team, user, :owner) do
if team.policy.force_sso == :all_but_owners and not Plausible.Auth.TOTP.enabled?(user) do
{:error, :mfa_disabled}
else
:ok
end
end
defp check_can_promote_to_owner(_team, _user, _new_role), do: :ok
else
defp check_can_promote_to_owner(_team, _user, _new_role) do
# The `else` branch is not reachable.
# This a workaround for Elixir 1.18+ compiler
# being too smart.
if :erlang.phash2(1, 1) == 0 do
:ok
else
{:error, :mfa_disabled}
end
end
end
defp check_valid_role(role) do
if role in (Teams.Membership.roles() -- [:guest]) do
:ok

View File

@ -63,15 +63,26 @@ defmodule Plausible.Teams.Users do
def team_member?(user, opts \\ []) do
excluded_team_ids = Keyword.get(opts, :except, [])
only_setup? = Keyword.get(opts, :only_setup?, false)
Repo.exists?(
query =
from(
tm in Teams.Membership,
where: tm.user_id == ^user.id,
where: tm.role != :guest,
where: tm.team_id not in ^excluded_team_ids
)
)
query =
if only_setup? do
query
|> join(:inner, [tm], t in assoc(tm, :team), as: :team)
|> where([team: t], t.setup_complete == true)
else
query
end
Repo.exists?(query)
end
def has_sites?(user, opts \\ []) do

View File

@ -2,11 +2,32 @@ defmodule Plausible.Users do
@moduledoc """
User context
"""
use Plausible
import Ecto.Query
alias Plausible.Auth
alias Plausible.Repo
on_ee do
@spec type(Auth.User.t()) :: :standard | :sso
def type(user) do
user.type
end
else
@spec type(Auth.User.t()) :: :standard
def type(_user) do
# The `else` branch is not reachable.
# This a workaround for Elixir 1.18+ compiler
# being too smart.
if :erlang.phash2(1, 1) == 0 do
:standard
else
:sso
end
end
end
@spec bump_last_seen(Auth.User.t() | pos_integer(), NaiveDateTime.t()) :: :ok
def bump_last_seen(%Auth.User{id: user_id}, now) do
bump_last_seen(user_id, now)

View File

@ -30,7 +30,7 @@ defmodule PlausibleWeb.AuthController do
:activate_form,
:activate,
:request_activation_code,
:initiate_2fa,
:initiate_2fa_setup,
:verify_2fa_setup_form,
:verify_2fa_setup,
:disable_2fa,
@ -40,6 +40,9 @@ defmodule PlausibleWeb.AuthController do
]
)
plug Plausible.Plugs.RestrictUserType,
[deny: :sso] when action in [:delete_me, :disable_2fa]
plug(
:clear_2fa_user
when action not in [
@ -169,21 +172,21 @@ defmodule PlausibleWeb.AuthController do
def password_reset_request(conn, %{"email" => email} = params) do
if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do
user = Repo.get_by(Plausible.Auth.User, email: email)
case Auth.lookup(email) do
{:ok, _user} ->
token = Auth.Token.sign_password_reset(email)
url = PlausibleWeb.Endpoint.url() <> "/password/reset?token=#{token}"
email_template = PlausibleWeb.Email.password_reset_email(email, url)
Plausible.Mailer.deliver_later(email_template)
if user do
token = Auth.Token.sign_password_reset(email)
url = PlausibleWeb.Endpoint.url() <> "/password/reset?token=#{token}"
email_template = PlausibleWeb.Email.password_reset_email(email, url)
Plausible.Mailer.deliver_later(email_template)
Logger.debug(
"Password reset e-mail sent. In dev environment GET /sent-emails for details."
)
Logger.debug(
"Password reset e-mail sent. In dev environment GET /sent-emails for details."
)
render(conn, "password_reset_request_success.html", email: email)
render(conn, "password_reset_request_success.html", email: email)
else
render(conn, "password_reset_request_success.html", email: email)
{:error, _} ->
render(conn, "password_reset_request_success.html", email: email)
end
else
render(conn, "password_reset_request_form.html",
@ -234,7 +237,7 @@ defmodule PlausibleWeb.AuthController do
def login(conn, %{"email" => email, "password" => password} = params) do
with :ok <- Auth.rate_limit(:login_ip, conn),
{:ok, user} <- Auth.get_user_by(email: email),
{:ok, user} <- Auth.lookup(email),
:ok <- Auth.rate_limit(:login_user, user),
:ok <- Auth.check_password(user, password),
:ok <- check_2fa_verified(conn, user) do

View File

@ -17,6 +17,9 @@ defmodule PlausibleWeb.SettingsController do
plug Plausible.Plugs.AuthorizeTeamAccess,
[:owner] when action in [:team_danger_zone, :delete_team]
plug Plausible.Plugs.RestrictUserType,
[deny: :sso] when action in [:update_name, :update_email, :update_password]
def index(conn, _params) do
redirect(conn, to: Routes.settings_path(conn, :preferences))
end

View File

@ -314,6 +314,13 @@ defmodule PlausibleWeb.Live.TeamManagement do
"The team has to have at least one owner"
)
{{:error, :mfa_disabled}, _} ->
socket
|> put_live_flash(
:error,
"User must have 2FA enabled to become an owner"
)
{{:error, {:over_limit, limit}}, _} ->
socket
|> put_live_flash(

View File

@ -0,0 +1,25 @@
defmodule Plausible.Plugs.RestrictUserType do
@moduledoc """
Plug for restricting user access by type.
"""
import Plug.Conn
alias PlausibleWeb.Router.Helpers, as: Routes
def init(opts) do
Keyword.fetch!(opts, :deny)
end
def call(conn, deny_type) do
user = conn.assigns[:current_user]
if user && Plausible.Users.type(user) == deny_type do
conn
|> Phoenix.Controller.redirect(to: Routes.site_path(conn, :index))
|> halt()
else
conn
end
end
end

View File

@ -1,5 +1,5 @@
<.settings_tiles>
<.tile>
<.tile :if={Plausible.Users.type(@current_user) == :standard}>
<:title>
<a id="update-name">Your Name</a>
</:title>
@ -20,6 +20,18 @@
</.form>
</.tile>
<.tile :if={Plausible.Users.type(@current_user) == :sso}>
<:title>
<a id="view-name">Your Name</a>
</:title>
<:subtitle>
The name associated with your account
</:subtitle>
<.form :let={f} for={@name_changeset}>
<.input type="text" field={f[:name]} disabled={true} label="Name" width="w-1/2" />
</.form>
</.tile>
<.tile docs="dashboard-appearance">
<:title>
<a id="update-theme">Dashboard Appearance</a>

View File

@ -1,5 +1,5 @@
<.settings_tiles>
<.tile docs="change-email">
<.tile :if={Plausible.Users.type(@current_user) == :standard} docs="change-email">
<:title>
<a id="update-email">Email Address</a>
</:title>
@ -31,7 +31,26 @@
</.form>
</.tile>
<.tile docs="reset-password">
<.tile :if={Plausible.Users.type(@current_user) == :sso}>
<:title>
<a id="view-email">Email Address</a>
</:title>
<:subtitle>
Address associated with your account
</:subtitle>
<.form :let={f} for={@email_changeset}>
<.input
type="text"
name="user[current_email]"
value={f.data.email}
label="Current Email"
width="w-1/2"
disabled
/>
</.form>
</.tile>
<.tile :if={Plausible.Users.type(@current_user) == :standard} docs="reset-password">
<:title>
<a id="update-password">Password</a>
</:title>
@ -97,6 +116,7 @@
<div x-data="{disable2FAOpen: false, regenerate2FAOpen: false}">
<div :if={@totp_enabled?}>
<.button
disabled={Plausible.Users.type(@current_user) == :sso}
x-on:click="disable2FAOpen = true; $refs.disable2FAPassword.value = ''"
theme="danger"
mt?={false}

View File

@ -64,7 +64,8 @@ defmodule PlausibleWeb.UserAuth do
to: Routes.sso_path(conn, :login_form, error: error, return_to: redirect_path)
)
{:error, :multiple_memberships, team, user} ->
{:error, reason, team, user}
when reason in [:multiple_memberships, :active_personal_team] ->
redirect_path = Routes.site_path(conn, :index, __team: team.identifier)
log_in_user(conn, user, redirect_path)

View File

@ -113,7 +113,9 @@ defmodule PlausibleWeb.LayoutView do
if(not Teams.setup?(current_team),
do: %{key: "API Keys", value: "api-keys", icon: :key}
),
%{key: "Danger Zone", value: "danger-zone", icon: :exclamation_triangle}
if(Plausible.Users.type(conn.assigns.current_user) == :standard,
do: %{key: "Danger Zone", value: "danger-zone", icon: :exclamation_triangle}
)
]
|> Enum.reject(&is_nil/1)
}

View File

@ -9,11 +9,15 @@ defmodule Plausible.Auth.SSO.DomainsTest do
use Plausible.Auth.SSO.Domain.Status
use Oban.Testing, repo: Plausible.Repo
alias Plausible.Auth
alias Plausible.Auth.SSO
alias Plausible.Teams
setup do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
integration = SSO.initiate_saml_integration(team)
@ -338,14 +342,5 @@ defmodule Plausible.Auth.SSO.DomainsTest do
defp generate_domain() do
"example-#{Enum.random(1..10_000)}.com"
end
defp new_identity(name, email, id \\ Ecto.UUID.generate()) do
%SSO.Identity{
id: id,
name: name,
email: email,
expires_at: NaiveDateTime.add(NaiveDateTime.utc_now(:second), 6, :hour)
}
end
end
end

View File

@ -256,6 +256,36 @@ defmodule Plausible.Auth.SSOTest do
assert sso_user.last_sso_login
end
test "provisions SSO user from existing user with personal team", %{
integration: integration,
team: team,
domain: domain,
sso_domain: sso_domain
} do
user = new_user(email: "jane@" <> domain, name: "Jane Sculley")
{:ok, _} = Plausible.Teams.get_or_create(user)
add_member(team, user: user, role: :editor)
# guest membership on a site on another team should not affect provisioning
another_team_site = new_site()
add_guest(another_team_site, user: user, role: :editor)
identity = new_identity(user.name, user.email)
assert {:ok, :standard, matched_team, sso_user} = SSO.provision_user(identity)
assert matched_team.id == team.id
assert sso_user.id == user.id
assert sso_user.email == identity.email
assert sso_user.type == :sso
assert sso_user.name == identity.name
assert sso_user.sso_identity_id == identity.id
assert sso_user.sso_integration_id == integration.id
assert sso_user.sso_domain_id == sso_domain.id
assert sso_user.email_verified
assert sso_user.last_sso_login
end
test "provisions existing SSO user", %{
integration: integration,
team: team,
@ -318,7 +348,7 @@ defmodule Plausible.Auth.SSOTest do
} do
user = new_user(email: "jane@" <> domain, name: "Jane Sculley")
add_member(team, user: user, role: :editor)
another_team = new_site().team
another_team = new_site().team |> Plausible.Teams.complete_setup()
add_member(another_team, user: user, role: :viewer)
identity = new_identity(user.name, user.email)
@ -329,6 +359,43 @@ defmodule Plausible.Auth.SSOTest do
assert matched_user.id == user.id
end
test "does not provision from existing user with personal team with subscription", %{
team: team,
domain: domain
} do
user =
new_user(email: "jane@" <> domain, name: "Jane Sculley") |> subscribe_to_growth_plan()
add_member(team, user: user, role: :editor)
identity = new_identity(user.name, user.email)
assert {:error, :active_personal_team, matched_team, matched_user} =
SSO.provision_user(identity)
assert matched_team.id == team.id
assert matched_user.id == user.id
end
test "does not provision from existing user with personal team with site", %{
team: team,
domain: domain
} do
user = new_user(email: "jane@" <> domain, name: "Jane Sculley")
new_site(owner: user)
add_member(team, user: user, role: :editor)
identity = new_identity(user.name, user.email)
assert {:error, :active_personal_team, matched_team, matched_user} =
SSO.provision_user(identity)
assert matched_team.id == team.id
assert matched_user.id == user.id
end
test "does not provision new SSO user from identity when team is over members limit", %{
domain: domain,
team: team
@ -446,7 +513,9 @@ defmodule Plausible.Auth.SSOTest do
describe "check_force_sso/2" do
test "returns ok when conditions are met for setting all_but_owners" do
# Owner with MFA enabled
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -465,7 +534,9 @@ defmodule Plausible.Auth.SSOTest do
end
test "returns error when one owner does not have MFA configured" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Owner without MFA
@ -489,7 +560,9 @@ defmodule Plausible.Auth.SSOTest do
test "returns error when there's no provisioned SSO user present" do
# Owner with MFA enabled
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -504,7 +577,9 @@ defmodule Plausible.Auth.SSOTest do
test "returns error when there's no verified SSO domain present" do
# Owner with MFA enabled
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -519,7 +594,9 @@ defmodule Plausible.Auth.SSOTest do
test "returns error when there's no SSO domain present" do
# Owner with MFA enabled
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -530,7 +607,9 @@ defmodule Plausible.Auth.SSOTest do
test "returns error when there's no SSO integration present" do
# Owner with MFA enabled
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
assert {:error, :no_integration} = SSO.check_force_sso(team, :all_but_owners)
@ -545,7 +624,9 @@ defmodule Plausible.Auth.SSOTest do
describe "set_enforce_sso/2" do
test "sets enforce mode to all_but_owners when conditions met" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -573,7 +654,9 @@ defmodule Plausible.Auth.SSOTest do
end
test "sets enforce mode to none" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -599,7 +682,9 @@ defmodule Plausible.Auth.SSOTest do
describe "check_can_remove_integration/1" do
test "returns ok if conditions to remove integration met" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -622,7 +707,9 @@ defmodule Plausible.Auth.SSOTest do
end
test "returns error if force SSO enabled" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -648,7 +735,9 @@ defmodule Plausible.Auth.SSOTest do
end
test "returns error if SSO user present" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -670,7 +759,9 @@ defmodule Plausible.Auth.SSOTest do
describe "remove_integration/1,2" do
test "removes integration when conditions met" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -696,7 +787,9 @@ defmodule Plausible.Auth.SSOTest do
end
test "returns error when conditions not met" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -719,7 +812,9 @@ defmodule Plausible.Auth.SSOTest do
end
test "succeeds when SSO user present and force flag set" do
owner = new_user(totp_enabled: true, totp_secret: "secret")
owner = new_user()
{:ok, owner, _} = Auth.TOTP.initiate(owner)
{:ok, owner, _} = Auth.TOTP.enable(owner, :skip_verify)
team = new_site(owner: owner).team
# Setup integration
@ -792,14 +887,5 @@ defmodule Plausible.Auth.SSOTest do
refute_enqueued(worker: SSO.Domain.Verification.Worker, args: %{domain: domain})
end
end
defp new_identity(name, email, id \\ Ecto.UUID.generate()) do
%SSO.Identity{
id: id,
name: name,
email: email,
expires_at: NaiveDateTime.add(NaiveDateTime.utc_now(:second), 6, :hour)
}
end
end
end

View File

@ -1,5 +1,6 @@
defmodule Plausible.SitesTest do
use Plausible.DataCase
use Plausible
use Plausible.Teams.Test
alias Plausible.Sites
@ -117,6 +118,30 @@ defmodule Plausible.SitesTest do
end
end
on_ee do
describe "create a site - SSO user" do
setup [:create_user, :create_team, :create_site, :setup_sso, :provision_sso_user]
test "creates a site for SSO user in a setup team", %{user: user, team: team} do
params = %{"domain" => "example.com", "timezone" => "Europe/London"}
assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"}}} =
Sites.create(user, params, team)
end
test "does not allow creating a site in SSO user's personal team", %{team: team} do
user = add_member(team, role: :editor)
{:ok, personal_team} = Plausible.Teams.get_or_create(user)
identity = new_identity(user.name, user.email)
{:ok, _, _, user} = Plausible.Auth.SSO.provision_user(identity)
params = %{"domain" => "example.com", "timezone" => "Europe/London"}
assert {:error, _, :permission_denied, _} = Sites.create(user, params, personal_team)
end
end
end
describe "stats_start_date" do
test "is nil if site has no stats" do
site = insert(:site)

View File

@ -108,6 +108,21 @@ defmodule Plausible.Teams.Invitations.AcceptTest do
end
end
on_ee do
describe "accept_invitation/3 - team invitations - SSO user" do
setup [:create_user, :create_team, :setup_sso, :provision_sso_user]
test "does not allow accepting invite by SSO user", %{user: invitee} do
inviter = new_user()
team = new_site(owner: inviter).team
invitation = invite_member(team, invitee, inviter: inviter, role: :editor)
assert {:error, :permission_denied} = Accept.accept(invitation.invitation_id, invitee)
end
end
end
describe "accept_invitation/3 - guest invitations" do
test "converts an invitation into a membership" do
inviter = new_user()

View File

@ -24,6 +24,8 @@ defmodule Plausible.Teams.Memberships.RemoveTest do
to: [nil: collaborator.email],
subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked"
)
assert Repo.reload(user)
end
test "when member is removed, associated personal segment is deleted" do
@ -123,4 +125,25 @@ defmodule Plausible.Teams.Memberships.RemoveTest do
assert_team_membership(another_member, team, :viewer)
end
end
on_ee do
describe "SSO user" do
setup [:create_user, :create_team, :setup_sso, :provision_sso_user]
test "removes SSO user along with membership", %{team: team, user: user} do
owner = add_member(team, role: :owner)
assert {:ok, _} = Remove.remove(team, user.id, owner)
refute_team_member(user, team)
assert_email_delivered_with(
to: [nil: user.email],
subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked"
)
refute Repo.reload(user)
end
end
end
end

View File

@ -116,4 +116,75 @@ defmodule Plausible.Teams.Memberships.UpdateRoleTest do
subject: @subject_prefix <> "Welcome to \"#{team.name}\" team"
)
end
on_ee do
describe "SSO user" do
setup [:create_user, :create_team, :setup_sso, :provision_sso_user]
test "updates an SSO member's role by user id", %{team: team, user: user} do
collaborator = add_member(team, role: :viewer)
{:ok, _, _, collaborator} =
new_identity(collaborator.name, collaborator.email)
|> Plausible.Auth.SSO.provision_user()
assert {:ok, _} = UpdateRole.update(team, collaborator.id, "editor", user)
assert_team_membership(collaborator, team, :editor)
end
test "updates an SSO member's role to owner when no Force SSO set", %{
team: team,
user: user
} do
collaborator = add_member(team, role: :viewer)
{:ok, _, _, collaborator} =
new_identity(collaborator.name, collaborator.email)
|> Plausible.Auth.SSO.provision_user()
assert {:ok, _} = UpdateRole.update(team, collaborator.id, "owner", user)
assert_team_membership(collaborator, team, :owner)
end
test "updates an SSO member's role with Force SSO to Owner provided they have 2FA enabled",
%{
team: team,
user: user
} do
{:ok, user, _} = Plausible.Auth.TOTP.initiate(user)
{:ok, user, _} = Plausible.Auth.TOTP.enable(user, :skip_verify)
{:ok, team} = Plausible.Auth.SSO.set_force_sso(team, :all_but_owners)
collaborator = add_member(team, role: :viewer)
{:ok, _, _, collaborator} =
new_identity(collaborator.name, collaborator.email)
|> Plausible.Auth.SSO.provision_user()
{:ok, collaborator, _} = Plausible.Auth.TOTP.initiate(collaborator)
{:ok, collaborator, _} = Plausible.Auth.TOTP.enable(collaborator, :skip_verify)
assert {:ok, _} = UpdateRole.update(team, collaborator.id, "owner", user)
assert_team_membership(collaborator, team, :owner)
end
test "does not update SSO member's role to Owner if they don't have 2FA enabled", %{
team: team,
user: user
} do
{:ok, user, _} = Plausible.Auth.TOTP.initiate(user)
{:ok, user, _} = Plausible.Auth.TOTP.enable(user, :skip_verify)
{:ok, team} = Plausible.Auth.SSO.set_force_sso(team, :all_but_owners)
collaborator = add_member(team, role: :viewer)
{:ok, _, _, collaborator} =
new_identity(collaborator.name, collaborator.email)
|> Plausible.Auth.SSO.provision_user()
assert {:error, :mfa_disabled} = UpdateRole.update(team, collaborator.id, "owner", user)
end
end
end
end

View File

@ -172,6 +172,26 @@ defmodule Plausible.TeamsTest do
end
end
on_ee do
describe "get_or_create/1 - SSO user" do
setup [:create_user, :create_team, :setup_sso, :provision_sso_user]
test "does not allow creating personal team to SSO user", %{user: user} do
assert {:error, :permission_denied} = Teams.get_or_create(user)
end
end
describe "force_create_my_team/1 - SSO user" do
setup [:create_user, :create_team, :setup_sso, :provision_sso_user]
test "crashes when trying to create a team for SSO user", %{user: user} do
assert_raise RuntimeError, ~r/SSO user tried to force create a personal team/, fn ->
Teams.force_create_my_team(user)
end
end
end
end
describe "get_by_owner/1" do
test "returns error if user does not own any team" do
user = new_user()

View File

@ -501,6 +501,23 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn) == "/sites"
end
test "valid email and password, user on multiple teams - logs the user in", %{conn: conn} do
user = insert(:user, password: "password")
# first team
new_site(owner: user)
# another team
another_team = new_site().team |> Plausible.Teams.complete_setup()
add_member(another_team, user: user, role: :owner)
conn = post(conn, "/login", email: user.email, password: "password")
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn, :user_token) == token
assert redirected_to(conn) == "/sites"
end
test "valid email and password with return_to set - redirects properly", %{conn: conn} do
user = insert(:user, password: "password")
@ -569,6 +586,76 @@ defmodule PlausibleWeb.AuthControllerTest do
refute get_session(conn, :user_token)
end
on_ee do
test "SSO owner user - logs in", %{conn: conn} do
owner = new_user(name: "Jane Shelley", email: "jane@example.com", password: "password")
team = new_site(owner: owner).team
team = Plausible.Teams.complete_setup(team)
# Setup SSO
integration = Auth.SSO.initiate_saml_integration(team)
{:ok, sso_domain} = Auth.SSO.Domains.add(integration, "example.com")
_sso_domain = Auth.SSO.Domains.verify(sso_domain, skip_checks?: true)
identity = new_identity(owner.name, owner.email)
{:ok, _, _, _sso_user} = Auth.SSO.provision_user(identity)
conn = post(conn, "/login", email: owner.email, password: "password")
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert conn.resp_cookies["session_2fa"].max_age == 0
assert %{sessions: [%{token: token}]} = owner |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn, :user_token) == token
end
test "SSO user other than owner - renders login form again", %{conn: conn} do
owner = new_user()
team = new_site(owner: owner).team
member = new_user(name: "Jane Shelley", email: "jane@example.com", password: "password")
add_member(team, user: member, role: :viewer)
# Setup SSO
integration = Auth.SSO.initiate_saml_integration(team)
{:ok, sso_domain} = Auth.SSO.Domains.add(integration, "example.com")
_sso_domain = Auth.SSO.Domains.verify(sso_domain, skip_checks?: true)
identity = new_identity(member.name, member.email)
{:ok, _, _, _sso_user} = Auth.SSO.provision_user(identity)
conn = post(conn, "/login", email: member.email, password: "password")
assert get_session(conn, :user_token) == nil
assert html_response(conn, 200) =~ "Enter your account credentials"
end
test "SSO user other than owner with personal team - renders login form again", %{
conn: conn
} do
owner = new_user()
team = new_site(owner: owner).team
member = new_user(name: "Jane Shelley", email: "jane@example.com", password: "password")
{:ok, _} = Plausible.Teams.get_or_create(member)
add_member(team, user: member, role: :viewer)
# Setup SSO
integration = Auth.SSO.initiate_saml_integration(team)
{:ok, sso_domain} = Auth.SSO.Domains.add(integration, "example.com")
_sso_domain = Auth.SSO.Domains.verify(sso_domain, skip_checks?: true)
identity = new_identity(member.name, member.email)
{:ok, _, _, _sso_user} = Auth.SSO.provision_user(identity)
conn = post(conn, "/login", email: member.email, password: "password")
assert get_session(conn, :user_token) == nil
assert html_response(conn, 200) =~ "Enter your account credentials"
end
end
test "email does not exist - renders login form again", %{conn: conn} do
conn = post(conn, "/login", email: "user@example.com", password: "password")
@ -639,6 +726,43 @@ defmodule PlausibleWeb.AuthControllerTest do
end
end
on_ee do
describe "POST /password/request-reset - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user]
test "initiates reset for owner SSO user email", %{conn: conn, user: user} do
mock_captcha_success()
conn = post(conn, "/password/request-reset", %{email: user.email})
assert html_response(conn, 200)
assert_email_delivered_with(
subject: "Plausible password reset",
to: [nil: user.email]
)
end
test "does not initiate reset for non-owner SSO user", %{conn: conn, user: user, team: team} do
add_member(team, role: :owner)
assert {:ok, _} =
Plausible.Teams.Memberships.UpdateRole.update(team, user.id, "editor", user)
assert Plausible.Teams.Memberships.team_role(team, user) == {:ok, :editor}
mock_captcha_success()
conn = post(conn, "/password/request-reset", %{email: user.email})
assert html_response(conn, 200)
refute_email_delivered_with(
subject: "Plausible password reset",
to: [nil: user.email]
)
end
end
end
describe "GET /password/reset" do
test "with valid token - shows form", %{conn: conn} do
user = insert(:user)
@ -703,6 +827,20 @@ defmodule PlausibleWeb.AuthControllerTest do
end
end
on_ee do
describe "DELETE /me - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user, :log_in]
test "refuses to delete SSO user", %{conn: conn, user: user} do
conn = delete(conn, "/me")
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert Repo.reload(user)
end
end
end
describe "DELETE /me" do
setup [:create_user, :log_in, :create_site]
use Plausible.Repo
@ -1079,6 +1217,23 @@ defmodule PlausibleWeb.AuthControllerTest do
end
end
on_ee do
describe "POST /2fa/disable - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user, :log_in]
test "refuses to disable for SSO user", %{conn: conn, user: user} do
{:ok, user, _} = Auth.TOTP.initiate(user)
{:ok, _, _} = Auth.TOTP.enable(user, :skip_verify)
conn = post(conn, Routes.auth_path(conn, :disable_2fa), %{password: "password"})
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert user |> Repo.reload!() |> Auth.TOTP.enabled?()
end
end
end
describe "POST /2fa/recovery_codes" do
setup [:create_user, :log_in]

View File

@ -1,6 +1,7 @@
defmodule PlausibleWeb.SettingsControllerTest do
use PlausibleWeb.ConnCase, async: true
use Bamboo.Test
use Plausible
use Plausible.Repo
use Plausible.Teams.Test
@ -672,6 +673,23 @@ defmodule PlausibleWeb.SettingsControllerTest do
end
end
on_ee do
describe "POST /preferences/name - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user, :log_in]
test "refuses to update for SSO user", %{conn: conn, user: user} do
conn =
post(conn, Routes.settings_path(conn, :update_name), %{
"user" => %{"name" => "New name"}
})
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert Repo.reload!(user).name == user.name
end
end
end
describe "POST /security/password" do
setup [:create_user, :log_in]
@ -843,6 +861,37 @@ defmodule PlausibleWeb.SettingsControllerTest do
end
end
on_ee do
describe "POST /security/password - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user, :log_in]
test "refuses to update for SSO user", %{conn: conn, user: user} do
password = "very-long-very-secret-123"
new_password = "super-long-super-secret-999"
original =
user
|> Auth.User.set_password(password)
|> Repo.update!()
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{
"password" => new_password,
"old_password" => password,
"password_confirmation" => new_password
}
})
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
current_hash = Repo.reload!(user).password_hash
assert current_hash == original.password_hash
assert Plausible.Auth.Password.match?(password, current_hash)
end
end
end
describe "POST /security/email" do
setup [:create_user, :log_in]
@ -948,6 +997,34 @@ defmodule PlausibleWeb.SettingsControllerTest do
end
end
on_ee do
describe "POST /security/email - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user, :log_in]
test "refuses to update for SSO user", %{conn: conn, user: user} do
password = "very-long-very-secret-123"
user
|> Auth.User.set_password(password)
|> Repo.update!()
assert user.email_verified
conn =
post(conn, Routes.settings_path(conn, :update_email), %{
"user" => %{"email" => "new" <> user.email, "password" => password}
})
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
updated_user = Repo.reload!(user)
assert updated_user.email == user.email
assert updated_user.email_verified
end
end
end
describe "POST /security/email/cancel" do
setup [:create_user, :log_in]
@ -1249,6 +1326,45 @@ defmodule PlausibleWeb.SettingsControllerTest do
end
end
on_ee do
describe "Account Settings - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user, :log_in]
test "does not allow to update name in preferences", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :preferences))
assert html = html_response(conn, 200)
refute html =~ "Change Name"
end
test "does not allow to update email in security settings", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :security))
assert html = html_response(conn, 200)
refute html =~ "Change Email"
end
test "does not allow to change password in security settings", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :security))
assert html = html_response(conn, 200)
refute html =~ "Change Password"
end
test "does not allow to disable 2FA in security settings", %{conn: conn, user: user} do
{:ok, user, _} = Auth.TOTP.initiate(user)
{:ok, _, _} = Auth.TOTP.enable(user, :skip_verify)
conn = get(conn, Routes.settings_path(conn, :security))
assert html = html_response(conn, 200)
assert text_of_element(html, "button[disabled]") =~ "Disable 2FA"
end
test "does not show account danger zone", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :preferences))
assert html = html_response(conn, 200)
refute html =~ "/settings/danger-zone"
end
end
end
describe "Team Settings" do
setup [:create_user, :log_in]

View File

@ -11,6 +11,7 @@ defmodule PlausibleWeb.Live.SSOMangementTest do
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
alias Plausible.Auth
alias Plausible.Auth.SSO
@cert_pem """
@ -241,9 +242,8 @@ defmodule PlausibleWeb.Live.SSOMangementTest do
assert element_exists?(html, "button#enable-force-sso-toggle[disabled]")
user
|> Ecto.Changeset.change(totp_enabled: true, totp_secret: "secret")
|> Plausible.Repo.update!()
{:ok, user, _} = Auth.TOTP.initiate(user)
{:ok, _user, _} = Auth.TOTP.enable(user, :skip_verify)
identity = new_identity("Lance Wurst", "lance@org.example.com")
{:ok, _, _, _sso_user} = SSO.provision_user(identity)
@ -277,15 +277,6 @@ defmodule PlausibleWeb.Live.SSOMangementTest do
{:ok, lv, html} = live(conn, Routes.sso_path(conn, :sso_settings))
{lv, html}
end
defp new_identity(name, email, id \\ Ecto.UUID.generate()) do
%SSO.Identity{
id: id,
name: name,
email: email,
expires_at: NaiveDateTime.add(NaiveDateTime.utc_now(:second), 6, :hour)
}
end
end
end
end

View File

@ -50,6 +50,51 @@ defmodule PlausibleWeb.Live.TeamMangementTest do
end
end
on_ee do
describe "live - SSO user" do
setup [
:create_user,
:create_team,
:setup_sso,
:provision_sso_user,
:log_in,
:setup_team
]
test "fails to save layout with SSO user updated to owner with Force SSO but without MFA",
%{
conn: conn,
team: team,
user: user
} do
{:ok, user, _} = Plausible.Auth.TOTP.initiate(user)
{:ok, _user, _} = Plausible.Auth.TOTP.enable(user, :skip_verify)
{:ok, team} = Plausible.Auth.SSO.set_force_sso(team, :all_but_owners)
member = add_member(team, role: :viewer)
{:ok, _, _, _member} =
new_identity(member.name, member.email)
|> Plausible.Auth.SSO.provision_user()
lv = get_liveview(conn)
html = render(lv)
assert text_of_element(
html,
"#{member_el()}:nth-of-type(1) button"
) == "Owner"
assert text_of_element(html, "#{member_el()}:nth-of-type(2) button") == "Viewer"
change_role(lv, 2, "owner")
html = render(lv)
assert html =~ "User must have 2FA enabled to become an owner"
end
end
end
describe "live" do
setup [:create_user, :log_in, :create_team, :setup_team]

View File

@ -76,7 +76,7 @@ defmodule PlausibleWeb.UserAuthTest do
conn: conn,
user: user
} do
team = new_site(owner: user).team
team = new_site(owner: user).team |> Plausible.Teams.complete_setup()
integration = SSO.initiate_saml_integration(team)
domain = "example-#{Enum.random(1..10_000)}.com"
user = user |> Ecto.Changeset.change(email: "jane@" <> domain) |> Repo.update!()
@ -189,12 +189,12 @@ defmodule PlausibleWeb.UserAuthTest do
conn: conn,
user: user
} do
team = new_site().team
team = new_site().team |> Plausible.Teams.complete_setup()
integration = SSO.initiate_saml_integration(team)
domain = "example-#{Enum.random(1..10_000)}.com"
user = user |> Ecto.Changeset.change(email: "jane@" <> domain) |> Repo.update!()
add_member(team, user: user, role: :editor)
another_team = new_site().team
another_team = new_site().team |> Plausible.Teams.complete_setup()
add_member(another_team, user: user, role: :viewer)
{:ok, sso_domain} = SSO.Domains.add(integration, domain)
@ -211,13 +211,31 @@ defmodule PlausibleWeb.UserAuthTest do
assert get_session(conn, :user_token)
end
defp new_identity(name, email, id \\ Ecto.UUID.generate()) do
%SSO.Identity{
id: id,
name: name,
email: email,
expires_at: NaiveDateTime.add(NaiveDateTime.utc_now(:second), 6, :hour)
}
test "passes through for user matching SSO identity with active personal team, redirecting to team",
%{
conn: conn,
user: user
} do
team = new_site().team
integration = SSO.initiate_saml_integration(team)
domain = "example-#{Enum.random(1..10_000)}.com"
user = user |> Ecto.Changeset.change(email: "jane@" <> domain) |> Repo.update!()
add_member(team, user: user, role: :editor)
# personal team with site created
new_site(owner: user)
{:ok, sso_domain} = SSO.Domains.add(integration, domain)
_sso_domain = SSO.Domains.verify(sso_domain, skip_checks?: true)
identity = new_identity(user.name, user.email)
conn =
conn
|> init_session()
|> UserAuth.log_in_user(identity)
assert redirected_to(conn, 302) == Routes.site_path(conn, :index, __team: team.identifier)
assert get_session(conn, :user_token)
end
end
end

View File

@ -1,5 +1,6 @@
defmodule Plausible.TestUtils do
use Plausible.Repo
use Plausible
alias Plausible.Factory
defmacro __using__(_) do
@ -52,6 +53,7 @@ defmodule Plausible.TestUtils do
conn =
conn
|> Plug.Conn.fetch_session()
|> Plug.Conn.put_session(:current_team_id, team.identifier)
{:ok, conn: conn, team: team}
@ -99,6 +101,27 @@ defmodule Plausible.TestUtils do
{:ok, conn: conn}
end
on_ee do
alias Plausible.Auth.SSO
def setup_sso(%{team: team}) do
team = Plausible.Teams.complete_setup(team)
integration = SSO.initiate_saml_integration(team)
{:ok, sso_domain} = SSO.Domains.add(integration, "example.com")
_sso_domain = SSO.Domains.verify(sso_domain, skip_checks?: true)
{:ok, team: team, sso_integration: integration, sso_domain: sso_domain}
end
def provision_sso_user(%{user: user}) do
identity = new_identity(user.name, user.email)
{:ok, _, _, sso_user} = SSO.provision_user(identity)
{:ok, user: sso_user}
end
end
def init_session(conn) do
opts =
Plug.Session.init(
@ -328,4 +351,15 @@ defmodule Plausible.TestUtils do
ExUnit.Callbacks.on_exit(fn -> File.rm_rf!(tmp_dir) end)
tmp_dir
end
on_ee do
def new_identity(name, email, id \\ Ecto.UUID.generate()) do
%Plausible.Auth.SSO.Identity{
id: id,
name: name,
email: email,
expires_at: NaiveDateTime.add(NaiveDateTime.utc_now(:second), 6, :hour)
}
end
end
end