Check for Sites API feature against respective team when using API key (#5753)

* Account for feature check error when adding custom prop via Sites API

* Expand Teams API with team membership check predicate

* Validate feature availability for Sites API endpoints

* Refactor tests to account for differrent errors
This commit is contained in:
Adrian Gruntkowski 2025-09-25 12:04:46 +02:00 committed by GitHub
parent 63a89cab8e
commit cfbcb3609f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 321 additions and 89 deletions

View File

@ -483,6 +483,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
{:missing, param} -> {:missing, param} ->
H.bad_request(conn, "Parameter `#{param}` is required to create a custom property") H.bad_request(conn, "Parameter `#{param}` is required to create a custom property")
{:error, :upgrade_required} ->
H.payment_required(
conn,
"Your current subscription plan does not include Custom Properties"
)
{:error, changeset} -> {:error, changeset} ->
%{allowed_event_props: [error | _]} = %{allowed_event_props: [error | _]} =
Ecto.Changeset.traverse_errors(changeset, fn {_msg, opts} -> Ecto.Changeset.traverse_errors(changeset, fn {_msg, opts} ->

View File

@ -31,6 +31,8 @@ defmodule Plausible.Teams.Memberships do
) )
end end
@spec team_role(Teams.Team.t(), Auth.User.t()) ::
{:ok, Teams.Membership.role()} | {:error, :not_a_member}
def team_role(team, user) do def team_role(team, user) do
result = result =
from(u in Auth.User, from(u in Auth.User,
@ -86,6 +88,7 @@ defmodule Plausible.Teams.Memberships do
end end
end end
@spec site_member?(Plausible.Site.t(), Auth.User.t() | nil) :: boolean()
def site_member?(site, user) do def site_member?(site, user) do
case site_role(site, user) do case site_role(site, user) do
{:ok, _} -> true {:ok, _} -> true
@ -93,6 +96,14 @@ defmodule Plausible.Teams.Memberships do
end end
end end
@spec team_member?(Teams.Team.t(), Auth.User.t()) :: boolean()
def team_member?(team, user) do
case team_role(team, user) do
{:ok, _} -> true
_ -> false
end
end
@spec has_editor_access?(Plausible.Site.t(), Auth.User.t() | nil) :: boolean() @spec has_editor_access?(Plausible.Site.t(), Auth.User.t() | nil) :: boolean()
def has_editor_access?(site, user) do def has_editor_access?(site, user) do
case site_role(site, user) do case site_role(site, user) do

View File

@ -122,13 +122,30 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
defp verify_by_scope(conn, api_key, "stats:read:" <> _ = scope) do defp verify_by_scope(conn, api_key, "stats:read:" <> _ = scope) do
with :ok <- check_scope(api_key, scope), with :ok <- check_scope(api_key, scope),
{:ok, site} <- find_site(conn.params["site_id"], api_key), {:ok, site} <- find_site(conn.params["site_id"], api_key),
:ok <- verify_site_access(api_key, site) do :ok <- verify_site_access(api_key, site, Plausible.Billing.Feature.StatsAPI) do
Plausible.OpenTelemetry.add_site_attributes(site) Plausible.OpenTelemetry.add_site_attributes(site)
site = Plausible.Repo.preload(site, :completed_imports) site = Plausible.Repo.preload(site, :completed_imports)
{:ok, assign(conn, :site, site)} {:ok, assign(conn, :site, site)}
end end
end end
defp verify_by_scope(conn, api_key, "sites:" <> scope_suffix = scope) do
feature =
case scope_suffix do
"read:" <> _ ->
Plausible.Billing.Feature.StatsAPI
"provision:" <> _ ->
Plausible.Billing.Feature.SitesAPI
end
with :ok <- check_scope(api_key, scope),
:ok <- maybe_verify_site_access(conn, api_key, feature),
:ok <- maybe_verify_team_access(conn, api_key, feature) do
{:ok, conn}
end
end
defp verify_by_scope(conn, api_key, scope) do defp verify_by_scope(conn, api_key, scope) do
with :ok <- check_scope(api_key, scope) do with :ok <- check_scope(api_key, scope) do
{:ok, conn} {:ok, conn}
@ -173,6 +190,26 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
end end
end end
defp maybe_verify_site_access(conn, api_key, feature) do
case find_site(conn.params["site_id"], api_key) do
{:ok, site} ->
verify_site_access(api_key, site, feature)
_ ->
:ok
end
end
defp maybe_verify_team_access(conn, api_key, feature) do
team = api_key.team || Teams.get(conn.params["team_id"])
if team do
verify_team_access(api_key, team, feature)
else
:ok
end
end
defp find_site(nil, _api_key), do: {:error, :missing_site_id} defp find_site(nil, _api_key), do: {:error, :missing_site_id}
defp find_site("rollup:" <> team_identifier, api_key) do defp find_site("rollup:" <> team_identifier, api_key) do
@ -198,7 +235,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
end end
end end
defp verify_site_access(api_key, site) do defp verify_site_access(api_key, site, feature) do
team = Repo.preload(site, :team).team team = Repo.preload(site, :team).team
is_member? = Plausible.Teams.Memberships.site_member?(site, api_key.user) is_member? = Plausible.Teams.Memberships.site_member?(site, api_key.user)
@ -214,7 +251,32 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
Teams.locked?(team) -> Teams.locked?(team) ->
{:error, :site_locked} {:error, :site_locked}
Plausible.Billing.Feature.StatsAPI.check_availability(team) !== :ok -> feature.check_availability(team) !== :ok ->
{:error, :upgrade_required}
is_member? ->
:ok
true ->
{:error, :invalid_api_key}
end
end
defp verify_team_access(api_key, team, feature) do
is_member? = Plausible.Teams.Memberships.team_member?(team, api_key.user)
is_super_admin? = Auth.is_super_admin?(api_key.user_id)
cond do
is_super_admin? ->
:ok
api_key.team_id && api_key.team_id != team.id ->
{:error, :invalid_api_key}
Teams.locked?(team) ->
{:error, :site_locked}
feature.check_availability(team) !== :ok ->
{:error, :upgrade_required} {:error, :upgrade_required}
is_member? -> is_member? ->
@ -260,11 +322,17 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
) )
end end
defp send_error(conn, _, {:error, :upgrade_required}) do defp send_error(conn, scope, {:error, :upgrade_required}) do
H.payment_required( feature =
conn, case scope do
"The account that owns this API key does not have access to Stats API. Please make sure you're using the API key of a subscriber account and that the subscription plan includes Stats API" "sites:provision:" <> _ ->
) Plausible.Billing.Feature.SitesAPI
_ ->
Plausible.Billing.Feature.StatsAPI
end
feature_payment_error(conn, feature)
end end
defp send_error(conn, _, {:error, :site_locked}) do defp send_error(conn, _, {:error, :site_locked}) do
@ -273,4 +341,13 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
"This Plausible site is locked due to missing active subscription. In order to access it, the site owner should subscribe to a suitable plan" "This Plausible site is locked due to missing active subscription. In order to access it, the site owner should subscribe to a suitable plan"
) )
end end
defp feature_payment_error(conn, feature) do
feature_name = feature.display_name()
H.payment_required(
conn,
"The account that owns this API key does not have access to #{feature_name}. Please make sure you're using the API key of a subscriber account and that the subscription plan includes #{feature_name}"
)
end
end end

