feat: No rate limit API key (#10515)

* feat: No rate limit API key

* Refactor, add tests

* Add tests
vb-nft-collection-trigger-metadata-refetch-admin-api-endpoint
Victor Baranov 4 months ago committed by GitHub
parent 0f7aba6337
commit 678b544d7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 80
      apps/block_scout_web/lib/block_scout_web/views/access_helper.ex
  2. 70
      apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs
  3. 1
      config/runtime.exs
  4. 1
      docker-compose/envs/common-blockscout.env

@ -53,13 +53,23 @@ defmodule BlockScoutWeb.AccessHelper do
|> Conn.halt()
end
@doc """
Checks, if rate limit reached before making a new request. It is applied to GraphQL API.
"""
@spec check_rate_limit(Plug.Conn.t(), list()) :: :ok | :rate_limit_reached | true | false
def check_rate_limit(conn, graphql?: true) do
rate_limit_config = Application.get_env(:block_scout_web, Api.GraphQL)
no_rate_limit_api_key = rate_limit_config[:no_rate_limit_api_key]
if rate_limit_config[:rate_limit_disabled?] do
:ok
else
check_graphql_rate_limit_inner(conn, rate_limit_config)
cond do
rate_limit_config[:rate_limit_disabled?] ->
:ok
check_no_rate_limit_api_key(conn, no_rate_limit_api_key) ->
:ok
true ->
check_graphql_rate_limit_inner(conn, rate_limit_config)
end
end
@ -74,31 +84,48 @@ defmodule BlockScoutWeb.AccessHelper do
ip_string = conn_to_ip_string(conn)
plan = get_plan(conn.query_params)
user_api_key = get_api_key(conn)
cond do
check_api_key(conn) && get_api_key(conn) == static_api_key ->
rate_limit(static_api_key, limit_by_key, time_interval_limit)
check_api_key(conn) && user_api_key == static_api_key ->
rate_limit(static_api_key, time_interval_limit, limit_by_key)
check_api_key(conn) && !is_nil(plan) ->
conn
|> get_api_key()
|> rate_limit(min(plan.max_req_per_second, limit_by_key), time_interval_limit)
rate_limit(user_api_key, time_interval_limit, min(plan.max_req_per_second, limit_by_key))
true ->
rate_limit("graphql_#{ip_string}", limit_by_ip, time_interval_by_ip) == :ok &&
rate_limit("graphql", global_limit, time_interval_limit) == :ok
rate_limit("graphql", time_interval_limit, global_limit) == :ok
end
end
@doc """
Checks, if rate limit reached before making a new request. It is applied to API v1, ETH RPC API.
"""
@spec check_rate_limit(Plug.Conn.t()) :: :ok | :rate_limit_reached
def check_rate_limit(conn) do
rate_limit_config = Application.get_env(:block_scout_web, :api_rate_limit)
no_rate_limit_api_key = rate_limit_config[:no_rate_limit_api_key]
if rate_limit_config[:disabled] do
:ok
else
check_rate_limit_inner(conn, rate_limit_config)
cond do
rate_limit_config[:disabled] ->
:ok
check_no_rate_limit_api_key(conn, no_rate_limit_api_key) ->
:ok
true ->
check_rate_limit_inner(conn, rate_limit_config)
end
end
defp check_no_rate_limit_api_key(conn, no_rate_limit_api_key) do
user_api_key = get_api_key(conn)
check_api_key(conn) && !is_nil(user_api_key) && String.trim(user_api_key) !== "" &&
user_api_key == no_rate_limit_api_key
end
# credo:disable-for-next-line /Complexity/
defp check_rate_limit_inner(conn, rate_limit_config) do
global_limit = rate_limit_config[:global_limit]
@ -117,26 +144,26 @@ defmodule BlockScoutWeb.AccessHelper do
user_agent = get_user_agent(conn)
user_api_key = get_api_key(conn)
cond do
check_api_key(conn) && get_api_key(conn) == static_api_key ->
rate_limit(static_api_key, limit_by_key, time_interval_limit)
check_api_key(conn) && user_api_key == static_api_key ->
rate_limit(static_api_key, time_interval_limit, limit_by_key)
check_api_key(conn) && !is_nil(plan) ->
conn
|> get_api_key()
|> rate_limit(plan.max_req_per_second, time_interval_limit)
rate_limit(user_api_key, time_interval_limit, plan.max_req_per_second)
Enum.member?(whitelisted_ips(rate_limit_config), ip_string) ->
rate_limit(ip_string, limit_by_whitelisted_ip, time_interval_limit)
rate_limit(ip_string, time_interval_limit, limit_by_whitelisted_ip)
api_v2_request?(conn) && !is_nil(token) && !is_nil(user_agent) ->
rate_limit(token, api_v2_ui_limit, time_interval_limit)
rate_limit(token, time_interval_limit, api_v2_ui_limit)
api_v2_request?(conn) && !is_nil(user_agent) ->
rate_limit(ip_string, limit_by_ip, time_interval_by_ip)
rate_limit(ip_string, time_interval_by_ip, limit_by_ip)
true ->
rate_limit("api", global_limit, time_interval_limit)
rate_limit("api", time_interval_limit, global_limit)
end
end
@ -156,11 +183,8 @@ defmodule BlockScoutWeb.AccessHelper do
end
end
defp rate_limit(key, limit, time_interval) do
rate_limit_inner(key, time_interval, limit)
end
defp rate_limit_inner(key, time_interval, limit) do
@spec rate_limit(String.t(), integer(), integer()) :: :ok | :rate_limit_reached
defp rate_limit(key, time_interval, limit) do
case Hammer.check_rate(key, time_interval, limit) do
{:allow, _count} ->
:ok

@ -0,0 +1,70 @@
defmodule BlockScoutWeb.AccessHelperTest do
alias BlockScoutWeb.AccessHelper
use BlockScoutWeb.ConnCase
import Mox
setup :verify_on_exit!
setup do
configuration = Application.get_env(:block_scout_web, :api_rate_limit)
on_exit(fn ->
Application.put_env(:block_scout_web, :api_rate_limit, configuration)
end)
:ok
end
describe "check_rate_limit/1" do
test "rate_limit_disabled", %{conn: conn} do
Application.put_env(:block_scout_web, :api_rate_limit,
global_limit: 0,
limit_by_key: 0,
limit_by_whitelisted_ip: 0,
time_interval_limit: 1_000,
disabled: true
)
assert AccessHelper.check_rate_limit(conn) == :ok
end
test "no_rate_limit_api_key", %{conn: conn} do
Application.put_env(:block_scout_web, :api_rate_limit,
global_limit: 0,
limit_by_key: 0,
limit_by_whitelisted_ip: 0,
time_interval_limit: 1_000,
no_rate_limit_api_key: "123"
)
conn = %{conn | query_params: %{"apikey" => "123"}}
assert AccessHelper.check_rate_limit(conn) == :ok
end
test "rate limit, if no_rate_limit_api_key is nil", %{conn: conn} do
Application.put_env(:block_scout_web, :api_rate_limit,
global_limit: 0,
limit_by_key: 0,
limit_by_whitelisted_ip: 0,
time_interval_limit: 1_000,
no_rate_limit_api_key: nil
)
conn = %{conn | query_params: %{"apikey" => nil}}
assert AccessHelper.check_rate_limit(conn) == :rate_limit_reached
end
test "rate limit, if no_rate_limit_api_key is empty", %{conn: conn} do
Application.put_env(:block_scout_web, :api_rate_limit,
global_limit: 0,
limit_by_key: 0,
limit_by_whitelisted_ip: 0,
time_interval_limit: 1_000,
no_rate_limit_api_key: ""
)
conn = %{conn | query_params: %{"apikey" => " "}}
assert AccessHelper.check_rate_limit(conn) == :rate_limit_reached
end
end
end

@ -105,6 +105,7 @@ config :block_scout_web, :api_rate_limit,
limit_by_ip: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_IP", 3000),
time_interval_limit_by_ip: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_IP_TIME_INTERVAL", "5m"),
static_api_key: System.get_env("API_RATE_LIMIT_STATIC_API_KEY"),
no_rate_limit_api_key: System.get_env("API_NO_RATE_LIMIT_API_KEY"),
whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS"),
is_blockscout_behind_proxy: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY"),
api_v2_ui_limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN", 5),

@ -131,6 +131,7 @@ API_RATE_LIMIT_WHITELISTED_IPS=
API_RATE_LIMIT_STATIC_API_KEY=
API_RATE_LIMIT_UI_V2_WITH_TOKEN=5
API_RATE_LIMIT_BY_IP=3000
API_NO_RATE_LIMIT_API_KEY=
# API_GRAPHQL_ENABLED=
# API_GRAPHQL_MAX_COMPLEXITY=
# API_GRAPHQL_TOKEN_LIMIT=

Loading…
Cancel
Save