diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index e815ead78e..7dfb10f30a 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -102,7 +102,7 @@ jobs: - run: make minio if: env.MIX_ENV == 'test' - - run: mix test --include slow --include minio --include migrations --max-failures 1 --warnings-as-errors + - run: mix test --include slow --include minio --include migrations --include kaffy_quirks --max-failures 1 --warnings-as-errors if: env.MIX_ENV == 'test' env: MINIO_HOST_FOR_CLICKHOUSE: "172.17.0.1" diff --git a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex index 4e3a0bb930..a1c0fa0942 100644 --- a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex +++ b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex @@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.ExternalSitesController do @pagination_opts [cursor_fields: [{:id, :desc}], limit: 100, maximum_limit: 1000] def index(conn, params) do - team = Teams.get(params["team_id"]) user = conn.assigns.current_user + team = conn.assigns.current_team || Teams.get(params["team_id"]) page = user @@ -30,9 +30,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do def guests_index(conn, params) do user = conn.assigns.current_user + team = conn.assigns.current_team with {:ok, site_id} <- expect_param_key(params, "site_id"), - {:ok, site} <- get_site(user, site_id, [:owner, :admin, :editor, :viewer]) do + {:ok, site} <- get_site(user, team, site_id, [:owner, :admin, :editor, :viewer]) do opts = [cursor_fields: [inserted_at: :desc, id: :desc], limit: 100, maximum_limit: 1000] page = site |> Sites.list_guests_query() |> paginate(params, opts) @@ -54,10 +55,11 @@ defmodule PlausibleWeb.Api.ExternalSitesController do def teams_index(conn, params) do user = conn.assigns.current_user + team = conn.assigns.current_team page = user - |> Teams.Users.teams_query(order_by: :id_desc) + |> Teams.Users.teams_query(identifier: team && team.identifier, order_by: :id_desc) |> paginate(params, @pagination_opts) json(conn, %{ @@ -78,9 +80,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do def goals_index(conn, params) do user = conn.assigns.current_user + team = conn.assigns.current_team with {:ok, site_id} <- expect_param_key(params, "site_id"), - {:ok, site} <- get_site(user, site_id, [:owner, :admin, :editor, :viewer]) do + {:ok, site} <- get_site(user, team, site_id, [:owner, :admin, :editor, :viewer]) do page = site |> Plausible.Goals.for_site_query() @@ -110,7 +113,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do def create_site(conn, params) do user = conn.assigns.current_user - team = Plausible.Teams.get(params["team_id"]) + team = conn.assigns.current_team || Teams.get(params["team_id"]) case Sites.create(user, params, team) do {:ok, %{site: site}} -> @@ -146,7 +149,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def get_site(conn, %{"site_id" => site_id}) do - case get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor, :viewer]) do + user = conn.assigns.current_user + team = conn.assigns.current_team + + case get_site(user, team, site_id, [:owner, :admin, :editor, :viewer]) do {:ok, site} -> json(conn, %{ domain: site.domain, @@ -160,7 +166,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def delete_site(conn, %{"site_id" => site_id}) do - case get_site(conn.assigns.current_user, site_id, [:owner]) do + user = conn.assigns.current_user + team = conn.assigns.current_team + + case get_site(user, team, site_id, [:owner]) do {:ok, site} -> {:ok, _} = Plausible.Site.Removal.run(site) json(conn, %{"deleted" => true}) @@ -171,8 +180,11 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def update_site(conn, %{"site_id" => site_id} = params) do + user = conn.assigns.current_user + team = conn.assigns.current_team + # for now this only allows to change the domain - with {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]), + with {:ok, site} <- get_site(user, team, site_id, [:owner, :admin, :editor]), {:ok, site} <- Plausible.Site.Domain.change(site, params["domain"]) do json(conn, site) else @@ -187,10 +199,13 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def find_or_create_guest(conn, params) do + user = conn.assigns.current_user + team = conn.assigns.current_team + with {:ok, site_id} <- expect_param_key(params, "site_id"), {:ok, email} <- expect_param_key(params, "email"), {:ok, role} <- expect_param_key(params, "role", ["viewer", "editor"]), - {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin]) do + {:ok, site} <- get_site(user, team, site_id, [:owner, :admin]) do existing = Repo.one(Sites.list_guests_query(site, email: email)) if existing do @@ -230,9 +245,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def delete_guest(conn, params) do + user = conn.assigns.current_user + team = conn.assigns.current_team + with {:ok, site_id} <- expect_param_key(params, "site_id"), {:ok, email} <- expect_param_key(params, "email"), - {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin]) do + {:ok, site} <- get_site(user, team, site_id, [:owner, :admin]) do existing = Repo.one(Sites.list_guests_query(site, email: email)) case existing do @@ -262,9 +280,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def find_or_create_shared_link(conn, params) do + user = conn.assigns.current_user + team = conn.assigns.current_team + with {:ok, site_id} <- expect_param_key(params, "site_id"), {:ok, link_name} <- expect_param_key(params, "name"), - {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]) do + {:ok, site} <- get_site(user, team, site_id, [:owner, :admin, :editor]) do shared_link = Repo.get_by(Plausible.Site.SharedLink, site_id: site.id, name: link_name) shared_link = @@ -296,9 +317,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def find_or_create_goal(conn, params) do + user = conn.assigns.current_user + team = conn.assigns.current_team + with {:ok, site_id} <- expect_param_key(params, "site_id"), {:ok, _} <- expect_param_key(params, "goal_type"), - {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]), + {:ok, site} <- get_site(user, team, site_id, [:owner, :admin, :editor]), {:ok, goal} <- Goals.find_or_create(site, params) do json(conn, goal) else @@ -314,9 +338,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end def delete_goal(conn, params) do + user = conn.assigns.current_user + team = conn.assigns.current_team + with {:ok, site_id} <- expect_param_key(params, "site_id"), {:ok, goal_id} <- expect_param_key(params, "goal_id"), - {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]), + {:ok, site} <- get_site(user, team, site_id, [:owner, :admin, :editor]), :ok <- Goals.delete(goal_id, site) do json(conn, %{"deleted" => true}) else @@ -345,10 +372,19 @@ defmodule PlausibleWeb.Api.ExternalSitesController do } end - defp get_site(user, site_id, roles) do + defp get_site(user, team, site_id, roles) do case Plausible.Sites.get_for_user(user, site_id, roles) do - nil -> {:error, :site_not_found} - site -> {:ok, site} + nil -> + {:error, :site_not_found} + + site -> + site = Repo.preload(site, :team) + + if team && team.id != site.team_id do + {:error, :site_not_found} + else + {:ok, site} + end end end diff --git a/lib/plausible/auth/api_key.ex b/lib/plausible/auth/api_key.ex index b6834d32fb..8b40f81470 100644 --- a/lib/plausible/auth/api_key.ex +++ b/lib/plausible/auth/api_key.ex @@ -18,6 +18,7 @@ defmodule Plausible.Auth.ApiKey do field :key_hash, :string field :key_prefix, :string + belongs_to :team, Plausible.Teams.Team belongs_to :user, Plausible.Auth.User timestamps() @@ -25,17 +26,24 @@ defmodule Plausible.Auth.ApiKey do def hourly_request_limit(), do: @hourly_request_limit - def changeset(schema, attrs \\ %{}) do - schema + def changeset(struct, team, attrs) do + struct |> cast(attrs, @required ++ @optional) |> validate_required(@required) |> maybe_put_key() |> process_key() + |> maybe_put_team(team) |> unique_constraint(:key_hash, error_key: :key) + |> unique_constraint([:team_id, :user_id], error_key: :team) end - def update(schema, attrs \\ %{}) do - schema + # NOTE: needed only because of lacking introspection in Kaffy + def changeset(struct, attrs) do + changeset(struct, nil, attrs) + end + + def update(struct, attrs \\ %{}) do + struct |> cast(attrs, [:name, :user_id, :scopes]) |> validate_required([:user_id, :name]) end @@ -57,6 +65,12 @@ defmodule Plausible.Auth.ApiKey do def process_key(changeset), do: changeset + defp maybe_put_team(changeset, nil), do: changeset + + defp maybe_put_team(changeset, team) do + put_assoc(changeset, :team, team) + end + defp maybe_put_key(changeset) do if get_change(changeset, :key) do changeset diff --git a/lib/plausible/auth/api_key_admin.ex b/lib/plausible/auth/api_key_admin.ex index d87d7ffdc3..09b6bb4a44 100644 --- a/lib/plausible/auth/api_key_admin.ex +++ b/lib/plausible/auth/api_key_admin.ex @@ -1,35 +1,76 @@ defmodule Plausible.Auth.ApiKeyAdmin do + @moduledoc """ + Stats and Sites API key logic for CRM. + """ use Plausible.Repo + alias Plausible.Auth + alias Plausible.Teams + def search_fields(_schema) do [ :name, - user: [:name, :email] + user: [:name, :email], + team: [:name, :identifier] ] end def custom_index_query(_conn, _schema, query) do - from(r in query, preload: [:user]) + from(r in query, preload: [:user, team: :owners]) end def create_changeset(schema, attrs) do - scopes = [attrs["scope"]] - Plausible.Auth.ApiKey.changeset(struct(schema, %{}), Map.merge(%{"scopes" => scopes}, attrs)) + team = Teams.get(attrs["team_identifier"]) + + user_id = + case Integer.parse(Map.get(attrs, "user_id", "")) do + {user_id, ""} -> user_id + _ -> nil + end + + user = user_id && Auth.find_user_by(id: user_id) + + team = + case {team, user} do + {%{} = team, _} -> + team + + {nil, %{} = user} -> + {:ok, team} = Teams.get_or_create(user) + + team + + _ -> + nil + end + + Auth.ApiKey.changeset(schema, team, attrs) end - def update_changeset(schema, attrs) do - Plausible.Auth.ApiKey.update(schema, attrs) + def update_changeset(entry, attrs) do + Auth.ApiKey.update(entry, attrs) end @plaintext_key_help """ The value of the API key is sensitive data like a password. Once created, the value of they will never be revealed again. Make sure to copy/paste this into a secure place before hitting 'save'. When sending the key to a customer, use a secure E2EE system that destructs the message after a certain period like https://bitwarden.com/products/send """ + + @team_identifier_help """ + Team under which the key is to be created. Defaults to user's personal team when left empty. + """ + def form_fields(_) do [ name: nil, key: %{create: :readonly, update: :hidden, help_text: @plaintext_key_help}, key_prefix: %{create: :hidden, update: :readonly}, - scope: %{choices: [{"Stats API", ["stats:read:*"]}, {"Sites API", ["sites:provision:*"]}]}, + scopes: %{ + choices: [ + {"Stats API", Jason.encode!(["stats:read:*"])}, + {"Sites API", Jason.encode!(["sites:provision:*"])} + ] + }, + team_identifier: %{update: :hidden, help_text: @team_identifier_help}, user_id: nil ] end @@ -39,7 +80,55 @@ defmodule Plausible.Auth.ApiKeyAdmin do key_prefix: nil, name: nil, scopes: nil, - owner: %{value: & &1.user.email} + owner: %{value: &get_owner/1}, + team: %{value: &get_team/1} ] end + + defp get_team(api_key) do + team_name = + case api_key.team && api_key.team.owners do + [owner] -> + if api_key.team.setup_complete do + api_key.team.name + else + owner.name + end + + [_ | _] -> + api_key.team.name + + nil -> + "(none)" + end + |> html_escape() + + if api_key.team do + Phoenix.HTML.raw(""" + #{team_name} + """) + else + team_name + end + end + + defp get_owner(api_key) do + escaped_name = html_escape(api_key.user.name) + escaped_email = html_escape(api_key.user.email) + + owner_html = + """ + #{escaped_name} +
+ #{escaped_email} + """ + + {:safe, owner_html} + end + + defp html_escape(string) do + string + |> Phoenix.HTML.html_escape() + |> Phoenix.HTML.safe_to_string() + end end diff --git a/lib/plausible/auth/auth.ex b/lib/plausible/auth/auth.ex index 5771261bab..143d1f6b9e 100644 --- a/lib/plausible/auth/auth.ex +++ b/lib/plausible/auth/auth.ex @@ -5,7 +5,11 @@ defmodule Plausible.Auth do use Plausible use Plausible.Repo + + import Ecto.Query + alias Plausible.Auth + alias Plausible.Billing alias Plausible.RateLimit alias Plausible.Teams @@ -154,13 +158,25 @@ defmodule Plausible.Auth do def is_super_admin?(_), do: false end - @spec create_api_key(Auth.User.t(), String.t(), String.t()) :: - {:ok, Auth.ApiKey.t()} | {:error, Ecto.Changeset.t() | :upgrade_required} - def create_api_key(user, name, key) do - params = %{name: name, user_id: user.id, key: key} - changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, params) + @spec list_api_keys(Auth.User.t(), Teams.Team.t() | nil) :: [Auth.ApiKey.t()] + def list_api_keys(user, team) do + query = + from(a in Auth.ApiKey, + where: a.user_id == ^user.id, + order_by: [desc: a.id] + ) + |> scope_api_keys_by_team(team) - with :ok <- check_stats_api_available(user) do + Repo.all(query) + end + + @spec create_api_key(Auth.User.t(), Teams.Team.t(), String.t(), String.t()) :: + {:ok, Auth.ApiKey.t()} | {:error, Ecto.Changeset.t() | :upgrade_required} + def create_api_key(user, team, name, key) do + params = %{name: name, user_id: user.id, key: key} + changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, team, params) + + with :ok <- Billing.Feature.StatsAPI.check_availability(team) do Repo.insert(changeset) end end @@ -176,7 +192,7 @@ defmodule Plausible.Auth do end @spec find_api_key(String.t(), Keyword.t()) :: - {:ok, Auth.ApiKey.t() | %{api_key: Auth.ApiKey.t(), team: Teams.Team.t() | nil}} + {:ok, %{api_key: Auth.ApiKey.t(), team: Teams.Team.t() | nil}} | {:error, :invalid_api_key | :missing_site_id} def find_api_key(raw_key, opts \\ []) do {team_scope, id} = Keyword.get(opts, :team_by, {nil, nil}) @@ -184,33 +200,52 @@ defmodule Plausible.Auth do find_api_key(raw_key, team_scope, id) end + defp scope_api_keys_by_team(query, nil) do + query + end + + defp scope_api_keys_by_team(query, team) do + where(query, [a], is_nil(a.team_id) or a.team_id == ^team.id) + end + defp find_api_key(raw_key, nil, _) do hashed_key = Auth.ApiKey.do_hash(raw_key) query = from(api_key in Auth.ApiKey, join: user in assoc(api_key, :user), + left_join: team in assoc(api_key, :team), where: api_key.key_hash == ^hashed_key, - preload: [user: user] + preload: [user: user, team: team] ) - if found = Repo.one(query) do - {:ok, found} - else - {:error, :invalid_api_key} + case Repo.one(query) do + nil -> + {:error, :invalid_api_key} + + %{team: %{} = team} = api_key -> + {:ok, %{api_key: api_key, team: team}} + + api_key -> + {:ok, %{api_key: api_key, team: nil}} end end defp find_api_key(raw_key, :site, nil) do - with {:ok, api_key} <- find_api_key(raw_key, nil, nil) do - {:ok, %{api_key: api_key, team: nil}} - end + find_api_key(raw_key, nil, nil) end defp find_api_key(raw_key, :site, domain) do - with {:ok, api_key} <- find_api_key(raw_key, nil, nil) do - team = find_team_by_site(domain) - {:ok, %{api_key: api_key, team: team}} + case find_api_key(raw_key, nil, nil) do + {:ok, %{api_key: api_key, team: nil}} -> + team = find_team_by_site(domain) + {:ok, %{api_key: api_key, team: team}} + + {:ok, %{api_key: api_key, team: team}} -> + {:ok, %{api_key: api_key, team: team}} + + {:error, _} = error -> + error end end @@ -223,21 +258,6 @@ defmodule Plausible.Auth do ) end - defp check_stats_api_available(user) do - case Plausible.Teams.get_by_owner(user) do - {:ok, team} -> - Plausible.Billing.Feature.StatsAPI.check_availability(team) - - {:error, :no_team} -> - Plausible.Billing.Feature.StatsAPI.check_availability(nil) - - {:error, :multiple_teams} -> - # NOTE: Loophole to allow creating API keys when user is a member - # on multiple teams. - :ok - end - end - defp rate_limit_key(%Auth.User{id: id}), do: id defp rate_limit_key(%Plug.Conn{} = conn), do: PlausibleWeb.RemoteIP.get(conn) end diff --git a/lib/plausible/teams.ex b/lib/plausible/teams.ex index 07e26b1d55..9cf60f3a80 100644 --- a/lib/plausible/teams.ex +++ b/lib/plausible/teams.ex @@ -42,7 +42,13 @@ defmodule Plausible.Teams do end def get(team_identifier) when is_binary(team_identifier) do - Repo.get_by(Teams.Team, identifier: team_identifier) + case Ecto.UUID.cast(team_identifier) do + {:ok, uuid} -> + Repo.get_by(Teams.Team, identifier: uuid) + + :error -> + nil + end end @spec get!(pos_integer() | binary()) :: Teams.Team.t() @@ -291,12 +297,14 @@ defmodule Plausible.Teams do end end + @spec last_subscription_join_query() :: Ecto.Query.t() def last_subscription_join_query() do from(subscription in last_subscription_query(), where: subscription.team_id == parent_as(:team).id ) end + @spec last_subscription_query() :: Ecto.Query.t() def last_subscription_query() do from(subscription in Plausible.Billing.Subscription, order_by: [desc: subscription.inserted_at, desc: subscription.id], @@ -304,7 +312,9 @@ defmodule Plausible.Teams do ) end - defp get_owned_team(user, opts \\ []) do + # Exposed for use in tests + @doc false + def get_owned_team(user, opts \\ []) do only_not_setup? = Keyword.get(opts, :only_not_setup?, false) query = diff --git a/lib/plausible_web/controllers/settings_controller.ex b/lib/plausible_web/controllers/settings_controller.ex index 1a6f80896a..e7ef51d4ee 100644 --- a/lib/plausible_web/controllers/settings_controller.ex +++ b/lib/plausible_web/controllers/settings_controller.ex @@ -92,21 +92,26 @@ defmodule PlausibleWeb.SettingsController do def api_keys(conn, _params) do current_user = conn.assigns.current_user + current_team = conn.assigns[:current_team] - api_keys = - Repo.preload(current_user, :api_keys).api_keys + api_keys = Auth.list_api_keys(current_user, current_team) render(conn, :api_keys, layout: {PlausibleWeb.LayoutView, :settings}, api_keys: api_keys) end def new_api_key(conn, _params) do - changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}) + current_team = conn.assigns[:current_team] + + changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, current_team, %{}) render(conn, "new_api_key.html", changeset: changeset) end def create_api_key(conn, %{"api_key" => %{"name" => name, "key" => key}}) do - case Auth.create_api_key(conn.assigns.current_user, name, key) do + current_user = conn.assigns.current_user + current_team = conn.assigns.current_team + + case Auth.create_api_key(current_user, current_team, name, key) do {:ok, _api_key} -> conn |> put_flash(:success, "API key created successfully") diff --git a/lib/plausible_web/plugs/authorize_public_api.ex b/lib/plausible_web/plugs/authorize_public_api.ex index 4a7788d92b..1d5b4f9e15 100644 --- a/lib/plausible_web/plugs/authorize_public_api.ex +++ b/lib/plausible_web/plugs/authorize_public_api.ex @@ -55,7 +55,9 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do {:ok, api_key, limit_key, hourly_limit} <- find_api_key(conn, token, context), :ok <- check_api_key_rate_limit(limit_key, hourly_limit), {:ok, conn} <- verify_by_scope(conn, api_key, requested_scope) do - assign(conn, :current_user, api_key.user) + conn + |> assign(:current_user, api_key.user) + |> assign(:current_team, api_key.team) else error -> send_error(conn, requested_scope, error) end @@ -97,8 +99,15 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do end defp find_api_key(_conn, token, _) do - with {:ok, api_key} <- Auth.find_api_key(token) do - {:ok, api_key, limit_key(api_key, nil), Auth.ApiKey.hourly_request_limit()} + case Auth.find_api_key(token) do + {:ok, %{api_key: api_key, team: nil}} -> + {:ok, api_key, limit_key(api_key, nil), Auth.ApiKey.hourly_request_limit()} + + {:ok, %{api_key: api_key, team: team}} -> + {:ok, api_key, limit_key(api_key, team.identifier), team.hourly_api_request_limit} + + {:error, _} = error -> + error end end @@ -189,6 +198,9 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do is_super_admin? -> :ok + api_key.team_id && api_key.team_id != site.team_id -> + {:error, :invalid_api_key} + Sites.locked?(site) -> {:error, :site_locked} diff --git a/lib/plausible_web/views/layout_view.ex b/lib/plausible_web/views/layout_view.ex index 67f33ee1db..4d8730ad36 100644 --- a/lib/plausible_web/views/layout_view.ex +++ b/lib/plausible_web/views/layout_view.ex @@ -111,7 +111,9 @@ defmodule PlausibleWeb.LayoutView do if(not Teams.setup?(current_team), do: %{key: "Invoices", value: "billing/invoices", icon: :banknotes} ), - %{key: "API Keys", value: "api-keys", icon: :key}, + if(not Teams.setup?(current_team), + do: %{key: "API Keys", value: "api-keys", icon: :key} + ), %{key: "Danger Zone", value: "danger-zone", icon: :exclamation_triangle} ] |> Enum.reject(&is_nil/1) @@ -127,6 +129,7 @@ defmodule PlausibleWeb.LayoutView do if(current_team_role in [:owner, :admin, :billing], do: %{key: "Invoices", value: "billing/invoices", icon: :banknotes} ), + %{key: "API Keys", value: "api-keys", icon: :key}, if(current_team_role == :owner, do: %{key: "Danger Zone", value: "team/delete", icon: :exclamation_triangle} ) diff --git a/test/plausible/auth/auth_test.exs b/test/plausible/auth/auth_test.exs index 3d72c134a6..f4834afc5c 100644 --- a/test/plausible/auth/auth_test.exs +++ b/test/plausible/auth/auth_test.exs @@ -21,16 +21,21 @@ defmodule Plausible.AuthTest do describe "create_api_key/3" do test "creates a new api key" do user = new_user(trial_expiry_date: Date.utc_today()) + team = team_of(user) key = Ecto.UUID.generate() - assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(user, "my new key", key) + assert {:ok, %Auth.ApiKey{} = api_key} = Auth.create_api_key(user, team, "my new key", key) + assert api_key.team_id == team.id + assert api_key.user_id == user.id end test "errors when key already exists" do u1 = new_user(trial_expiry_date: Date.utc_today()) + t1 = team_of(u1) u2 = new_user(trial_expiry_date: Date.utc_today()) + t2 = team_of(u2) key = Ecto.UUID.generate() - assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(u1, "my new key", key) - assert {:error, changeset} = Auth.create_api_key(u2, "my other key", key) + assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(u1, t1, "my new key", key) + assert {:error, changeset} = Auth.create_api_key(u2, t2, "my other key", key) assert changeset.errors[:key] == {"has already been taken", @@ -38,27 +43,30 @@ defmodule Plausible.AuthTest do end @tag :ee_only - test "returns error when user is on a growth plan" do + test "returns error when team is on a growth plan" do user = new_user() |> subscribe_to_growth_plan() + team = team_of(user) assert {:error, :upgrade_required} = - Auth.create_api_key(user, "my new key", Ecto.UUID.generate()) + Auth.create_api_key(user, team, "my new key", Ecto.UUID.generate()) end - test "creates a key for user on a growth plan when they are an owner of more than one team" do - user = new_user() |> subscribe_to_growth_plan() + test "creates a key for user in a team with a bunsiness plan" do + user = new_user() |> subscribe_to_business_plan() + team = team_of(user) another_site = new_site() add_member(another_site.team, user: user, role: :owner) assert {:ok, %Auth.ApiKey{}} = - Auth.create_api_key(user, "my new key", Ecto.UUID.generate()) + Auth.create_api_key(user, team, "my new key", Ecto.UUID.generate()) end end describe "delete_api_key/2" do test "deletes the record" do user = new_user(trial_expiry_date: Date.utc_today()) - assert {:ok, api_key} = Auth.create_api_key(user, "my new key", Ecto.UUID.generate()) + team = team_of(user) + assert {:ok, api_key} = Auth.create_api_key(user, team, "my new key", Ecto.UUID.generate()) assert :ok = Auth.delete_api_key(user, api_key.id) refute Plausible.Repo.reload(api_key) end @@ -67,7 +75,10 @@ defmodule Plausible.AuthTest do me = new_user(trial_expiry_date: Date.utc_today()) other_user = new_user(trial_expiry_date: Date.utc_today()) - {:ok, other_api_key} = Auth.create_api_key(other_user, "my new key", Ecto.UUID.generate()) + other_team = team_of(other_user) + + {:ok, other_api_key} = + Auth.create_api_key(other_user, other_team, "my new key", Ecto.UUID.generate()) assert {:error, :not_found} = Auth.delete_api_key(me, other_api_key.id) assert {:error, :not_found} = Auth.delete_api_key(me, -1) diff --git a/test/plausible_web/controllers/admin_controller_test.exs b/test/plausible_web/controllers/admin_controller_test.exs index 127e2498de..2e16f5955e 100644 --- a/test/plausible_web/controllers/admin_controller_test.exs +++ b/test/plausible_web/controllers/admin_controller_test.exs @@ -3,6 +3,7 @@ defmodule PlausibleWeb.AdminControllerTest do use Plausible.Teams.Test alias Plausible.Repo + alias Plausible.Teams describe "GET /crm/teams/team/:team_id/usage" do setup [:create_user, :log_in, :create_team] @@ -269,4 +270,158 @@ defmodule PlausibleWeb.AdminControllerTest do } end end + + describe "POST /crm/auth/api_key" do + setup [:create_user, :log_in] + + @tag :kaffy_quirks + test "creates a team-bound API key", %{conn: conn, user: user} do + patch_env(:super_admin_user_ids, [user.id]) + + another_user = new_user() + team = another_user |> subscribe_to_business_plan() |> team_of() + + params = %{ + "api_key" => %{ + "name" => "Some key", + "key" => Ecto.UUID.generate(), + "scope" => "stats:read:*", + "user_id" => "#{another_user.id}" + } + } + + conn = post(conn, "/crm/auth/api_key", params) + + assert api_key = Repo.get_by(Plausible.Auth.ApiKey, user_id: another_user.id) + + assert redirected_to(conn, 302) == "/crm/auth/api_key" + + assert api_key.team_id == team.id + assert api_key.user_id == another_user.id + end + + @tag :kaffy_quirks + test "Creates personal team when creating the api key if there's none", %{ + conn: conn, + user: user + } do + patch_env(:super_admin_user_ids, [user.id]) + + another_user = new_user() + + another_team = new_site().team |> Teams.complete_setup() + add_member(another_team, user: another_user, role: :owner) + + params = %{ + "api_key" => %{ + "name" => "Some key", + "key" => Ecto.UUID.generate(), + "scopes" => Jason.encode!(["stats:read:*"]), + "user_id" => "#{another_user.id}" + } + } + + conn = post(conn, "/crm/auth/api_key", params) + + assert api_key = Repo.get_by(Plausible.Auth.ApiKey, user_id: another_user.id) + + assert {:ok, personal_team} = Teams.get_owned_team(another_user, only_not_setup?: true) + + assert redirected_to(conn, 302) == "/crm/auth/api_key" + + assert api_key.team_id == personal_team.id + end + + @tag :kaffy_quirks + test "Creates team for a particular team if provided", %{conn: conn, user: user} do + patch_env(:super_admin_user_ids, [user.id]) + + another_user = new_user() |> subscribe_to_business_plan() + + another_team = new_site().team |> Teams.complete_setup() + add_member(another_team, user: another_user, role: :owner) + + params = %{ + "api_key" => %{ + "team_identifier" => another_team.identifier, + "name" => "Some key", + "key" => Ecto.UUID.generate(), + "scopes" => Jason.encode!(["stats:read:*"]), + "user_id" => "#{another_user.id}" + } + } + + conn = post(conn, "/crm/auth/api_key", params) + + assert api_key = Repo.get_by(Plausible.Auth.ApiKey, user_id: another_user.id) + + assert redirected_to(conn, 302) == "/crm/auth/api_key" + + assert api_key.team_id == another_team.id + end + end + + describe "PUT /crm/auth/api_key/:id" do + setup [:create_user, :log_in] + + @tag :ee_only + test "updates an API key", %{conn: conn, user: user} do + patch_env(:super_admin_user_ids, [user.id]) + + another_user = new_user() + team = another_user |> subscribe_to_business_plan() |> team_of() + + api_key = insert(:api_key, user: user, team: team) + + assert api_key.scopes == ["stats:read:*"] + + params = %{ + "api_key" => %{ + "name" => "Some key", + "key" => Ecto.UUID.generate(), + "scopes" => Jason.encode!(["sites:provision:*"]), + "user_id" => "#{another_user.id}" + } + } + + conn = put(conn, "/crm/auth/api_key/#{api_key.id}", params) + + assert api_key = Repo.get_by(Plausible.Auth.ApiKey, user_id: another_user.id) + + assert redirected_to(conn, 302) == "/crm/auth/api_key" + + assert api_key.team_id == team.id + assert api_key.scopes == ["sites:provision:*"] + end + + @tag :ee_only + test "leaves legacy API key without a team on update", %{conn: conn, user: user} do + patch_env(:super_admin_user_ids, [user.id]) + + another_user = new_user() + _team = another_user |> subscribe_to_business_plan() |> team_of() + + api_key = insert(:api_key, user: user) + + assert api_key.scopes == ["stats:read:*"] + + params = %{ + "api_key" => %{ + "name" => "Some key", + "key" => Ecto.UUID.generate(), + "scopes" => Jason.encode!(["sites:provision:*"]), + "user_id" => "#{another_user.id}" + } + } + + conn = put(conn, "/crm/auth/api_key/#{api_key.id}", params) + + assert api_key = Repo.get_by(Plausible.Auth.ApiKey, user_id: another_user.id) + + assert redirected_to(conn, 302) == "/crm/auth/api_key" + + refute api_key.team_id + assert api_key.scopes == ["sites:provision:*"] + end + end end diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs index 61fe3f1227..37526ad688 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_test.exs @@ -74,6 +74,35 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do } } end + + test "shows only one team for team scoped key", %{conn: conn, user: user} do + user |> subscribe_to_business_plan() + + personal_team = team_of(user) + + another_team = new_site().team |> Plausible.Teams.complete_setup() + add_member(another_team, user: user, role: :admin) + + api_key = insert(:api_key, user: user, team: personal_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = get(conn, "/api/v1/sites/teams") + + assert json_response(conn, 200) == %{ + "teams" => [ + %{ + "id" => personal_team.identifier, + "name" => "My Personal Sites", + "api_available" => true + } + ], + "meta" => %{ + "before" => nil, + "after" => nil, + "limit" => 100 + } + } + end end describe "POST /api/v1/sites" do @@ -129,6 +158,31 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert Repo.get_by(Plausible.Site, domain: "some-site.domain").team_id == team.id end + 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() + + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = + post(conn, "/api/v1/sites", %{ + # is ignored + "team_id" => personal_team.identifier, + "domain" => "some-site.domain", + "timezone" => "Europe/Tallinn" + }) + + assert json_response(conn, 200) == %{ + "domain" => "some-site.domain", + "timezone" => "Europe/Tallinn" + } + + assert Repo.get_by(Plausible.Site, domain: "some-site.domain").team_id == another_team.id + end + test "timezone is validated", %{conn: conn} do conn = post(conn, "/api/v1/sites", %{ @@ -244,6 +298,21 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert json_response(conn, 404) == %{"error" => "Site could not be found"} end + test "cannot delete if team not matching team-scoped API key", %{ + conn: conn, + user: user, + site: site + } do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = delete(conn, "/api/v1/sites/" <> site.domain) + + assert json_response(conn, 404) == %{"error" => "Site could not be found"} + end + test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) @@ -312,6 +381,22 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert %{"url" => ^url} = json_response(conn, 200) end + test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = + put(conn, "/api/v1/sites/shared-links", %{ + site_id: site.domain, + name: "WordPress" + }) + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "returns 400 when site id missing", %{conn: conn} do conn = put(conn, "/api/v1/sites/shared-links", %{ @@ -459,6 +544,23 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert %{"id" => ^goal_id} = json_response(conn, 200) end + test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = + put(conn, "/api/v1/sites/goals", %{ + site_id: site.domain, + goal_type: "event", + event_name: "Signup" + }) + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "returns 400 when site id missing", %{conn: conn} do conn = put(conn, "/api/v1/sites/goals", %{ @@ -580,6 +682,30 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert json_response(conn, 200) == %{"deleted" => true} end + test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do + conn1 = + put(conn, "/api/v1/sites/goals", %{ + site_id: site.domain, + goal_type: "event", + event_name: "Signup" + }) + + %{"id" => goal_id} = json_response(conn1, 200) + + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = + delete(conn, "/api/v1/sites/goals/#{goal_id}", %{ + site_id: site.domain + }) + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "is 404 when goal cannot be found", %{conn: conn, site: site} do conn = delete(conn, "/api/v1/sites/goals/0", %{ @@ -690,6 +816,32 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do } = json_response(conn, 200) end + test "implicitly scopes to a team for a team-scoped key", %{ + conn: conn, + user: user + } do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + site = new_site(team: another_team) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + _owned_site = new_site(owner: user) + other_site = new_site() + add_guest(other_site, user: user, role: :viewer) + other_team_site = new_site() + add_member(other_team_site.team, user: user, role: :viewer) + + # `team_id` paramaeter is ignored + conn = get(conn, "/api/v1/sites?team_id=" <> other_team_site.team.identifier) + + assert_matches %{ + "sites" => [ + %{"domain" => ^site.domain} + ] + } = json_response(conn, 200) + end + test "handles pagination correctly", %{conn: conn, user: user} do [ %{domain: site1_domain}, @@ -827,6 +979,22 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert is_binary(before_cursor) end + + test "fails when team does not match team-scoped key", %{conn: conn, user: user} do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + site = new_site(owner: user) + + _guest = add_guest(site, site: site, role: :editor) + + conn = get(conn, "/api/v1/sites/guests?site_id=#{site.domain}") + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end end describe "PUT /api/v1/sites/guests" do @@ -900,6 +1068,24 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert_no_emails_delivered() end + test "fails when team does not match team-scoped key", %{conn: conn, user: user} do + site = new_site(owner: user) + + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = + put(conn, "/api/v1/sites/guests?site_id=#{site.domain}", %{ + "role" => "viewer", + "email" => "test@example.com" + }) + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "fails for unknown role", %{conn: conn, user: user} do site = new_site(owner: user) @@ -970,6 +1156,20 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert json_response(conn2, 200) == %{"deleted" => true} end + test "fails when team does not match team-scoped key", %{conn: conn, user: user} do + site = new_site(owner: user) + + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = delete(conn, "/api/v1/sites/guests/test@example.com?site_id=#{site.domain}") + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "won't delete non-guest membership", %{conn: conn, user: user} do site = new_site(owner: user) @@ -1031,6 +1231,18 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do } end + test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + conn = get(conn, "/api/v1/sites/" <> site.domain) + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "is 404 when site cannot be found", %{conn: conn} do conn = get(conn, "/api/v1/sites/foobar.baz") @@ -1164,6 +1376,20 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert %{"goals" => [%{"id" => ^goal_id}]} = json_response(conn, 200) end + test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + _goal = insert(:goal, %{site: site, page_path: "/login"}) + + conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "returns error when `site_id` parameter is missing", %{conn: conn} do conn = get(conn, "/api/v1/sites/goals") @@ -1214,6 +1440,24 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert site.domain_changed_from == old_domain end + test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do + another_team = new_user() |> subscribe_to_business_plan() |> team_of() + add_member(another_team, user: user, role: :admin) + api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) + conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") + + old_domain = site.domain + assert old_domain != "new.example.com" + + conn = + put(conn, "/api/v1/sites/#{old_domain}", %{ + "domain" => "new.example.com" + }) + + res = json_response(conn, 404) + assert res["error"] == "Site could not be found" + end + test "can't make a no-op change", %{conn: conn, site: site} do conn = put(conn, "/api/v1/sites/#{site.domain}", %{ diff --git a/test/plausible_web/controllers/settings_controller_test.exs b/test/plausible_web/controllers/settings_controller_test.exs index 6aac420b09..9eb1fef9eb 100644 --- a/test/plausible_web/controllers/settings_controller_test.exs +++ b/test/plausible_web/controllers/settings_controller_test.exs @@ -1030,6 +1030,8 @@ defmodule PlausibleWeb.SettingsControllerTest do test "can create an API key", %{conn: conn, user: user} do new_site(owner: user) + team = team_of(user) + conn = post(conn, Routes.settings_path(conn, :api_keys), %{ "api_key" => %{ @@ -1042,6 +1044,31 @@ defmodule PlausibleWeb.SettingsControllerTest do key = Plausible.Auth.ApiKey |> where(user_id: ^user.id) |> Repo.one() assert conn.status == 302 assert key.name == "all your code are belong to us" + assert key.team_id == team.id + end + + test "can create an API key when switched to another team", %{conn: conn, user: user} do + new_site(owner: user) + + team = new_site().team |> Plausible.Teams.complete_setup() + + add_member(team, user: user, role: :editor) + + conn = set_current_team(conn, team) + + conn = + post(conn, Routes.settings_path(conn, :api_keys), %{ + "api_key" => %{ + "user_id" => user.id, + "name" => "all your code are belong to us", + "key" => "swordfish" + } + }) + + key = Plausible.Auth.ApiKey |> where(user_id: ^user.id) |> Repo.one() + assert conn.status == 302 + assert key.name == "all your code are belong to us" + assert key.team_id == team.id end test "cannot create a duplicate API key", %{conn: conn, user: user} do @@ -1095,10 +1122,11 @@ defmodule PlausibleWeb.SettingsControllerTest do test "can't delete api key that doesn't belong to me", %{conn: conn} do other_user = new_user() new_site(owner: other_user) + team = team_of(other_user) assert {:ok, %ApiKey{} = api_key} = %ApiKey{user_id: other_user.id} - |> ApiKey.changeset(%{"name" => "other user's key"}) + |> ApiKey.changeset(team, %{"name" => "other user's key"}) |> Repo.insert() conn = delete(conn, Routes.settings_path(conn, :delete_api_key, api_key.id)) diff --git a/test/plausible_web/plugs/authorize_public_api_test.exs b/test/plausible_web/plugs/authorize_public_api_test.exs index ec6cb8c9a0..aaf41853fb 100644 --- a/test/plausible_web/plugs/authorize_public_api_test.exs +++ b/test/plausible_web/plugs/authorize_public_api_test.exs @@ -287,6 +287,47 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPITest do assert conn.assigns.site.id == site.id end + test "passes for team-bound API key when team matches", %{conn: conn} do + user = new_user() + _site = new_site(owner: user) + another_site = new_site() + another_team = another_site.team |> Plausible.Teams.complete_setup() + add_member(another_team, user: user, role: :editor) + api_key = insert(:api_key, user: user, team: another_team) + + conn = + conn + |> put_req_header("authorization", "Bearer #{api_key.key}") + |> get("/", %{"site_id" => another_site.domain}) + |> assign(:api_scope, "stats:read:*") + |> AuthorizePublicAPI.call(nil) + + refute conn.halted + assert conn.assigns.current_team.id == another_team.id + assert conn.assigns.current_user.id == user.id + assert conn.assigns.site.id == another_site.id + end + + test "halts for team-bound API key when team does not match", %{conn: conn} do + user = new_user() + site = new_site(owner: user) + another_site = new_site() + another_team = another_site.team |> Plausible.Teams.complete_setup() + add_member(another_team, user: user, role: :editor) + api_key = insert(:api_key, user: user, team: another_team) + + conn = + conn + |> put_req_header("authorization", "Bearer #{api_key.key}") + |> get("/", %{"site_id" => site.domain}) + |> assign(:api_scope, "stats:read:*") + |> AuthorizePublicAPI.call(nil) + + assert conn.halted + + assert json_response(conn, 401)["error"] =~ "Invalid API key or site ID." + end + @tag :ee_only test "passes for super admin user even if not a member of the requested site", %{conn: conn} do user = new_user() diff --git a/test/support/test_utils.ex b/test/support/test_utils.ex index fc00a9c2e6..04104242e0 100644 --- a/test/support/test_utils.ex +++ b/test/support/test_utils.ex @@ -75,7 +75,8 @@ defmodule Plausible.TestUtils do end def create_api_key(%{user: user}) do - api_key = Factory.insert(:api_key, user: user) + team = Plausible.Teams.Test.team_of(user) + api_key = Factory.insert(:api_key, user: user, team: team) {:ok, api_key: api_key.key} end diff --git a/test/test_helper.exs b/test/test_helper.exs index fef0ce84c6..1fbe5e797d 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -17,7 +17,7 @@ if :minio in Keyword.fetch!(ExUnit.configuration(), :include) do Plausible.TestUtils.ensure_minio() end -default_exclude = [:slow, :minio, :migrations] +default_exclude = [:slow, :minio, :migrations, :kaffy_quirks] # avoid slowdowns contacting the code server https://github.com/sasa1977/con_cache/pull/79 :code.ensure_loaded(ConCache.Lock.Resource)