View File

@ -21,6 +21,17 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
end end
describe "POST /api/v1/sites" do describe "POST /api/v1/sites" do
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "can create a site", %{conn: conn} do test "can create a site", %{conn: conn} do
conn = conn =
post(conn, "/api/v1/sites", %{ post(conn, "/api/v1/sites", %{
@ -113,15 +124,19 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
"timezone" => "Europe/Tallinn" "timezone" => "Europe/Tallinn"
}) })
assert json_response(conn, 403) == %{ assert %{"error" => error} = json_response(conn, 402)
"error" => "You can't add sites to the selected team." assert error =~ "API key does not have access to Sites API"
}
end end
test "can create a site under a specific team if permitted", %{conn: conn, user: user} do test "can create a site under a specific team if permitted", %{conn: conn, user: user} do
_site = new_site(owner: user) _site = new_site(owner: user)
owner = new_user() |> subscribe_to_growth_plan() owner =
new_user()
|> subscribe_to_enterprise_plan(
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
team = owner |> team_of() |> Plausible.Teams.complete_setup() team = owner |> team_of() |> Plausible.Teams.complete_setup()
add_member(team, user: user, role: :owner) add_member(team, user: user, role: :owner)
@ -159,7 +174,13 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
test "creates under a particular team when team-scoped key used", %{conn: conn, user: user} do test "creates under a particular team when team-scoped key used", %{conn: conn, user: user} do
personal_team = user |> subscribe_to_business_plan() |> team_of() personal_team = user |> subscribe_to_business_plan() |> team_of()
another_team = new_user() |> subscribe_to_business_plan() |> team_of() another_team =
new_user()
|> subscribe_to_enterprise_plan(
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
|> team_of()
add_member(another_team, user: user, role: :admin) add_member(another_team, user: user, role: :admin)
api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"])
@ -331,21 +352,6 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
}) = response }) = response
end end
test "does not allow creating more sites than the limit", %{conn: conn, user: user} do
for _ <- 1..10, do: new_site(owner: user)
conn =
post(conn, "/api/v1/sites", %{
"domain" => "some-site.domain",
"timezone" => "Europe/Tallinn"
})
assert json_response(conn, 402) == %{
"error" =>
"Your account has reached the limit of 10 sites. To unlock more sites, please upgrade your subscription."
}
end
test "cannot access with a bad API key scope", %{conn: conn, user: user} do test "cannot access with a bad API key scope", %{conn: conn, user: user} do
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
@ -364,6 +370,17 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
describe "DELETE /api/v1/sites/:site_id" do describe "DELETE /api/v1/sites/:site_id" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "delete a site by its domain", %{conn: conn, site: site} do test "delete a site by its domain", %{conn: conn, site: site} do
conn = delete(conn, "/api/v1/sites/" <> site.domain) conn = delete(conn, "/api/v1/sites/" <> site.domain)
@ -393,7 +410,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
add_guest(site, user: user, role: :editor) add_guest(site, user: user, role: :editor)
conn = delete(conn, "/api/v1/sites/" <> site.domain) conn = delete(conn, "/api/v1/sites/" <> site.domain)
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 402)
assert error =~ "API key does not have access to Sites API"
end end
test "cannot delete if team not matching team-scoped API key", %{ test "cannot delete if team not matching team-scoped API key", %{
@ -408,7 +426,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
conn = delete(conn, "/api/v1/sites/" <> site.domain) conn = delete(conn, "/api/v1/sites/" <> site.domain)
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 401)
assert error =~ "Invalid API key"
end end
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
@ -669,8 +688,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
conn = get(conn, "/api/v1/sites/" <> site.domain) conn = get(conn, "/api/v1/sites/" <> site.domain)
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "is 404 when site cannot be found", %{conn: conn} do test "is 404 when site cannot be found", %{conn: conn} do
@ -679,18 +698,31 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert json_response(conn, 404) == %{"error" => "Site could not be found"}
end end
@tag :capture_log
test "is 404 when user is not a member of the site", %{conn: conn} do test "is 404 when user is not a member of the site", %{conn: conn} do
site = insert(:site) site = new_site()
conn = get(conn, "/api/v1/sites/" <> site.domain) conn = get(conn, "/api/v1/sites/" <> site.domain)
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 401)
assert error =~ "Invalid API key"
end end
end end
describe "PUT /api/v1/sites/:site_id" do describe "PUT /api/v1/sites/:site_id" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "can change domain name", %{conn: conn, site: site} do test "can change domain name", %{conn: conn, site: site} do
old_domain = site.domain old_domain = site.domain
assert old_domain != "new.example.com" assert old_domain != "new.example.com"
@ -836,9 +868,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
"domain" => "new.example.com" "domain" => "new.example.com"
}) })
assert json_response(conn, 404) == %{ assert %{"error" => error} = json_response(conn, 401)
"error" => "Site could not be found" assert error =~ "Invalid API key"
}
end end
test "fails when neither 'domain' nor 'tracker_script_configuration' is provided", %{ test "fails when neither 'domain' nor 'tracker_script_configuration' is provided", %{

View File

@ -132,15 +132,20 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
"timezone" => "Europe/Tallinn" "timezone" => "Europe/Tallinn"
}) })
assert json_response(conn, 403) == %{ assert %{"error" => error} = json_response(conn, 402)
"error" => "You can't add sites to the selected team."
} assert error =~ "API key does not have access to Sites API"
end end
test "can create a site under a specific team if permitted", %{conn: conn, user: user} do test "can create a site under a specific team if permitted", %{conn: conn, user: user} do
_site = new_site(owner: user) _site = new_site(owner: user)
owner = new_user() |> subscribe_to_growth_plan() owner =
new_user()
|> subscribe_to_enterprise_plan(
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
team = owner |> team_of() |> Plausible.Teams.complete_setup() team = owner |> team_of() |> Plausible.Teams.complete_setup()
add_member(team, user: user, role: :owner) add_member(team, user: user, role: :owner)
@ -163,7 +168,13 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
test "creates under a particular team when team-scoped key used", %{conn: conn, user: user} do test "creates under a particular team when team-scoped key used", %{conn: conn, user: user} do
personal_team = user |> subscribe_to_business_plan() |> team_of() personal_team = user |> subscribe_to_business_plan() |> team_of()
another_team = new_user() |> subscribe_to_business_plan() |> team_of() another_team =
new_user()
|> subscribe_to_enterprise_plan(
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
|> team_of()
add_member(another_team, user: user, role: :admin) add_member(another_team, user: user, role: :admin)
api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"])
@ -270,6 +281,14 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
describe "DELETE /api/v1/sites/:site_id" do describe "DELETE /api/v1/sites/:site_id" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
:ok
end
test "delete a site by its domain", %{conn: conn, site: site} do test "delete a site by its domain", %{conn: conn, site: site} do
conn = delete(conn, "/api/v1/sites/" <> site.domain) conn = delete(conn, "/api/v1/sites/" <> site.domain)
@ -299,7 +318,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
add_guest(site, user: user, role: :editor) add_guest(site, user: user, role: :editor)
conn = delete(conn, "/api/v1/sites/" <> site.domain) conn = delete(conn, "/api/v1/sites/" <> site.domain)
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 402)
assert error =~ "API key does not have access to Sites API"
end end
test "cannot delete if team not matching team-scoped API key", %{ test "cannot delete if team not matching team-scoped API key", %{
@ -314,7 +334,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
conn = delete(conn, "/api/v1/sites/" <> site.domain) conn = delete(conn, "/api/v1/sites/" <> site.domain)
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 401)
assert error =~ "Invalid API key"
end end
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
@ -335,6 +356,18 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
describe "PUT /api/v1/sites/shared-links" do describe "PUT /api/v1/sites/shared-links" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.SharedLinks,
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "can add a shared link to a site", %{conn: conn, site: site} do test "can add a shared link to a site", %{conn: conn, site: site} do
conn = conn =
put(conn, "/api/v1/sites/shared-links", %{ put(conn, "/api/v1/sites/shared-links", %{
@ -397,8 +430,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
name: "WordPress" name: "WordPress"
}) })
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "returns 400 when site id missing", %{conn: conn} do test "returns 400 when site id missing", %{conn: conn} do
@ -437,8 +470,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
name: "WordPress" name: "WordPress"
}) })
res = json_response(conn, 404) res = json_response(conn, 402)
assert res["error"] == "Site could not be found" assert res["error"] =~ "API key does not have access to Sites API"
end end
test "fails to create without access to SharedLinks feature", %{ test "fails to create without access to SharedLinks feature", %{
@ -478,6 +511,18 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
describe "PUT /api/v1/sites/custom-props" do describe "PUT /api/v1/sites/custom-props" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.Props,
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "can add a custom property to a site", %{conn: conn, site: site} do test "can add a custom property to a site", %{conn: conn, site: site} do
conn = conn =
put(conn, "/api/v1/sites/custom-props", %{ put(conn, "/api/v1/sites/custom-props", %{
@ -573,8 +618,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
property: "prop1" property: "prop1"
}) })
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "returns 400 when site id missing", %{conn: conn} do test "returns 400 when site id missing", %{conn: conn} do
@ -613,8 +658,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
property: "prop1" property: "prop1"
}) })
res = json_response(conn, 404) res = json_response(conn, 402)
assert res["error"] == "Site could not be found" assert res["error"] =~ "API key does not have access to Sites API"
end end
test "returns 400 when property missing", %{conn: conn, site: site} do test "returns 400 when property missing", %{conn: conn, site: site} do
@ -631,6 +676,14 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
describe "PUT /api/v1/sites/goals" do describe "PUT /api/v1/sites/goals" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
:ok
end
test "can add a goal as event to a site", %{conn: conn, site: site} do test "can add a goal as event to a site", %{conn: conn, site: site} do
conn = conn =
put(conn, "/api/v1/sites/goals", %{ put(conn, "/api/v1/sites/goals", %{
@ -747,8 +800,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
event_name: "Signup" event_name: "Signup"
}) })
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "returns 400 when site id missing", %{conn: conn} do test "returns 400 when site id missing", %{conn: conn} do
@ -790,8 +843,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
event_name: "Signup" event_name: "Signup"
}) })
res = json_response(conn, 404) res = json_response(conn, 402)
assert res["error"] == "Site could not be found" assert res["error"] =~ "API key does not have access to Sites API"
end end
test "returns 400 when goal type missing", %{conn: conn, site: site} do test "returns 400 when goal type missing", %{conn: conn, site: site} do
@ -831,6 +884,18 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
describe "DELETE /api/v1/sites/custom-props/:property" do describe "DELETE /api/v1/sites/custom-props/:property" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.Props,
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "deletes a custom property", %{conn: conn, site: site} do test "deletes a custom property", %{conn: conn, site: site} do
conn = conn =
put(conn, "/api/v1/sites/custom-props", %{ put(conn, "/api/v1/sites/custom-props", %{
@ -914,8 +979,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
site_id: site.domain site_id: site.domain
}) })
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "handles non-existent custom prop gracefully", %{conn: conn, site: site} do test "handles non-existent custom prop gracefully", %{conn: conn, site: site} do
@ -940,7 +1005,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
site_id: site.domain site_id: site.domain
}) })
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 402)
assert error =~ "API key does not have access to Sites API"
end end
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
@ -966,6 +1032,17 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
describe "DELETE /api/v1/sites/goals/:goal_id" do describe "DELETE /api/v1/sites/goals/:goal_id" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "delete a goal by its id", %{conn: conn, site: site} do test "delete a goal by its id", %{conn: conn, site: site} do
conn = conn =
put(conn, "/api/v1/sites/goals", %{ put(conn, "/api/v1/sites/goals", %{
@ -1027,8 +1104,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
site_id: site.domain site_id: site.domain
}) })
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "is 404 when goal cannot be found", %{conn: conn, site: site} do test "is 404 when goal cannot be found", %{conn: conn, site: site} do
@ -1053,7 +1130,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
site_id: site.domain site_id: site.domain
}) })
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 402)
assert error =~ "API key does not have access to Sites API"
end end
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
@ -1318,12 +1396,23 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
conn = get(conn, "/api/v1/sites/guests?site_id=#{site.domain}") conn = get(conn, "/api/v1/sites/guests?site_id=#{site.domain}")
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
end end
describe "PUT /api/v1/sites/guests" do describe "PUT /api/v1/sites/guests" do
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SitesAPI
]
)
:ok
end
test "creates new invitation", %{conn: conn, user: user} do test "creates new invitation", %{conn: conn, user: user} do
site = new_site(owner: user) site = new_site(owner: user)
@ -1408,8 +1497,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
"email" => "test@example.com" "email" => "test@example.com"
}) })
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "fails for unknown role", %{conn: conn, user: user} do test "fails for unknown role", %{conn: conn, user: user} do
@ -1431,6 +1520,14 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end end
describe "DELETE /api/v1/sites/guests" do describe "DELETE /api/v1/sites/guests" do
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
:ok
end
test "no-op when nothing to delete", %{conn: conn, user: user} do test "no-op when nothing to delete", %{conn: conn, user: user} do
site = new_site(owner: user) site = new_site(owner: user)
@ -1492,8 +1589,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
conn = delete(conn, "/api/v1/sites/guests/test@example.com?site_id=#{site.domain}") conn = delete(conn, "/api/v1/sites/guests/test@example.com?site_id=#{site.domain}")
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "won't delete non-guest membership", %{conn: conn, user: user} do test "won't delete non-guest membership", %{conn: conn, user: user} do
@ -1565,8 +1662,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
conn = get(conn, "/api/v1/sites/" <> site.domain) conn = get(conn, "/api/v1/sites/" <> site.domain)
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "is 404 when site cannot be found", %{conn: conn} do test "is 404 when site cannot be found", %{conn: conn} do
@ -1575,12 +1672,14 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert json_response(conn, 404) == %{"error" => "Site could not be found"}
end end
test "is 404 when user is not a member of the site", %{conn: conn} do @tag :capture_log
site = insert(:site) test "is 401 when user is not a member of the site", %{conn: conn} do
site = new_site()
conn = get(conn, "/api/v1/sites/" <> site.domain) conn = get(conn, "/api/v1/sites/" <> site.domain)
assert json_response(conn, 404) == %{"error" => "Site could not be found"} assert %{"error" => error} = json_response(conn, 401)
assert error =~ "Invalid API key"
end end
end end
@ -1650,8 +1749,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain) conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain)
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "returns error when `site_id` parameter is missing", %{conn: conn} do test "returns error when `site_id` parameter is missing", %{conn: conn} do
@ -1663,21 +1762,21 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end end
test "returns error when `site_id` parameter is invalid", %{conn: conn} do test "returns error when `site_id` parameter is invalid", %{conn: conn} do
conn = get(conn, "/api/v1/sites/custom-props=does.not.exist") conn = get(conn, "/api/v1/sites/custom-props?site_id=does.not.exist")
assert json_response(conn, 404) == %{ assert json_response(conn, 404) == %{
"error" => "Site could not be found" "error" => "Site could not be found"
} }
end end
@tag :capture_log
test "returns error when user is not a member of the site", %{conn: conn} do test "returns error when user is not a member of the site", %{conn: conn} do
site = insert(:site) site = new_site()
conn = get(conn, "/api/v1/sites/custom-props=" <> site.domain) conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain)
assert json_response(conn, 404) == %{ assert %{"error" => error} = json_response(conn, 401)
"error" => "Site could not be found" assert(error =~ "Invalid API key")
}
end end
end end
@ -1809,8 +1908,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain)
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "returns error when `site_id` parameter is missing", %{conn: conn} do test "returns error when `site_id` parameter is missing", %{conn: conn} do
@ -1829,20 +1928,28 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
} }
end end
@tag :capture_log
test "returns error when user is not a member of the site", %{conn: conn} do test "returns error when user is not a member of the site", %{conn: conn} do
site = insert(:site) site = new_site()
conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain)
assert json_response(conn, 404) == %{ assert %{"error" => error} = json_response(conn, 401)
"error" => "Site could not be found" assert error =~ "Invalid API key"
}
end end
end end
describe "PUT /api/v1/sites/:site_id" do describe "PUT /api/v1/sites/:site_id" do
setup :create_site setup :create_site
setup %{user: user} do
subscribe_to_enterprise_plan(user,
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
)
:ok
end
test "can change domain name", %{conn: conn, site: site} do test "can change domain name", %{conn: conn, site: site} do
old_domain = site.domain old_domain = site.domain
assert old_domain != "new.example.com" assert old_domain != "new.example.com"
@ -1878,8 +1985,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
"domain" => "new.example.com" "domain" => "new.example.com"
}) })
res = json_response(conn, 404) res = json_response(conn, 401)
assert res["error"] == "Site could not be found" assert res["error"] =~ "Invalid API key"
end end
test "can't make a no-op change", %{conn: conn, site: site} do test "can't make a no-op change", %{conn: conn, site: site} do