analytics / test /plausible_web /controllers /sso_controller_sync_test.exs
Leon4gr45's picture
Upload folder using huggingface_hub (part 6)
6778ee0 verified
Raw
History Blame Contribute Delete
18 kB
defmodule PlausibleWeb.SSOControllerSyncTest do
use PlausibleWeb.ConnCase
@moduletag :ee_only
on_ee do
alias Plausible.Auth
alias Plausible.Auth.SSO
alias Plausible.Repo
@cert_pem """
-----BEGIN CERTIFICATE-----
MIICmjCCAYICCQDX5sKPsYV3+jANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0
ZXN0MB4XDTE5MTIyMzA5MDI1MVoXDTIwMDEyMjA5MDI1MVowDzENMAsGA1UEAwwE
dGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMdtDJ278DQTp84O
5Nq5F8s5YOR34GFOGI2Swb/3pU7X7918lVljiKv7WVM65S59nJSyXV+fa15qoXLf
sdRnq3yw0hTSTs2YDX+jl98kK3ksk3rROfYh1LIgByj4/4NeNpExgeB6rQk5Ay7Y
S+ARmMzEjXa0favHxu5BOdB2y6WvRQyjPS2lirT/PKWBZc04QZepsZ56+W7bd557
tdedcYdY/nKI1qmSQClG2qgslzgqFOv1KCOw43a3mcK/TiiD8IXyLMJNC6OFW3xT
L/BG6SOZ3dQ9rjQOBga+6GIaQsDjC4Xp7Kx+FkSvgaw0sJV8gt1mlZy+27Sza6d+
hHD2pWECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAm2fk1+gd08FQxK7TL04O8EK1
f0bzaGGUxWzlh98a3Dm8+OPhVQRi/KLsFHliLC86lsZQKunYdDB+qd0KUk2oqDG6
tstG/htmRYD/S/jNmt8gyPAVi11dHUqW3IvQgJLwxZtoAv6PNs188hvT1WK3VWJ4
YgFKYi5XQYnR5sv69Vsr91lYAxyrIlMKahjSW1jTD3ByRfAQghsSLk6fV0OyJHyh
uF1TxOVBVf8XOdaqfmvD90JGIPGtfMLPUX4m35qaGAU48PwCL7L3cRHYs9wZWc0i
fXZcBENLtHYCLi5txR8c5lyHB9d3AQHzKHMFNjLswn5HsckKg83RH7+eVqHqGw==
-----END CERTIFICATE-----
"""
@other_cert_pem """
-----BEGIN CERTIFICATE-----
MIIFdTCCA12gAwIBAgIUNcATm3CidmlEMMsZa9KBZpWYCVcwDQYJKoZIhvcNAQEL
BQAwYzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEcMBoGA1UEAwwTc29tZWlkcC5leGFt
cGxlLmNvbTAeFw0yNTA1MjExMjI5MzVaFw0yNjA1MjExMjI5MzVaMGMxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxHDAaBgNVBAMME3NvbWVpZHAuZXhhbXBsZS5jb20wggIi
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1N6Drbjed+lFXpOYvE6Efgndy
W7kYiO8LqQTr4UwVrp9ArxgYuK4TrcNRh2rhS08xAzNTo+NqnJOm95baG97ADYk1
TqVIKxzaFurv+L/Na0wVXyeNUtxIVKF59uElsg2YLm5eQhL9fmN8jVINCvwDPzxc
Ihm6mQOaL/i/0DGINOqwHG9MGMZ11AeOM0wKMuXJ2+aKjHOCedhMYVuOaHZgLkcX
Zzgiv7itm3+JpCjL474MMfibiqKHR0e3QRNcsEC13f/LD8BAGOwsKLznFC8Uctms
48EDNbxxLG01jVbnJSxRrcDN3RUDjtCdHyaTCCFJAgmldHKKua3VQEynOwJIkFMC
fpL1LpLvATzIt0cT1ESb1RHIlgacmESVn/TW2QjO5tp4FAu7GJK+5xY7jPvI6saG
oUHsk0zo9obLK8WYneF19ln+Ea5ZCl9PcTi559AKGpYzpL/9uxoPT1zxxTn6c2lt
4xkxkuHtYqi/ENHGdo4CLBL93GDZEilSVmZjD/9N9990yWbPXXQ0eNoFckYSZuls
HaWz8W5c046/ob8mASI6wzAUCkO9Zz4WbIj9A+mNZB32hMZbMA02gU//ffvNkFjL
DGlNbROCg2DX64rvGs/RuqhuDVCnVfid9B36Cgs76GWI8dCInEfyZMtiqUb7E8Oe
BPVwtTscz1StlF/0cQIDAQABoyEwHzAdBgNVHQ4EFgQU9lvXH4X04v99rrwKNzsw
pNQP/dUwDQYJKoZIhvcNAQELBQADggIBAJD0MD+OK58vlP2HEoKLQYAKYM/4NsBz
vSK1PtZsEj0fqiuu66ceH0wlKlGquRad4Z+LXMptu1DzMNmJsf0zSQfleGFks3xI
86hgkQ7f0qjs+YJzjJUxF9H8zX4jJk5poOqOJwStHBCDLsUmIxnT7/il3jlT0Nj4
cVs4946pCg7rP1kR9jojFD5yvzKoRBJG3/qvFnzAi8cDv9CRjSgoDTZyzZmwdCgu
NioW7YeFCtvYxvY7HDXinwq/w8Gn3n8zdISoAqSpYrt5Y5ygJGiEYVDWdA50a6PC
gq5xt8RCizz1L7a5BUJFMCQ0pyAUuODTndPUGLT8i7jFgzhamFPD72zFMk2+IabE
Dutyt2GFeTQ75wL8QvfsKm29Vd5EjAsdfmup3hCpLGqF3g8Sh0aXDrj8KPqIecuS
gkL69M9iXfnwZhTo23zUuFjBNoAIPXkNKXiJS7p9IEpYRVnlPYLToSEnnzptoPPQ
zMBb8x/UMMtNYkyehSLhuIPrRLvv3eth7Hq3hA7tOCRyyf78tReVm+VoRx6AK68v
5ufxMKBFRTNoLIN3sD+DmSUNY+CaHxRMDhSESy0Ac/95J2yKi+Y1Kml2GV53pSlT
6FPm8B0R9YXM7lHhTLyL7DYqnvklkLh2bUqCLyBowynPyGqdYV4DbFSiST14fGXR
mNEYF78kg0IA
-----END CERTIFICATE-----
"""
describe "saml_signin/2 (real SAML)" do
setup do
patch_env(:sso_saml_adapter, PlausibleWeb.SSO.RealSAMLAdapter)
team = new_site().team |> Plausible.Teams.complete_setup()
integration = SSO.initiate_saml_integration(team)
{:ok, integration} =
SSO.update_integration(integration, %{
idp_signin_url: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
idp_entity_id: "http://localhost:8080/simplesaml/saml2/idp/metadata.php",
idp_cert_pem: @cert_pem
})
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, team: team, integration: integration, domain: domain, sso_domain: sso_domain}
end
test "redirects to IdP", %{conn: conn, domain: domain, integration: integration} do
email = "paul@" <> domain
conn =
get(
conn,
Routes.sso_path(conn, :saml_signin, integration.identifier,
email: email,
return_to: "/sites"
)
)
session = fetch_cookies(conn, encrypted: ["session_saml"]).cookies["session_saml"]
assert String.length(session.relay_state) > 0
assert session.return_to == "/sites"
assert url = redirected_to(conn, 302)
assert {:ok, uri} = URI.new(url)
assert uri.host == "localhost"
assert uri.port == 8080
assert uri.scheme == "http"
assert %{
"RelayState" => relay_state,
"SAMLEncoding" => "urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE",
"SAMLRequest" => saml_request,
"login_hint" => ^email
} = URI.decode_query(uri.query)
assert relay_state == session.relay_state
xml = saml_request |> Base.decode64!() |> :zlib.unzip()
assert {:ok, root_node} = SimpleXml.parse(xml)
assert {:ok, issuer_node} = SimpleXml.XmlNode.first_child(root_node, "saml:Issuer")
assert {:ok, acs_url} = SimpleXml.XmlNode.text(issuer_node)
assert acs_url == SSO.SAMLConfig.entity_id(integration)
end
test "redirects to login if integration not found", %{conn: conn, domain: domain} do
email = "paul@" <> domain
conn =
get(
conn,
Routes.sso_path(conn, :saml_signin, Ecto.UUID.generate(),
email: email,
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) ==
"We couldn't find a Single Sign-On account for that email."
end
end
@assertion File.read!("extra/fixture/assertion.xml")
@assertion_missing_email File.read!("extra/fixture/assertion_missing_email.xml")
@assertion_invalid_email File.read!("extra/fixture/assertion_invalid_email.xml")
@assertion_missing_name File.read!("extra/fixture/assertion_missing_name.xml")
describe "saml_consume/2 (real SAML)" do
setup %{conn: conn} do
patch_env(:sso_saml_adapter, PlausibleWeb.SSO.RealSAMLAdapter)
team = new_site().team |> Plausible.Teams.complete_setup()
integration = SSO.initiate_saml_integration(team)
{:ok, integration} =
SSO.update_integration(integration, %{
idp_signin_url: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
idp_entity_id: "http://localhost:8080/simplesaml/saml2/idp/metadata.php",
idp_cert_pem: @cert_pem
})
domain = "plausible.test"
{:ok, sso_domain} = SSO.Domains.add(integration, domain)
sso_domain = SSO.Domains.verify(sso_domain, skip_checks?: true)
{:ok, root_node} = @assertion |> SimpleXml.parse()
{:ok, assertion_node} = SimpleXml.XmlNode.first_child(root_node, "*:Assertion")
{:ok, subject_node} = SimpleXml.XmlNode.first_child(assertion_node, "*:Subject")
{:ok, name_id_node} = SimpleXml.XmlNode.first_child(subject_node, "*:NameID")
{:ok, name_id} = SimpleXml.XmlNode.text(name_id_node)
email = "user@plausible.test"
conn =
get(
conn,
Routes.sso_path(conn, :saml_signin, integration.identifier,
email: email,
return_to: "/sites"
)
)
saml_session = fetch_cookies(conn, encrypted: ["session_saml"]).cookies["session_saml"]
relay_state = saml_session.relay_state
conn =
conn
|> recycle()
|> prepare_conn()
{:ok,
team: team,
integration: integration,
domain: domain,
sso_domain: sso_domain,
email: email,
conn: conn,
name_id: name_id,
relay_state: relay_state}
end
test "provisions identity and logs in", %{
conn: conn,
team: team,
integration: integration,
sso_domain: sso_domain,
relay_state: relay_state,
name_id: name_id
} do
params = %{
"SAMLResponse" => Base.encode64(@assertion),
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == "/sites"
session = get_session(conn)
assert session["current_team_id"] == team.identifier
assert {:ok, user_session} = Auth.UserSessions.get_by_token(session["user_token"])
assert user_session.user.email == "user@plausible.test"
assert user_session.user.type == :sso
assert user_session.user.name == "Jane Smith"
assert user_session.user.sso_integration_id == integration.id
assert user_session.user.sso_domain_id == sso_domain.id
assert user_session.user.sso_identity_id == name_id
timeout_minutes = team.policy.sso_session_timeout_minutes
timeout_threshold_lower =
NaiveDateTime.add(NaiveDateTime.utc_now(:second), timeout_minutes - 2, :minute)
timeout_threshold_upper =
NaiveDateTime.add(NaiveDateTime.utc_now(:second), timeout_minutes + 2, :minute)
assert NaiveDateTime.compare(user_session.timeout_at, timeout_threshold_lower) == :gt
assert NaiveDateTime.compare(user_session.timeout_at, timeout_threshold_upper) == :lt
assert entry = audited_entry("sso_login_success", team_id: integration.team_id)
assert entry.change["email"] == "user@plausible.test"
assert entry.change["name"] == "Jane Smith"
assert entry.change["integration_id"] == integration.identifier
end
test "redirects with error when no matching integration found", %{
conn: conn,
relay_state: relay_state
} do
params = %{
"SAMLResponse" => Base.encode64(@assertion),
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, Ecto.UUID.generate()), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"We couldn't find a Single Sign-On account for that email."
end
test "redirects with error on mismatch of RelayState", %{
conn: conn,
integration: integration
} do
params = %{
"SAMLResponse" => Base.encode64(@assertion),
"RelayState" => Ecto.UUID.generate()
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :invalid_relay_state)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":invalid_relay_state"
}
)
end
test "redirects with error on missing relay state", %{
conn: conn,
integration: integration
} do
params = %{"SAMLResponse" => Base.encode64(@assertion)}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :invalid_relay_state)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":invalid_relay_state"
}
)
end
test "redirects with error on malformed assertion", %{
conn: conn,
integration: integration,
relay_state: relay_state
} do
params = %{
"SAMLResponse" => "malformed",
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :base64_decoding_failed)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":base64_decoding_failed"
}
)
end
test "redirects with error on malformed certificate in config (should not happen)", %{
conn: conn,
integration: integration,
relay_state: relay_state
} do
Repo.query!(
"""
UPDATE sso_integrations SET config = jsonb_set(config, '{idp_cert_pem}', '"invalid"')
WHERE id = $1
""",
[integration.id]
)
params = %{
"SAMLResponse" => Base.encode64(@assertion),
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :malformed_certificate)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":malformed_certificate"
}
)
end
test "redirects with error on mismatched certificate in config", %{
conn: conn,
integration: integration,
relay_state: relay_state
} do
{:ok, integration} = SSO.update_integration(integration, %{idp_cert_pem: @other_cert_pem})
params = %{
"SAMLResponse" => Base.encode64(@assertion),
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :digest_verification_failed)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":digest_verification_failed"
}
)
end
test "redirects with error on missing email attribute in assertion", %{
conn: conn,
integration: integration,
relay_state: relay_state
} do
params = %{
"SAMLResponse" => Base.encode64(@assertion_missing_email),
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :missing_email_attribute)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":missing_email_attribute"
}
)
end
test "redirects with error on invalid email attribute in assertion", %{
conn: conn,
integration: integration,
relay_state: relay_state
} do
params = %{
"SAMLResponse" => Base.encode64(@assertion_invalid_email),
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :invalid_email_attribute)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":invalid_email_attribute"
}
)
end
test "redirects with error on missing name attributes in assertion", %{
conn: conn,
integration: integration,
relay_state: relay_state
} do
params = %{
"SAMLResponse" => Base.encode64(@assertion_missing_name),
"RelayState" => relay_state
}
conn = post(conn, Routes.sso_path(conn, :saml_consume, integration.identifier), params)
assert redirected_to(conn, 302) == Routes.sso_path(conn, :login_form, return_to: "/sites")
assert Phoenix.Flash.get(conn.assigns.flash, :login_error) ==
"Authentication failed (reason: :missing_name_attributes)"
assert audited_entry("sso_login_failure",
team_id: integration.team_id,
change: %{
"error" => ":missing_name_attributes"
}
)
end
end
end
end