Populate `team_id` when provisioning API key (#5234)
* Set team when creating API key * Create API key with team ID and adjust API key CRM * Make CRM work with team-less API keys still * Scope owner's API keys by team on setup * Rate limit team scoped API keys by their team * Enforce team scoping for API key with a team * Prevent using legacy API keys against setup teams * Enforce team scoping in Sites API too * Scope API keys list in settings by team * Do not prevent legacy API keys from accessing setup teams * List legacy API keys across all teams * Display "API Keys" under "Team Settings" when team is setup * Scope teams index in Sites API as well * Test Sites API actions * Revert "Scope owner's API keys by team on setup" This reverts commit 08fd5b4e801417a28ebb9937457cf3e59f7386a0. * Test and slightly simplify API key CRM logic * Test API key provisioning from Account Settings * Test `AuthorizePublicApi` plug adjustments * Simplify conditionals (h/t @aerosol) * Change back to using `schema` in CRM logic * Don't run tests triggering Kaffy warning locally * Run quirky Kaffy tests only on CI in EE env
This commit is contained in:
parent
8d98a75cd5
commit
94e9a20038
|
|
@ -102,7 +102,7 @@ jobs:
|
||||||
|
|
||||||
- run: make minio
|
- run: make minio
|
||||||
if: env.MIX_ENV == 'test'
|
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'
|
if: env.MIX_ENV == 'test'
|
||||||
env:
|
env:
|
||||||
MINIO_HOST_FOR_CLICKHOUSE: "172.17.0.1"
|
MINIO_HOST_FOR_CLICKHOUSE: "172.17.0.1"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
@pagination_opts [cursor_fields: [{:id, :desc}], limit: 100, maximum_limit: 1000]
|
@pagination_opts [cursor_fields: [{:id, :desc}], limit: 100, maximum_limit: 1000]
|
||||||
|
|
||||||
def index(conn, params) do
|
def index(conn, params) do
|
||||||
team = Teams.get(params["team_id"])
|
|
||||||
user = conn.assigns.current_user
|
user = conn.assigns.current_user
|
||||||
|
team = conn.assigns.current_team || Teams.get(params["team_id"])
|
||||||
|
|
||||||
page =
|
page =
|
||||||
user
|
user
|
||||||
|
|
@ -30,9 +30,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
|
|
||||||
def guests_index(conn, params) do
|
def guests_index(conn, params) do
|
||||||
user = conn.assigns.current_user
|
user = conn.assigns.current_user
|
||||||
|
team = conn.assigns.current_team
|
||||||
|
|
||||||
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
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]
|
opts = [cursor_fields: [inserted_at: :desc, id: :desc], limit: 100, maximum_limit: 1000]
|
||||||
page = site |> Sites.list_guests_query() |> paginate(params, opts)
|
page = site |> Sites.list_guests_query() |> paginate(params, opts)
|
||||||
|
|
||||||
|
|
@ -54,10 +55,11 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
|
|
||||||
def teams_index(conn, params) do
|
def teams_index(conn, params) do
|
||||||
user = conn.assigns.current_user
|
user = conn.assigns.current_user
|
||||||
|
team = conn.assigns.current_team
|
||||||
|
|
||||||
page =
|
page =
|
||||||
user
|
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)
|
|> paginate(params, @pagination_opts)
|
||||||
|
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
|
|
@ -78,9 +80,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
|
|
||||||
def goals_index(conn, params) do
|
def goals_index(conn, params) do
|
||||||
user = conn.assigns.current_user
|
user = conn.assigns.current_user
|
||||||
|
team = conn.assigns.current_team
|
||||||
|
|
||||||
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
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 =
|
page =
|
||||||
site
|
site
|
||||||
|> Plausible.Goals.for_site_query()
|
|> Plausible.Goals.for_site_query()
|
||||||
|
|
@ -110,7 +113,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
|
|
||||||
def create_site(conn, params) do
|
def create_site(conn, params) do
|
||||||
user = conn.assigns.current_user
|
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
|
case Sites.create(user, params, team) do
|
||||||
{:ok, %{site: site}} ->
|
{:ok, %{site: site}} ->
|
||||||
|
|
@ -146,7 +149,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_site(conn, %{"site_id" => site_id}) do
|
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} ->
|
{:ok, site} ->
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
domain: site.domain,
|
domain: site.domain,
|
||||||
|
|
@ -160,7 +166,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_site(conn, %{"site_id" => site_id}) do
|
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, site} ->
|
||||||
{:ok, _} = Plausible.Site.Removal.run(site)
|
{:ok, _} = Plausible.Site.Removal.run(site)
|
||||||
json(conn, %{"deleted" => true})
|
json(conn, %{"deleted" => true})
|
||||||
|
|
@ -171,8 +180,11 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_site(conn, %{"site_id" => site_id} = params) do
|
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
|
# 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
|
{:ok, site} <- Plausible.Site.Domain.change(site, params["domain"]) do
|
||||||
json(conn, site)
|
json(conn, site)
|
||||||
else
|
else
|
||||||
|
|
@ -187,10 +199,13 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_guest(conn, params) do
|
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"),
|
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
||||||
{:ok, email} <- expect_param_key(params, "email"),
|
{:ok, email} <- expect_param_key(params, "email"),
|
||||||
{:ok, role} <- expect_param_key(params, "role", ["viewer", "editor"]),
|
{: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))
|
existing = Repo.one(Sites.list_guests_query(site, email: email))
|
||||||
|
|
||||||
if existing do
|
if existing do
|
||||||
|
|
@ -230,9 +245,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_guest(conn, params) do
|
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"),
|
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
||||||
{:ok, email} <- expect_param_key(params, "email"),
|
{: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))
|
existing = Repo.one(Sites.list_guests_query(site, email: email))
|
||||||
|
|
||||||
case existing do
|
case existing do
|
||||||
|
|
@ -262,9 +280,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_shared_link(conn, params) do
|
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"),
|
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
||||||
{:ok, link_name} <- expect_param_key(params, "name"),
|
{: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 = Repo.get_by(Plausible.Site.SharedLink, site_id: site.id, name: link_name)
|
||||||
|
|
||||||
shared_link =
|
shared_link =
|
||||||
|
|
@ -296,9 +317,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_goal(conn, params) do
|
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"),
|
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
||||||
{:ok, _} <- expect_param_key(params, "goal_type"),
|
{: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
|
{:ok, goal} <- Goals.find_or_create(site, params) do
|
||||||
json(conn, goal)
|
json(conn, goal)
|
||||||
else
|
else
|
||||||
|
|
@ -314,9 +338,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_goal(conn, params) do
|
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"),
|
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
||||||
{:ok, goal_id} <- expect_param_key(params, "goal_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
|
:ok <- Goals.delete(goal_id, site) do
|
||||||
json(conn, %{"deleted" => true})
|
json(conn, %{"deleted" => true})
|
||||||
else
|
else
|
||||||
|
|
@ -345,10 +372,19 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
}
|
}
|
||||||
end
|
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
|
case Plausible.Sites.get_for_user(user, site_id, roles) do
|
||||||
nil -> {:error, :site_not_found}
|
nil ->
|
||||||
site -> {:ok, site}
|
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ defmodule Plausible.Auth.ApiKey do
|
||||||
field :key_hash, :string
|
field :key_hash, :string
|
||||||
field :key_prefix, :string
|
field :key_prefix, :string
|
||||||
|
|
||||||
|
belongs_to :team, Plausible.Teams.Team
|
||||||
belongs_to :user, Plausible.Auth.User
|
belongs_to :user, Plausible.Auth.User
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
|
|
@ -25,17 +26,24 @@ defmodule Plausible.Auth.ApiKey do
|
||||||
|
|
||||||
def hourly_request_limit(), do: @hourly_request_limit
|
def hourly_request_limit(), do: @hourly_request_limit
|
||||||
|
|
||||||
def changeset(schema, attrs \\ %{}) do
|
def changeset(struct, team, attrs) do
|
||||||
schema
|
struct
|
||||||
|> cast(attrs, @required ++ @optional)
|
|> cast(attrs, @required ++ @optional)
|
||||||
|> validate_required(@required)
|
|> validate_required(@required)
|
||||||
|> maybe_put_key()
|
|> maybe_put_key()
|
||||||
|> process_key()
|
|> process_key()
|
||||||
|
|> maybe_put_team(team)
|
||||||
|> unique_constraint(:key_hash, error_key: :key)
|
|> unique_constraint(:key_hash, error_key: :key)
|
||||||
|
|> unique_constraint([:team_id, :user_id], error_key: :team)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(schema, attrs \\ %{}) do
|
# NOTE: needed only because of lacking introspection in Kaffy
|
||||||
schema
|
def changeset(struct, attrs) do
|
||||||
|
changeset(struct, nil, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(struct, attrs \\ %{}) do
|
||||||
|
struct
|
||||||
|> cast(attrs, [:name, :user_id, :scopes])
|
|> cast(attrs, [:name, :user_id, :scopes])
|
||||||
|> validate_required([:user_id, :name])
|
|> validate_required([:user_id, :name])
|
||||||
end
|
end
|
||||||
|
|
@ -57,6 +65,12 @@ defmodule Plausible.Auth.ApiKey do
|
||||||
|
|
||||||
def process_key(changeset), do: changeset
|
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
|
defp maybe_put_key(changeset) do
|
||||||
if get_change(changeset, :key) do
|
if get_change(changeset, :key) do
|
||||||
changeset
|
changeset
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,76 @@
|
||||||
defmodule Plausible.Auth.ApiKeyAdmin do
|
defmodule Plausible.Auth.ApiKeyAdmin do
|
||||||
|
@moduledoc """
|
||||||
|
Stats and Sites API key logic for CRM.
|
||||||
|
"""
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
|
|
||||||
|
alias Plausible.Auth
|
||||||
|
alias Plausible.Teams
|
||||||
|
|
||||||
def search_fields(_schema) do
|
def search_fields(_schema) do
|
||||||
[
|
[
|
||||||
:name,
|
:name,
|
||||||
user: [:name, :email]
|
user: [:name, :email],
|
||||||
|
team: [:name, :identifier]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_index_query(_conn, _schema, query) do
|
def custom_index_query(_conn, _schema, query) do
|
||||||
from(r in query, preload: [:user])
|
from(r in query, preload: [:user, team: :owners])
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_changeset(schema, attrs) do
|
def create_changeset(schema, attrs) do
|
||||||
scopes = [attrs["scope"]]
|
team = Teams.get(attrs["team_identifier"])
|
||||||
Plausible.Auth.ApiKey.changeset(struct(schema, %{}), Map.merge(%{"scopes" => scopes}, attrs))
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def update_changeset(schema, attrs) do
|
def update_changeset(entry, attrs) do
|
||||||
Plausible.Auth.ApiKey.update(schema, attrs)
|
Auth.ApiKey.update(entry, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@plaintext_key_help """
|
@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
|
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
|
def form_fields(_) do
|
||||||
[
|
[
|
||||||
name: nil,
|
name: nil,
|
||||||
key: %{create: :readonly, update: :hidden, help_text: @plaintext_key_help},
|
key: %{create: :readonly, update: :hidden, help_text: @plaintext_key_help},
|
||||||
key_prefix: %{create: :hidden, update: :readonly},
|
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
|
user_id: nil
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -39,7 +80,55 @@ defmodule Plausible.Auth.ApiKeyAdmin do
|
||||||
key_prefix: nil,
|
key_prefix: nil,
|
||||||
name: nil,
|
name: nil,
|
||||||
scopes: nil,
|
scopes: nil,
|
||||||
owner: %{value: & &1.user.email}
|
owner: %{value: &get_owner/1},
|
||||||
|
team: %{value: &get_team/1}
|
||||||
]
|
]
|
||||||
end
|
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("""
|
||||||
|
<a href="/crm/teams/team/#{api_key.team.id}">#{team_name}</a>
|
||||||
|
""")
|
||||||
|
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 =
|
||||||
|
"""
|
||||||
|
<a href="/crm/auth/user/#{api_key.user.id}">#{escaped_name}</a>
|
||||||
|
<br/>
|
||||||
|
#{escaped_email}
|
||||||
|
"""
|
||||||
|
|
||||||
|
{:safe, owner_html}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp html_escape(string) do
|
||||||
|
string
|
||||||
|
|> Phoenix.HTML.html_escape()
|
||||||
|
|> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ defmodule Plausible.Auth do
|
||||||
|
|
||||||
use Plausible
|
use Plausible
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
alias Plausible.Auth
|
alias Plausible.Auth
|
||||||
|
alias Plausible.Billing
|
||||||
alias Plausible.RateLimit
|
alias Plausible.RateLimit
|
||||||
alias Plausible.Teams
|
alias Plausible.Teams
|
||||||
|
|
||||||
|
|
@ -154,13 +158,25 @@ defmodule Plausible.Auth do
|
||||||
def is_super_admin?(_), do: false
|
def is_super_admin?(_), do: false
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec create_api_key(Auth.User.t(), String.t(), String.t()) ::
|
@spec list_api_keys(Auth.User.t(), Teams.Team.t() | nil) :: [Auth.ApiKey.t()]
|
||||||
{:ok, Auth.ApiKey.t()} | {:error, Ecto.Changeset.t() | :upgrade_required}
|
def list_api_keys(user, team) do
|
||||||
def create_api_key(user, name, key) do
|
query =
|
||||||
params = %{name: name, user_id: user.id, key: key}
|
from(a in Auth.ApiKey,
|
||||||
changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, params)
|
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)
|
Repo.insert(changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -176,7 +192,7 @@ defmodule Plausible.Auth do
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec find_api_key(String.t(), Keyword.t()) ::
|
@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}
|
| {:error, :invalid_api_key | :missing_site_id}
|
||||||
def find_api_key(raw_key, opts \\ []) do
|
def find_api_key(raw_key, opts \\ []) do
|
||||||
{team_scope, id} = Keyword.get(opts, :team_by, {nil, nil})
|
{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)
|
find_api_key(raw_key, team_scope, id)
|
||||||
end
|
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
|
defp find_api_key(raw_key, nil, _) do
|
||||||
hashed_key = Auth.ApiKey.do_hash(raw_key)
|
hashed_key = Auth.ApiKey.do_hash(raw_key)
|
||||||
|
|
||||||
query =
|
query =
|
||||||
from(api_key in Auth.ApiKey,
|
from(api_key in Auth.ApiKey,
|
||||||
join: user in assoc(api_key, :user),
|
join: user in assoc(api_key, :user),
|
||||||
|
left_join: team in assoc(api_key, :team),
|
||||||
where: api_key.key_hash == ^hashed_key,
|
where: api_key.key_hash == ^hashed_key,
|
||||||
preload: [user: user]
|
preload: [user: user, team: team]
|
||||||
)
|
)
|
||||||
|
|
||||||
if found = Repo.one(query) do
|
case Repo.one(query) do
|
||||||
{:ok, found}
|
nil ->
|
||||||
else
|
{:error, :invalid_api_key}
|
||||||
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
defp find_api_key(raw_key, :site, nil) do
|
defp find_api_key(raw_key, :site, nil) do
|
||||||
with {:ok, api_key} <- find_api_key(raw_key, nil, nil) do
|
find_api_key(raw_key, nil, nil)
|
||||||
{:ok, %{api_key: api_key, team: nil}}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp find_api_key(raw_key, :site, domain) do
|
defp find_api_key(raw_key, :site, domain) do
|
||||||
with {:ok, api_key} <- find_api_key(raw_key, nil, nil) do
|
case find_api_key(raw_key, nil, nil) do
|
||||||
team = find_team_by_site(domain)
|
{:ok, %{api_key: api_key, team: nil}} ->
|
||||||
{:ok, %{api_key: api_key, team: team}}
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -223,21 +258,6 @@ defmodule Plausible.Auth do
|
||||||
)
|
)
|
||||||
end
|
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(%Auth.User{id: id}), do: id
|
||||||
defp rate_limit_key(%Plug.Conn{} = conn), do: PlausibleWeb.RemoteIP.get(conn)
|
defp rate_limit_key(%Plug.Conn{} = conn), do: PlausibleWeb.RemoteIP.get(conn)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,13 @@ defmodule Plausible.Teams do
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(team_identifier) when is_binary(team_identifier) do
|
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
|
end
|
||||||
|
|
||||||
@spec get!(pos_integer() | binary()) :: Teams.Team.t()
|
@spec get!(pos_integer() | binary()) :: Teams.Team.t()
|
||||||
|
|
@ -291,12 +297,14 @@ defmodule Plausible.Teams do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec last_subscription_join_query() :: Ecto.Query.t()
|
||||||
def last_subscription_join_query() do
|
def last_subscription_join_query() do
|
||||||
from(subscription in last_subscription_query(),
|
from(subscription in last_subscription_query(),
|
||||||
where: subscription.team_id == parent_as(:team).id
|
where: subscription.team_id == parent_as(:team).id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec last_subscription_query() :: Ecto.Query.t()
|
||||||
def last_subscription_query() do
|
def last_subscription_query() do
|
||||||
from(subscription in Plausible.Billing.Subscription,
|
from(subscription in Plausible.Billing.Subscription,
|
||||||
order_by: [desc: subscription.inserted_at, desc: subscription.id],
|
order_by: [desc: subscription.inserted_at, desc: subscription.id],
|
||||||
|
|
@ -304,7 +312,9 @@ defmodule Plausible.Teams do
|
||||||
)
|
)
|
||||||
end
|
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)
|
only_not_setup? = Keyword.get(opts, :only_not_setup?, false)
|
||||||
|
|
||||||
query =
|
query =
|
||||||
|
|
|
||||||
|
|
@ -92,21 +92,26 @@ defmodule PlausibleWeb.SettingsController do
|
||||||
|
|
||||||
def api_keys(conn, _params) do
|
def api_keys(conn, _params) do
|
||||||
current_user = conn.assigns.current_user
|
current_user = conn.assigns.current_user
|
||||||
|
current_team = conn.assigns[:current_team]
|
||||||
|
|
||||||
api_keys =
|
api_keys = Auth.list_api_keys(current_user, current_team)
|
||||||
Repo.preload(current_user, :api_keys).api_keys
|
|
||||||
|
|
||||||
render(conn, :api_keys, layout: {PlausibleWeb.LayoutView, :settings}, api_keys: api_keys)
|
render(conn, :api_keys, layout: {PlausibleWeb.LayoutView, :settings}, api_keys: api_keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_api_key(conn, _params) do
|
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)
|
render(conn, "new_api_key.html", changeset: changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_api_key(conn, %{"api_key" => %{"name" => name, "key" => key}}) do
|
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} ->
|
{:ok, _api_key} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:success, "API key created successfully")
|
|> put_flash(:success, "API key created successfully")
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
|
||||||
{:ok, api_key, limit_key, hourly_limit} <- find_api_key(conn, token, context),
|
{:ok, api_key, limit_key, hourly_limit} <- find_api_key(conn, token, context),
|
||||||
:ok <- check_api_key_rate_limit(limit_key, hourly_limit),
|
:ok <- check_api_key_rate_limit(limit_key, hourly_limit),
|
||||||
{:ok, conn} <- verify_by_scope(conn, api_key, requested_scope) do
|
{: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
|
else
|
||||||
error -> send_error(conn, requested_scope, error)
|
error -> send_error(conn, requested_scope, error)
|
||||||
end
|
end
|
||||||
|
|
@ -97,8 +99,15 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp find_api_key(_conn, token, _) do
|
defp find_api_key(_conn, token, _) do
|
||||||
with {:ok, api_key} <- Auth.find_api_key(token) do
|
case Auth.find_api_key(token) do
|
||||||
{:ok, api_key, limit_key(api_key, nil), Auth.ApiKey.hourly_request_limit()}
|
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -189,6 +198,9 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
|
||||||
is_super_admin? ->
|
is_super_admin? ->
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
|
api_key.team_id && api_key.team_id != site.team_id ->
|
||||||
|
{:error, :invalid_api_key}
|
||||||
|
|
||||||
Sites.locked?(site) ->
|
Sites.locked?(site) ->
|
||||||
{:error, :site_locked}
|
{:error, :site_locked}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,9 @@ defmodule PlausibleWeb.LayoutView do
|
||||||
if(not Teams.setup?(current_team),
|
if(not Teams.setup?(current_team),
|
||||||
do: %{key: "Invoices", value: "billing/invoices", icon: :banknotes}
|
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}
|
%{key: "Danger Zone", value: "danger-zone", icon: :exclamation_triangle}
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
@ -127,6 +129,7 @@ defmodule PlausibleWeb.LayoutView do
|
||||||
if(current_team_role in [:owner, :admin, :billing],
|
if(current_team_role in [:owner, :admin, :billing],
|
||||||
do: %{key: "Invoices", value: "billing/invoices", icon: :banknotes}
|
do: %{key: "Invoices", value: "billing/invoices", icon: :banknotes}
|
||||||
),
|
),
|
||||||
|
%{key: "API Keys", value: "api-keys", icon: :key},
|
||||||
if(current_team_role == :owner,
|
if(current_team_role == :owner,
|
||||||
do: %{key: "Danger Zone", value: "team/delete", icon: :exclamation_triangle}
|
do: %{key: "Danger Zone", value: "team/delete", icon: :exclamation_triangle}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,21 @@ defmodule Plausible.AuthTest do
|
||||||
describe "create_api_key/3" do
|
describe "create_api_key/3" do
|
||||||
test "creates a new api key" do
|
test "creates a new api key" do
|
||||||
user = new_user(trial_expiry_date: Date.utc_today())
|
user = new_user(trial_expiry_date: Date.utc_today())
|
||||||
|
team = team_of(user)
|
||||||
key = Ecto.UUID.generate()
|
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
|
end
|
||||||
|
|
||||||
test "errors when key already exists" do
|
test "errors when key already exists" do
|
||||||
u1 = new_user(trial_expiry_date: Date.utc_today())
|
u1 = new_user(trial_expiry_date: Date.utc_today())
|
||||||
|
t1 = team_of(u1)
|
||||||
u2 = new_user(trial_expiry_date: Date.utc_today())
|
u2 = new_user(trial_expiry_date: Date.utc_today())
|
||||||
|
t2 = team_of(u2)
|
||||||
key = Ecto.UUID.generate()
|
key = Ecto.UUID.generate()
|
||||||
assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(u1, "my new key", key)
|
assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(u1, t1, "my new key", key)
|
||||||
assert {:error, changeset} = Auth.create_api_key(u2, "my other key", key)
|
assert {:error, changeset} = Auth.create_api_key(u2, t2, "my other key", key)
|
||||||
|
|
||||||
assert changeset.errors[:key] ==
|
assert changeset.errors[:key] ==
|
||||||
{"has already been taken",
|
{"has already been taken",
|
||||||
|
|
@ -38,27 +43,30 @@ defmodule Plausible.AuthTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag :ee_only
|
@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()
|
user = new_user() |> subscribe_to_growth_plan()
|
||||||
|
team = team_of(user)
|
||||||
|
|
||||||
assert {:error, :upgrade_required} =
|
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
|
end
|
||||||
|
|
||||||
test "creates a key for user on a growth plan when they are an owner of more than one team" do
|
test "creates a key for user in a team with a bunsiness plan" do
|
||||||
user = new_user() |> subscribe_to_growth_plan()
|
user = new_user() |> subscribe_to_business_plan()
|
||||||
|
team = team_of(user)
|
||||||
another_site = new_site()
|
another_site = new_site()
|
||||||
add_member(another_site.team, user: user, role: :owner)
|
add_member(another_site.team, user: user, role: :owner)
|
||||||
|
|
||||||
assert {:ok, %Auth.ApiKey{}} =
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_api_key/2" do
|
describe "delete_api_key/2" do
|
||||||
test "deletes the record" do
|
test "deletes the record" do
|
||||||
user = new_user(trial_expiry_date: Date.utc_today())
|
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)
|
assert :ok = Auth.delete_api_key(user, api_key.id)
|
||||||
refute Plausible.Repo.reload(api_key)
|
refute Plausible.Repo.reload(api_key)
|
||||||
end
|
end
|
||||||
|
|
@ -67,7 +75,10 @@ defmodule Plausible.AuthTest do
|
||||||
me = new_user(trial_expiry_date: Date.utc_today())
|
me = new_user(trial_expiry_date: Date.utc_today())
|
||||||
|
|
||||||
other_user = 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, other_api_key.id)
|
||||||
assert {:error, :not_found} = Auth.delete_api_key(me, -1)
|
assert {:error, :not_found} = Auth.delete_api_key(me, -1)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ defmodule PlausibleWeb.AdminControllerTest do
|
||||||
use Plausible.Teams.Test
|
use Plausible.Teams.Test
|
||||||
|
|
||||||
alias Plausible.Repo
|
alias Plausible.Repo
|
||||||
|
alias Plausible.Teams
|
||||||
|
|
||||||
describe "GET /crm/teams/team/:team_id/usage" do
|
describe "GET /crm/teams/team/:team_id/usage" do
|
||||||
setup [:create_user, :log_in, :create_team]
|
setup [:create_user, :log_in, :create_team]
|
||||||
|
|
@ -269,4 +270,158 @@ defmodule PlausibleWeb.AdminControllerTest do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,35 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "POST /api/v1/sites" do
|
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
|
assert Repo.get_by(Plausible.Site, domain: "some-site.domain").team_id == team.id
|
||||||
end
|
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
|
test "timezone is validated", %{conn: conn} do
|
||||||
conn =
|
conn =
|
||||||
post(conn, "/api/v1/sites", %{
|
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"}
|
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
||||||
end
|
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
|
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:*"])
|
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)
|
assert %{"url" => ^url} = json_response(conn, 200)
|
||||||
end
|
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
|
test "returns 400 when site id missing", %{conn: conn} do
|
||||||
conn =
|
conn =
|
||||||
put(conn, "/api/v1/sites/shared-links", %{
|
put(conn, "/api/v1/sites/shared-links", %{
|
||||||
|
|
@ -459,6 +544,23 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
assert %{"id" => ^goal_id} = json_response(conn, 200)
|
assert %{"id" => ^goal_id} = json_response(conn, 200)
|
||||||
end
|
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
|
test "returns 400 when site id missing", %{conn: conn} do
|
||||||
conn =
|
conn =
|
||||||
put(conn, "/api/v1/sites/goals", %{
|
put(conn, "/api/v1/sites/goals", %{
|
||||||
|
|
@ -580,6 +682,30 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
assert json_response(conn, 200) == %{"deleted" => true}
|
assert json_response(conn, 200) == %{"deleted" => true}
|
||||||
end
|
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
|
test "is 404 when goal cannot be found", %{conn: conn, site: site} do
|
||||||
conn =
|
conn =
|
||||||
delete(conn, "/api/v1/sites/goals/0", %{
|
delete(conn, "/api/v1/sites/goals/0", %{
|
||||||
|
|
@ -690,6 +816,32 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
} = json_response(conn, 200)
|
} = json_response(conn, 200)
|
||||||
end
|
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
|
test "handles pagination correctly", %{conn: conn, user: user} do
|
||||||
[
|
[
|
||||||
%{domain: site1_domain},
|
%{domain: site1_domain},
|
||||||
|
|
@ -827,6 +979,22 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
|
|
||||||
assert is_binary(before_cursor)
|
assert is_binary(before_cursor)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "PUT /api/v1/sites/guests" do
|
describe "PUT /api/v1/sites/guests" do
|
||||||
|
|
@ -900,6 +1068,24 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
assert_no_emails_delivered()
|
assert_no_emails_delivered()
|
||||||
end
|
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
|
test "fails for unknown role", %{conn: conn, user: user} do
|
||||||
site = new_site(owner: user)
|
site = new_site(owner: user)
|
||||||
|
|
||||||
|
|
@ -970,6 +1156,20 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
assert json_response(conn2, 200) == %{"deleted" => true}
|
assert json_response(conn2, 200) == %{"deleted" => true}
|
||||||
end
|
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
|
test "won't delete non-guest membership", %{conn: conn, user: user} do
|
||||||
site = new_site(owner: user)
|
site = new_site(owner: user)
|
||||||
|
|
||||||
|
|
@ -1031,6 +1231,18 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
}
|
}
|
||||||
end
|
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
|
test "is 404 when site cannot be found", %{conn: conn} do
|
||||||
conn = get(conn, "/api/v1/sites/foobar.baz")
|
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)
|
assert %{"goals" => [%{"id" => ^goal_id}]} = json_response(conn, 200)
|
||||||
end
|
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
|
test "returns error when `site_id` parameter is missing", %{conn: conn} do
|
||||||
conn = get(conn, "/api/v1/sites/goals")
|
conn = get(conn, "/api/v1/sites/goals")
|
||||||
|
|
||||||
|
|
@ -1214,6 +1440,24 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
assert site.domain_changed_from == old_domain
|
assert site.domain_changed_from == old_domain
|
||||||
end
|
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
|
test "can't make a no-op change", %{conn: conn, site: site} do
|
||||||
conn =
|
conn =
|
||||||
put(conn, "/api/v1/sites/#{site.domain}", %{
|
put(conn, "/api/v1/sites/#{site.domain}", %{
|
||||||
|
|
|
||||||
|
|
@ -1030,6 +1030,8 @@ defmodule PlausibleWeb.SettingsControllerTest do
|
||||||
test "can create an API key", %{conn: conn, user: user} do
|
test "can create an API key", %{conn: conn, user: user} do
|
||||||
new_site(owner: user)
|
new_site(owner: user)
|
||||||
|
|
||||||
|
team = team_of(user)
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
post(conn, Routes.settings_path(conn, :api_keys), %{
|
post(conn, Routes.settings_path(conn, :api_keys), %{
|
||||||
"api_key" => %{
|
"api_key" => %{
|
||||||
|
|
@ -1042,6 +1044,31 @@ defmodule PlausibleWeb.SettingsControllerTest do
|
||||||
key = Plausible.Auth.ApiKey |> where(user_id: ^user.id) |> Repo.one()
|
key = Plausible.Auth.ApiKey |> where(user_id: ^user.id) |> Repo.one()
|
||||||
assert conn.status == 302
|
assert conn.status == 302
|
||||||
assert key.name == "all your code are belong to us"
|
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
|
end
|
||||||
|
|
||||||
test "cannot create a duplicate API key", %{conn: conn, user: user} do
|
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
|
test "can't delete api key that doesn't belong to me", %{conn: conn} do
|
||||||
other_user = new_user()
|
other_user = new_user()
|
||||||
new_site(owner: other_user)
|
new_site(owner: other_user)
|
||||||
|
team = team_of(other_user)
|
||||||
|
|
||||||
assert {:ok, %ApiKey{} = api_key} =
|
assert {:ok, %ApiKey{} = api_key} =
|
||||||
%ApiKey{user_id: other_user.id}
|
%ApiKey{user_id: other_user.id}
|
||||||
|> ApiKey.changeset(%{"name" => "other user's key"})
|
|> ApiKey.changeset(team, %{"name" => "other user's key"})
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|
||||||
conn = delete(conn, Routes.settings_path(conn, :delete_api_key, api_key.id))
|
conn = delete(conn, Routes.settings_path(conn, :delete_api_key, api_key.id))
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,47 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPITest do
|
||||||
assert conn.assigns.site.id == site.id
|
assert conn.assigns.site.id == site.id
|
||||||
end
|
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
|
@tag :ee_only
|
||||||
test "passes for super admin user even if not a member of the requested site", %{conn: conn} do
|
test "passes for super admin user even if not a member of the requested site", %{conn: conn} do
|
||||||
user = new_user()
|
user = new_user()
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ defmodule Plausible.TestUtils do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_api_key(%{user: user}) do
|
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}
|
{:ok, api_key: api_key.key}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ if :minio in Keyword.fetch!(ExUnit.configuration(), :include) do
|
||||||
Plausible.TestUtils.ensure_minio()
|
Plausible.TestUtils.ensure_minio()
|
||||||
end
|
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
|
# avoid slowdowns contacting the code server https://github.com/sasa1977/con_cache/pull/79
|
||||||
:code.ensure_loaded(ConCache.Lock.Resource)
|
:code.ensure_loaded(ConCache.Lock.Resource)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue