analytics/test/plausible_web/controllers/sso_controller_test.exs

473 lines
15 KiB
Elixir

defmodule PlausibleWeb.SSOControllerTest do
use PlausibleWeb.ConnCase, async: true
use Plausible
@moduletag :ee_only
on_ee do
import Plausible.Teams.Test
import Plausible.Test.Support.HTML
alias Plausible.Auth
alias Plausible.Auth.SSO
alias Plausible.Repo
setup do
owner = new_user()
team = new_site(owner: owner).team |> Plausible.Teams.complete_setup()
integration = SSO.initiate_saml_integration(team)
domain = "example-#{Enum.random(1..10_000)}.com"
{:ok, sso_domain} = SSO.Domains.add(integration, domain)
sso_domain = SSO.Domains.verify(sso_domain, skip_checks?: true)
{:ok,
owner: owner, team: team, integration: integration, domain: domain, sso_domain: sso_domain}
end
describe "settings item visibility" do
test "sso team settings item renders when SSO feature plan is added", %{conn: conn} do
user =
new_user() |> subscribe_to_enterprise_plan(features: [Plausible.Billing.Feature.SSO])
team = new_site(owner: user).team |> Plausible.Teams.complete_setup()
{:ok, conn: conn} = log_in(%{conn: conn, user: user})
conn = set_current_team(conn, team)
conn = get(conn, Routes.settings_path(conn, :team_general))
assert html = html_response(conn, 200)
assert html =~ "/settings/sso/general"
refute html =~ "/settings/sso/info"
end
test "sso team settings item is hidden when there's no SSO plan feature", %{conn: conn} do
user = new_user()
team = new_site(owner: user).team |> Plausible.Teams.complete_setup()
{:ok, conn: conn} = log_in(%{conn: conn, user: user})
conn = set_current_team(conn, team)
conn = get(conn, Routes.settings_path(conn, :team_general))
assert html = html_response(conn, 200)
refute html =~ "/settings/sso/general"
assert html =~ "/settings/sso/info"
end
end
describe "login_form/2" do
test "renders login view", %{conn: conn} do
conn = get(conn, Routes.sso_path(conn, :login_form, prefer: "sso"))
assert html = html_response(conn, 200)
assert html =~ "Enter your Single Sign-On email"
assert element_exists?(html, "input[name=email]")
assert text_of_attr(html, ~s|input[name="return_to"]|, "value") == nil
end
test "renders autosubmit js snippet when instructed", %{conn: conn} do
conn =
get(
conn,
Routes.sso_path(conn, :login_form,
prefer: "sso",
email: "user@example.com",
autosubmit: true
)
)
assert html = html_response(conn, 200)
assert html =~ "Enter your Single Sign-On email"
assert text_of_attr(html, "input[name=email]", "value") == "user@example.com"
assert html =~ ~s|document.getElementById("sso-login-form").submit()|
end
test "passes return_to parameter to form", %{conn: conn} do
conn = get(conn, Routes.sso_path(conn, :login_form, return_to: "/sites", prefer: "sso"))
assert html = html_response(conn, 200)
assert text_of_attr(html, "input[name=return_to]", "value") == "/sites"
end
test "renders error if provided in login_error flash message", %{conn: conn} do
conn =
conn
|> init_session()
|> fetch_session()
|> fetch_flash()
|> put_flash(:login_error, "Wrong email.")
conn = get(conn, Routes.sso_path(conn, :login_form, return_to: "/sites"))
assert html = html_response(conn, 200)
assert html =~ "Wrong email."
assert element_exists?(html, "input[name=email]")
assert text_of_attr(html, "input[name=return_to]", "value") == "/sites"
end
end
describe "login/2" do
test "redirects to SAML signin on matching integration", %{
conn: conn,
domain: domain,
integration: integration
} do
email = "paul@" <> domain
conn = post(conn, Routes.sso_path(conn, :login), %{"email" => email})
assert redirected_to(conn, 302) ==
Routes.sso_path(conn, :saml_signin, integration.identifier,
email: email,
return_to: ""
)
end
test "passes redirect path if provided", %{
conn: conn,
domain: domain,
integration: integration
} do
email = "claire@" <> domain
conn =
post(conn, Routes.sso_path(conn, :login), %{"email" => email, "return_to" => "/sites"})
assert redirected_to(conn, 302) ==
Routes.sso_path(conn, :saml_signin, integration.identifier,
email: email,
return_to: "/sites"
)
end
test "renders login form with error on no matching integration", %{conn: conn} do
conn =
post(conn, Routes.sso_path(conn, :login), %{
"email" => "nomatch@example.com",
"return_to" => "/sites"
})
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form)
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) == "Wrong email."
end
test "limits login attempts to 5 per minute", %{conn: conn} do
conn = put_req_header(conn, "x-forwarded-for", "1.2.3.9")
email = "invalid@example.com"
response =
eventually(
fn ->
Enum.each(1..5, fn _ ->
post(conn, Routes.sso_path(conn, :login), %{"email" => email})
end)
conn = post(conn, Routes.sso_path(conn, :login), %{"email" => email})
{conn.status == 429, conn}
end,
500
)
assert html_response(response, 429) =~ "Too many login attempts"
end
end
describe "saml_signin/2 (fake SAML)" do
test "renders autosubmitted form", %{conn: conn, domain: domain, integration: integration} do
email = "jesper@" <> domain
conn =
get(
conn,
Routes.sso_path(conn, :saml_signin, integration.identifier,
email: email,
return_to: "/sites"
)
)
assert html = html_response(conn, 200)
assert html =~ "Processing Single Sign-On request..."
assert text_of_attr(html, "form#sso-req-form", "action") ==
Routes.sso_path(conn, :saml_consume, integration.identifier)
assert text_of_attr(html, "input[name=email]", "value") == email
assert text_of_attr(html, "input[name=return_to]", "value") == "/sites"
end
end
describe "saml_consume/2 (fake SAML)" do
test "provisions identity for new user", %{
conn: conn,
domain: domain,
integration: integration
} do
email = "dana.lake@" <> domain
conn =
post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), %{
"email" => email,
"return_to" => "/sites"
})
assert redirected_to(conn, 302) == "/sites"
assert %{sessions: [sso_session]} =
user = Repo.get_by(Plausible.Auth.User, email: email) |> Repo.preload(:sessions)
assert user.type == :sso
assert user.email == email
assert user.name == "Dana Lake"
assert get_session(conn, :user_token) == sso_session.token
end
test "provisions identity for existing user", %{
conn: conn,
team: team,
domain: domain,
integration: integration
} do
email = "dana@" <> domain
existing_user = new_user(name: "Dana Woodworth", email: email)
add_member(team, user: existing_user, role: :admin)
conn =
post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), %{
"email" => email,
"return_to" => "/sites"
})
assert redirected_to(conn, 302) == "/sites"
assert %{sessions: [sso_session]} =
user = Repo.get_by(Plausible.Auth.User, email: email) |> Repo.preload(:sessions)
assert user.type == :sso
assert user.email == email
assert user.name == "Dana Woodworth"
assert get_session(conn, :user_token) == sso_session.token
end
test "redirects to login when no matching integration found", %{conn: conn} do
conn =
post(conn, Routes.sso_path(conn, :saml_consume, Ecto.UUID.generate()), %{
"email" => "missed@example.com",
"return_to" => "/sites"
})
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) == "Wrong email."
end
end
describe "sso_settings/2" do
setup [:create_user, :log_in, :create_team]
test "redirects when team is not setup", %{conn: conn, team: team} do
conn = set_current_team(conn, team)
conn = get(conn, Routes.sso_path(conn, :sso_settings))
assert redirected_to(conn, 302) == "/sites"
end
test "redirects when team lacks SSO plan feature", %{conn: conn, team: team} do
team = Plausible.Teams.complete_setup(team)
conn = set_current_team(conn, team)
conn = get(conn, Routes.sso_path(conn, :sso_settings))
assert redirected_to(conn, 302) == "/sites"
end
test "renders when team has SSO plan feature", %{conn: conn, team: team, user: user} do
user |> subscribe_to_enterprise_plan(features: [Plausible.Billing.Feature.SSO])
team = Plausible.Teams.complete_setup(team)
conn = set_current_team(conn, team)
conn = get(conn, Routes.sso_path(conn, :sso_settings))
assert html = html_response(conn, 200)
assert html =~ "Configure and manage Single Sign-On for your team"
end
end
describe "provision_notice/2" do
test "renders the notice", %{conn: conn} do
conn = get(conn, Routes.sso_path(conn, :provision_notice))
assert html = html_response(conn, 200)
assert html =~ "Single Sign-On enforcement"
assert html =~ "To access this team, you must first"
assert html =~ "log out"
assert html =~ "and log in as SSO user"
end
end
describe "provision_issue/2" do
test "renders issue for not_a_member", %{conn: conn} do
conn = get(conn, Routes.sso_path(conn, :provision_issue, issue: "not_a_member"))
assert html = html_response(conn, 200)
assert html =~ "Single Sign-On enforcement"
assert html =~ "To access this team, you must join as a team member first"
end
test "renders issue for multiple_memberships", %{conn: conn} do
conn = get(conn, Routes.sso_path(conn, :provision_issue, issue: "multiple_memberships"))
assert html = html_response(conn, 200)
assert html =~ "Single Sign-On enforcement"
assert html =~ "To access this team, you must first leave all other teams"
end
test "renders issue for multiple_memberships_noforce", %{conn: conn} do
conn =
get(
conn,
Routes.sso_path(conn, :provision_issue, issue: "multiple_memberships_noforce")
)
assert html = html_response(conn, 200)
assert html =~ "Single Sign-On enforcement"
assert html =~ "To log in as an SSO user, you must first leave all other teams"
assert html =~ "Log in"
assert html =~ "with your email and password"
end
test "renders issue for active_personal_team", %{conn: conn} do
conn = get(conn, Routes.sso_path(conn, :provision_issue, issue: "active_personal_team"))
assert html = html_response(conn, 200)
assert html =~ "Single Sign-On enforcement"
assert html =~ "To access this team, you must either remove or transfer all sites"
end
test "renders issue for active_personal_team_noforce", %{conn: conn} do
conn =
get(
conn,
Routes.sso_path(conn, :provision_issue, issue: "active_personal_team_noforce")
)
assert html = html_response(conn, 200)
assert html =~ "Single Sign-On enforcement"
assert html =~
"To log in as an SSO user, you must either remove or transfer all sites"
assert html =~ "Log in"
assert html =~ "with your email and password"
end
end
describe "team_sessions/2" do
setup %{conn: conn, team: team, owner: owner} do
%{conn: conn} =
%{conn: conn, user: owner}
|> setup_do(&log_in/1)
conn = set_current_team(conn, team)
{:ok, conn: conn}
end
test "lists SSO sessions", %{conn: conn, domain: domain, integration: integration} do
now = NaiveDateTime.utc_now(:second)
%{user: user1} =
%{user: %{name: "Frank Rubin", email: "frank@" <> domain}, sso_integration: integration}
|> setup_do(&provision_sso_user/1)
Auth.UserSessions.create!(user1, "Device 1", now: NaiveDateTime.shift(now, hour: -3))
%{user: user2} =
%{
user: %{name: "Grace Holmes", email: "grace@" <> domain},
sso_integration: integration
}
|> setup_do(&provision_sso_user/1)
Auth.UserSessions.create!(user2, "Device 2")
Auth.UserSessions.create!(user2, "Device 3", now: NaiveDateTime.shift(now, hour: -6))
%{user: user3} =
%{user: %{name: "Kate Loselet", email: "kate@" <> domain}, sso_integration: integration}
|> setup_do(&provision_sso_user/1)
Auth.UserSessions.create!(user3, "Device 4", now: NaiveDateTime.shift(now, hour: -2))
conn = get(conn, Routes.sso_path(conn, :team_sessions))
assert html = html_response(conn, 200)
assert ["Grace Holmes", "Kate Loselet", "Frank Rubin", "Grace Holmes"] =
find(html, "table#sso-sessions-list tr td:nth-of-type(1)")
|> Enum.map(&text/1)
assert ["Device 2", "Device 4", "Device 1", "Device 3"] =
find(html, "table#sso-sessions-list tr td:nth-of-type(2)")
|> Enum.map(&text/1)
end
test "shows empty state when there are no sessions", %{conn: conn} do
conn = get(conn, Routes.sso_path(conn, :team_sessions))
assert html = html_response(conn, 200)
assert html =~ "There are currently no active SSO sessions"
end
end
describe "delete_session/2" do
setup %{conn: conn, team: team, owner: owner} do
%{conn: conn} =
%{conn: conn, user: owner}
|> setup_do(&log_in/1)
conn = set_current_team(conn, team)
{:ok, conn: conn}
end
test "revokes session and redirects back to sessions list", %{
conn: conn,
domain: domain,
integration: integration
} do
%{user: user} =
%{user: %{name: "Frank Rubin", email: "frank@" <> domain}, sso_integration: integration}
|> setup_do(&provision_sso_user/1)
session = Auth.UserSessions.create!(user, "Unknown")
conn = delete(conn, Routes.sso_path(conn, :delete_session, session.id))
assert redirected_to(conn, 302) == Routes.sso_path(conn, :team_sessions)
assert Phoenix.Flash.get(conn.assigns.flash, :success) ==
"Session logged out successfully"
refute Repo.reload(session)
end
end
end
end