mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +03:00
Refactor Sites and Stats API authorization logic (#4297)
* Refactor and unify auth plugs for Stats and Sites APIs * Expose get site Sites API endpoint to all API keys * Test the new plug * Add test for endpoint with modified scope * Fix typos Co-authored-by: hq1 <hq@mtod.org> * Rename plug for consistency (h/t @aerosol) --------- Co-authored-by: hq1 <hq@mtod.org>
This commit is contained in:
parent
839691d29f
commit
790984e1ad
@ -1,57 +0,0 @@
|
||||
defmodule PlausibleWeb.AuthorizeSitesApiPlug do
|
||||
import Plug.Conn
|
||||
use Plausible.Repo
|
||||
alias Plausible.Auth.ApiKey
|
||||
alias PlausibleWeb.Api.Helpers, as: H
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
with {:ok, raw_api_key} <- get_bearer_token(conn),
|
||||
{:ok, api_key} <- verify_access(raw_api_key) do
|
||||
user = Repo.get_by(Plausible.Auth.User, id: api_key.user_id)
|
||||
assign(conn, :current_user, user)
|
||||
else
|
||||
{:error, :missing_api_key} ->
|
||||
H.unauthorized(
|
||||
conn,
|
||||
"Missing API key. Please use a valid Plausible API key as a Bearer Token."
|
||||
)
|
||||
|
||||
{:error, :invalid_api_key} ->
|
||||
H.unauthorized(
|
||||
conn,
|
||||
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_access(api_key) do
|
||||
hashed_key = ApiKey.do_hash(api_key)
|
||||
|
||||
found_key =
|
||||
Repo.one(
|
||||
from a in ApiKey,
|
||||
where: a.key_hash == ^hashed_key,
|
||||
where: fragment("? @> ?", a.scopes, ["sites:provision:*"])
|
||||
)
|
||||
|
||||
cond do
|
||||
found_key -> {:ok, found_key}
|
||||
true -> {:error, :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_bearer_token(conn) do
|
||||
authorization_header =
|
||||
Plug.Conn.get_req_header(conn, "authorization")
|
||||
|> List.first()
|
||||
|
||||
case authorization_header do
|
||||
"Bearer " <> token -> {:ok, String.trim(token)}
|
||||
_ -> {:error, :missing_api_key}
|
||||
end
|
||||
end
|
||||
end
|
206
lib/plausible_web/plugs/authorize_public_api.ex
Normal file
206
lib/plausible_web/plugs/authorize_public_api.ex
Normal file
@ -0,0 +1,206 @@
|
||||
defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
|
||||
@moduledoc """
|
||||
Plug for authorizing access to Stats and Sites APIs.
|
||||
|
||||
The plug expects `:api_scope` to be provided in the assigns. The scope
|
||||
will then be used to check for API key validity. The assign can be
|
||||
provided in the router configuration in a following way:
|
||||
|
||||
scope "/api/v1/stats", PlausibleWeb.Api, assigns: %{api_scope: "some:scope:*"} do
|
||||
pipe_through [:public_api, #{inspect(__MODULE__)}]
|
||||
|
||||
# route definitions follow
|
||||
# ...
|
||||
end
|
||||
|
||||
The scope from `:api_scope` is checked for match against all scopes from API key's
|
||||
`scopes` field. If the scope is among `@implicit_scopes`, it's considered to be
|
||||
present for any valid API key. Scopes are checked for match by prefix, so if we have
|
||||
`some:scope:*` in matching route `:api_scope` and the API key has `some:*` in its
|
||||
`scopes` field, they will match.
|
||||
|
||||
After a match is found, additional verification can be conducted, like in case of
|
||||
`stats:read:*`, where valid site ID is expected among parameters too.
|
||||
|
||||
All API requests are rate limited per API key, enforcing a given hourly request limit.
|
||||
"""
|
||||
|
||||
use Plausible.Repo
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
alias Plausible.Auth
|
||||
alias Plausible.RateLimit
|
||||
alias Plausible.Sites
|
||||
alias PlausibleWeb.Api.Helpers, as: H
|
||||
|
||||
# Scopes permitted implicitly for every API key. Existing API keys
|
||||
# have _either_ `["stats:read:*"]` (the default) or `["sites:provision:*"]`
|
||||
# set as their valid scopes. We always consider implicit scopes as
|
||||
# present in addition to whatever else is provided for a particular
|
||||
# API key.
|
||||
@implicit_scopes ["stats:read:*", "sites:read:*"]
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
requested_scope = Map.fetch!(conn.assigns, :api_scope)
|
||||
|
||||
with {:ok, token} <- get_bearer_token(conn),
|
||||
{:ok, api_key} <- Auth.find_api_key(token),
|
||||
:ok <- check_api_key_rate_limit(api_key),
|
||||
{:ok, conn} <- verify_by_scope(conn, api_key, requested_scope) do
|
||||
assign(conn, :current_user, api_key.user)
|
||||
else
|
||||
error -> send_error(conn, requested_scope, error)
|
||||
end
|
||||
end
|
||||
|
||||
### Verification dispatched by scope
|
||||
|
||||
defp verify_by_scope(conn, api_key, "stats:read:" <> _ = scope) do
|
||||
with :ok <- check_scope(api_key, scope),
|
||||
{:ok, site} <- find_site(conn.params["site_id"]),
|
||||
:ok <- verify_site_access(api_key, site) do
|
||||
Plausible.OpenTelemetry.add_site_attributes(site)
|
||||
site = Plausible.Imported.load_import_data(site)
|
||||
{:ok, assign(conn, :site, site)}
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_by_scope(conn, api_key, scope) do
|
||||
with :ok <- check_scope(api_key, scope) do
|
||||
{:ok, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_scope(_api_key, required_scope) when required_scope in @implicit_scopes do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp check_scope(api_key, required_scope) do
|
||||
found? =
|
||||
Enum.any?(api_key.scopes, fn scope ->
|
||||
scope = String.trim_trailing(scope, "*")
|
||||
|
||||
String.starts_with?(required_scope, scope)
|
||||
end)
|
||||
|
||||
if found? do
|
||||
:ok
|
||||
else
|
||||
{:error, :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_bearer_token(conn) do
|
||||
authorization_header =
|
||||
conn
|
||||
|> Plug.Conn.get_req_header("authorization")
|
||||
|> List.first()
|
||||
|
||||
case authorization_header do
|
||||
"Bearer " <> token -> {:ok, String.trim(token)}
|
||||
_ -> {:error, :missing_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_api_key_rate_limit(api_key) do
|
||||
case RateLimit.check_rate(
|
||||
"api_request:#{api_key.id}",
|
||||
to_timeout(hour: 1),
|
||||
api_key.hourly_request_limit
|
||||
) do
|
||||
{:allow, _} -> :ok
|
||||
{:deny, _} -> {:error, :rate_limit, api_key.hourly_request_limit}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_site(nil), do: {:error, :missing_site_id}
|
||||
|
||||
defp find_site(site_id) do
|
||||
domain_based_search =
|
||||
from s in Plausible.Site, where: s.domain == ^site_id or s.domain_changed_from == ^site_id
|
||||
|
||||
case Repo.one(domain_based_search) do
|
||||
%Plausible.Site{} = site ->
|
||||
{:ok, site}
|
||||
|
||||
nil ->
|
||||
{:error, :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_site_access(api_key, site) do
|
||||
is_member? = Sites.is_member?(api_key.user_id, site)
|
||||
is_super_admin? = Auth.is_super_admin?(api_key.user_id)
|
||||
|
||||
cond do
|
||||
is_super_admin? ->
|
||||
:ok
|
||||
|
||||
Sites.locked?(site) ->
|
||||
{:error, :site_locked}
|
||||
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(api_key.user) !== :ok ->
|
||||
{:error, :upgrade_required}
|
||||
|
||||
is_member? ->
|
||||
:ok
|
||||
|
||||
true ->
|
||||
{:error, :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp send_error(conn, _, {:error, :missing_api_key}) do
|
||||
H.unauthorized(
|
||||
conn,
|
||||
"Missing API key. Please use a valid Plausible API key as a Bearer Token."
|
||||
)
|
||||
end
|
||||
|
||||
defp send_error(conn, "stats:read:" <> _, {:error, :invalid_api_key}) do
|
||||
H.unauthorized(
|
||||
conn,
|
||||
"Invalid API key or site ID. Please make sure you're using a valid API key with access to the site you've requested."
|
||||
)
|
||||
end
|
||||
|
||||
defp send_error(conn, _, {:error, :invalid_api_key}) do
|
||||
H.unauthorized(
|
||||
conn,
|
||||
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
||||
)
|
||||
end
|
||||
|
||||
defp send_error(conn, _, {:error, :rate_limit, limit}) do
|
||||
H.too_many_requests(
|
||||
conn,
|
||||
"Too many API requests. Your API key is limited to #{limit} requests per hour. Please contact us to request more capacity."
|
||||
)
|
||||
end
|
||||
|
||||
defp send_error(conn, _, {:error, :missing_site_id}) do
|
||||
H.bad_request(
|
||||
conn,
|
||||
"Missing site ID. Please provide the required site_id parameter with your request."
|
||||
)
|
||||
end
|
||||
|
||||
defp send_error(conn, _, {:error, :upgrade_required}) do
|
||||
H.payment_required(
|
||||
conn,
|
||||
"The account that owns this API key does not have access to Stats API. Please make sure you're using the API key of a subscriber account and that the subscription plan includes Stats API"
|
||||
)
|
||||
end
|
||||
|
||||
defp send_error(conn, _, {:error, :site_locked}) do
|
||||
H.payment_required(
|
||||
conn,
|
||||
"This Plausible site is locked due to missing active subscription. In order to access it, the site owner should subscribe to a suitable plan"
|
||||
)
|
||||
end
|
||||
end
|
@ -1,115 +0,0 @@
|
||||
defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||
import Plug.Conn
|
||||
use Plausible.Repo
|
||||
alias Plausible.Auth
|
||||
alias Plausible.Sites
|
||||
alias Plausible.RateLimit
|
||||
alias PlausibleWeb.Api.Helpers, as: H
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
with {:ok, token} <- get_bearer_token(conn),
|
||||
{:ok, api_key} <- Auth.find_api_key(token),
|
||||
:ok <- check_api_key_rate_limit(api_key),
|
||||
{:ok, site} <- verify_access(api_key, conn.params["site_id"]) do
|
||||
Plausible.OpenTelemetry.add_site_attributes(site)
|
||||
site = Plausible.Imported.load_import_data(site)
|
||||
assign(conn, :site, site)
|
||||
else
|
||||
{:error, :missing_api_key} ->
|
||||
H.unauthorized(
|
||||
conn,
|
||||
"Missing API key. Please use a valid Plausible API key as a Bearer Token."
|
||||
)
|
||||
|
||||
{:error, :missing_site_id} ->
|
||||
H.bad_request(
|
||||
conn,
|
||||
"Missing site ID. Please provide the required site_id parameter with your request."
|
||||
)
|
||||
|
||||
{:error, :rate_limit, limit} ->
|
||||
H.too_many_requests(
|
||||
conn,
|
||||
"Too many API requests. Your API key is limited to #{limit} requests per hour. Please contact us to request more capacity."
|
||||
)
|
||||
|
||||
{:error, :invalid_api_key} ->
|
||||
H.unauthorized(
|
||||
conn,
|
||||
"Invalid API key or site ID. Please make sure you're using a valid API key with access to the site you've requested."
|
||||
)
|
||||
|
||||
{:error, :upgrade_required} ->
|
||||
H.payment_required(
|
||||
conn,
|
||||
"The account that owns this API key does not have access to Stats API. Please make sure you're using the API key of a subscriber account and that the subscription plan includes Stats API"
|
||||
)
|
||||
|
||||
{:error, :site_locked} ->
|
||||
H.payment_required(
|
||||
conn,
|
||||
"This Plausible site is locked due to missing active subscription. In order to access it, the site owner should subscribe to a suitable plan"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_access(_api_key, nil), do: {:error, :missing_site_id}
|
||||
|
||||
defp verify_access(api_key, site_id) do
|
||||
domain_based_search =
|
||||
from s in Plausible.Site, where: s.domain == ^site_id or s.domain_changed_from == ^site_id
|
||||
|
||||
case Repo.one(domain_based_search) do
|
||||
%Plausible.Site{} = site ->
|
||||
is_member? = Sites.is_member?(api_key.user_id, site)
|
||||
is_super_admin? = Plausible.Auth.is_super_admin?(api_key.user_id)
|
||||
|
||||
cond do
|
||||
is_super_admin? ->
|
||||
{:ok, site}
|
||||
|
||||
Sites.locked?(site) ->
|
||||
{:error, :site_locked}
|
||||
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(api_key.user) !== :ok ->
|
||||
{:error, :upgrade_required}
|
||||
|
||||
is_member? ->
|
||||
{:ok, site}
|
||||
|
||||
true ->
|
||||
{:error, :invalid_api_key}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_bearer_token(conn) do
|
||||
authorization_header =
|
||||
Plug.Conn.get_req_header(conn, "authorization")
|
||||
|> List.first()
|
||||
|
||||
case authorization_header do
|
||||
"Bearer " <> token -> {:ok, String.trim(token)}
|
||||
_ -> {:error, :missing_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
@one_hour 60 * 60 * 1000
|
||||
defp check_api_key_rate_limit(api_key) do
|
||||
case RateLimit.check_rate(
|
||||
"api_request:#{api_key.id}",
|
||||
@one_hour,
|
||||
api_key.hourly_request_limit
|
||||
) do
|
||||
{:allow, _} -> :ok
|
||||
{:deny, _} -> {:error, :rate_limit, api_key.hourly_request_limit}
|
||||
end
|
||||
end
|
||||
end
|
@ -166,8 +166,8 @@ defmodule PlausibleWeb.Router do
|
||||
get "/:domain/suggestions/:filter_name", StatsController, :filter_suggestions
|
||||
end
|
||||
|
||||
scope "/api/v1/stats", PlausibleWeb.Api do
|
||||
pipe_through [:public_api, PlausibleWeb.AuthorizeStatsApiPlug]
|
||||
scope "/api/v1/stats", PlausibleWeb.Api, assigns: %{api_scope: "stats:read:*"} do
|
||||
pipe_through [:public_api, PlausibleWeb.Plugs.AuthorizePublicAPI]
|
||||
|
||||
get "/realtime/visitors", ExternalStatsController, :realtime_visitors
|
||||
get "/aggregate", ExternalStatsController, :aggregate
|
||||
@ -175,23 +175,32 @@ defmodule PlausibleWeb.Router do
|
||||
get "/timeseries", ExternalStatsController, :timeseries
|
||||
end
|
||||
|
||||
scope "/api/v2", PlausibleWeb.Api do
|
||||
pipe_through [:public_api, PlausibleWeb.AuthorizeStatsApiPlug]
|
||||
scope "/api/v2", PlausibleWeb.Api, assigns: %{api_scope: "stats:read:*"} do
|
||||
pipe_through [:public_api, PlausibleWeb.Plugs.AuthorizePublicAPI]
|
||||
|
||||
post "/query", ExternalQueryApiController, :query
|
||||
end
|
||||
|
||||
on_ee do
|
||||
scope "/api/v1/sites", PlausibleWeb.Api do
|
||||
pipe_through [:public_api, PlausibleWeb.AuthorizeSitesApiPlug]
|
||||
pipe_through :public_api
|
||||
|
||||
post "/", ExternalSitesController, :create_site
|
||||
put "/shared-links", ExternalSitesController, :find_or_create_shared_link
|
||||
put "/goals", ExternalSitesController, :find_or_create_goal
|
||||
delete "/goals/:goal_id", ExternalSitesController, :delete_goal
|
||||
get "/:site_id", ExternalSitesController, :get_site
|
||||
put "/:site_id", ExternalSitesController, :update_site
|
||||
delete "/:site_id", ExternalSitesController, :delete_site
|
||||
scope assigns: %{api_scope: "sites:read:*"} do
|
||||
pipe_through PlausibleWeb.Plugs.AuthorizePublicAPI
|
||||
|
||||
get "/:site_id", ExternalSitesController, :get_site
|
||||
end
|
||||
|
||||
scope assigns: %{api_scope: "sites:provision:*"} do
|
||||
pipe_through PlausibleWeb.Plugs.AuthorizePublicAPI
|
||||
|
||||
post "/", ExternalSitesController, :create_site
|
||||
put "/shared-links", ExternalSitesController, :find_or_create_shared_link
|
||||
put "/goals", ExternalSitesController, :find_or_create_goal
|
||||
delete "/goals/:goal_id", ExternalSitesController, :delete_goal
|
||||
put "/:site_id", ExternalSitesController, :update_site
|
||||
delete "/:site_id", ExternalSitesController, :delete_site
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -509,6 +509,17 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||
assert json_response(conn, 200) == %{"domain" => new_domain, "timezone" => site.timezone}
|
||||
end
|
||||
|
||||
test "get a site with basic scope config", %{conn: conn, user: user, site: site} do
|
||||
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert json_response(conn, 200) == %{"domain" => site.domain, "timezone" => site.timezone}
|
||||
end
|
||||
|
||||
test "is 404 when site cannot be found", %{conn: conn} do
|
||||
conn = get(conn, "/api/v1/sites/foobar.baz")
|
||||
|
||||
|
235
test/plausible_web/plugs/authorize_public_api_test.exs
Normal file
235
test/plausible_web/plugs/authorize_public_api_test.exs
Normal file
@ -0,0 +1,235 @@
|
||||
defmodule PlausibleWeb.Plugs.AuthorizePublicAPITest do
|
||||
use PlausibleWeb.ConnCase, async: false
|
||||
|
||||
alias PlausibleWeb.Plugs.AuthorizePublicAPI
|
||||
|
||||
setup %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_private(PlausibleWeb.FirstLaunchPlug, :skip)
|
||||
|> bypass_through(PlausibleWeb.Router)
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
|
||||
test "halts with error when bearer token is missing", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "stats:read:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
assert conn.halted
|
||||
assert json_response(conn, 401)["error"] =~ "Missing API key."
|
||||
end
|
||||
|
||||
test "halts with error when bearer token is invalid against read-only Stats API", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer invalid")
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "stats:read:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
assert conn.halted
|
||||
assert json_response(conn, 401)["error"] =~ "Invalid API key or site ID."
|
||||
end
|
||||
|
||||
test "halts with error when bearer token is invalid", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer invalid")
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "sites:provision:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
assert conn.halted
|
||||
assert json_response(conn, 401)["error"] =~ "Invalid API key."
|
||||
end
|
||||
|
||||
test "halts with error on missing site ID when request made to Stats API", %{conn: conn} do
|
||||
api_key = insert(:api_key, user: build(:user))
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "stats:read:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
assert conn.halted
|
||||
assert json_response(conn, 400)["error"] =~ "Missing site ID."
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "halts with error when upgrade is required", %{conn: conn} do
|
||||
user = insert(:user, trial_expiry_date: nil)
|
||||
site = insert(:site, members: [user])
|
||||
api_key = insert(:api_key, user: user)
|
||||
|
||||
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, 402)["error"] =~
|
||||
"The account that owns this API key does not have access"
|
||||
end
|
||||
|
||||
test "halts with error when site is locked", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
site = insert(:site, members: [user], locked: true)
|
||||
api_key = insert(:api_key, user: user)
|
||||
|
||||
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, 402)["error"] =~ "This Plausible site is locked"
|
||||
end
|
||||
|
||||
test "halts with error when site ID is invalid", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
_site = insert(:site, members: [user])
|
||||
api_key = insert(:api_key, user: user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/", %{"site_id" => "invalid.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
|
||||
|
||||
test "halts with error when API key owner does not have access to the requested site", %{
|
||||
conn: conn
|
||||
} do
|
||||
user = insert(:user)
|
||||
site = insert(:site)
|
||||
api_key = insert(:api_key, user: user)
|
||||
|
||||
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
|
||||
|
||||
test "halts with error when API lacks required scope", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
api_key = insert(:api_key, user: user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "sites:provision:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
assert conn.halted
|
||||
assert json_response(conn, 401)["error"] =~ "Invalid API key."
|
||||
end
|
||||
|
||||
test "halts with error when API rate limit hit", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
api_key = insert(:api_key, user: user, hourly_request_limit: 1)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "sites:read:*")
|
||||
|
||||
first_resp = AuthorizePublicAPI.call(conn, nil)
|
||||
second_resp = AuthorizePublicAPI.call(conn, nil)
|
||||
|
||||
refute first_resp.halted
|
||||
assert second_resp.halted
|
||||
assert json_response(second_resp, 429)["error"] =~ "Too many API requests."
|
||||
end
|
||||
|
||||
test "passes and sets current user when valid API key with required scope provided", %{
|
||||
conn: conn
|
||||
} do
|
||||
user = insert(:user)
|
||||
api_key = insert(:api_key, user: user, scopes: ["sites:provision:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "sites:provision:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
refute conn.halted
|
||||
assert conn.assigns.current_user.id == user.id
|
||||
end
|
||||
|
||||
test "passes and sets current user and site when valid API key and site ID provided", %{
|
||||
conn: conn
|
||||
} do
|
||||
user = insert(:user)
|
||||
site = insert(:site, members: [user])
|
||||
api_key = insert(:api_key, user: user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/", %{"site_id" => site.domain})
|
||||
|> assign(:api_scope, "stats:read:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
refute conn.halted
|
||||
assert conn.assigns.current_user.id == user.id
|
||||
assert conn.assigns.site.id == 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 = insert(:user)
|
||||
patch_env(:super_admin_user_ids, [user.id])
|
||||
site = insert(:site, locked: true)
|
||||
api_key = insert(:api_key, user: user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/", %{"site_id" => site.domain})
|
||||
|> assign(:api_scope, "stats:read:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
refute conn.halted
|
||||
assert conn.assigns.current_user.id == user.id
|
||||
assert conn.assigns.site.id == site.id
|
||||
end
|
||||
|
||||
test "passes for subscope match", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
api_key = insert(:api_key, user: user, scopes: ["funnels:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/")
|
||||
|> assign(:api_scope, "funnels:read:*")
|
||||
|> AuthorizePublicAPI.call(nil)
|
||||
|
||||
refute conn.halted
|
||||
assert conn.assigns.current_user.id == user.id
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user