Skip to content

Commit

Permalink
Merge 91f1e93 into b9582f1
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielpra1 committed Feb 14, 2020
2 parents b9582f1 + 91f1e93 commit ea87f88
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 0 deletions.
3 changes: 3 additions & 0 deletions lib/authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ defmodule Rajska.Authorization do

@callback get_current_user(context) :: current_user

@callback get_ip(context) :: String.t()

@callback get_user_role(current_user) :: role

@callback not_scoped_roles() :: list(role)
Expand All @@ -29,6 +31,7 @@ defmodule Rajska.Authorization do
@callback context_user_authorized?(context, scoped_struct, rule) :: boolean()

@optional_callbacks get_current_user: 1,
get_ip: 1,
get_user_role: 1,
not_scoped_roles: 0,
role_authorized?: 2,
Expand Down
31 changes: 31 additions & 0 deletions lib/middlewares/rate_limiter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule Rajska.RateLimiter do
@moduledoc """
Rate limiter absinthe middleware.
"""
@behaviour Absinthe.Middleware

alias Absinthe.Resolution

def call(%Resolution{state: :resolved} = resolution, _config), do: resolution

def call(%Resolution{} = resolution, config) do
scale_ms = Keyword.get(config, :scale_ms, 60_000)
limit = Keyword.get(config, :limit, 10)
identifier = get_identifier(resolution, config[:keys], config[:id])
error_msg = Keyword.get(config, :error_msg, "Too many requests")

case Hammer.check_rate("query:#{identifier}", scale_ms, limit) do
{:allow, _count} -> resolution
{:deny, _limit} -> Resolution.put_result(resolution, {:error, error_msg})
end
end

defp get_identifier(%Resolution{context: context, arguments: arguments}, keys, id) do
case {keys, id} do
{nil, nil} -> Rajska.apply_auth_mod(context, :get_ip, [context])
{keys, nil} -> get_in(arguments, List.wrap(keys)) || raise "Invalid configuration in Rate Limiter. Key not found in arguments."
{nil, id} -> id
{_key, _id} -> raise "Invalid configuration in Rate Limiter. If key is defined, then id must not be defined"
end
end
end
2 changes: 2 additions & 0 deletions lib/rajska.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ defmodule Rajska do

def get_current_user(%{current_user: current_user}), do: current_user

def get_ip(%{ip: ip}), do: ip

def get_user_role(%{role: role}), do: role
def get_user_role(nil), do: nil

Expand Down
2 changes: 2 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ defmodule Rajska.MixProject do
{:credo, "~> 1.1.0", only: [:dev, :test], runtime: false},
{:absinthe, "~> 1.4.0"},
{:excoveralls, "~> 0.11", only: :test},
{:hammer, "~> 6.0", optional: true},
{:mock, "~> 0.3.0", only: :test},
]
end

Expand Down
4 changes: 4 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.12.0", "50e17a1b116fdb7facc2fe127a94db246169f38d7627b391376a0bc418413ce1", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"hammer": {:hex, :hammer, "6.0.0", "72ec6fff10e9d63856968988a22ee04c4d6d5248071ddccfbda50aa6c455c1d7", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.4", "c5862eb3b8c64237f45f586cf00c9d892ba07bb48305a43319d428ce3c2897dd", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
}
128 changes: 128 additions & 0 deletions test/middlewares/rate_limiter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
defmodule Rajska.RateLimiterTest do
use ExUnit.Case, async: false

import Mock

defmodule Authorization do
use Rajska,
valid_roles: [:user, :admin],
super_role: :admin
end

defmodule Schema do
use Absinthe.Schema

def context(ctx), do: Map.put(ctx, :authorization, Authorization)

input_object :keys_params do
field :id, :string
end

query do
field :default_config, :string do
middleware Rajska.RateLimiter
resolve fn _, _ -> {:ok, "ok"} end
end

field :scale_limit, :string do
middleware Rajska.RateLimiter, scale_ms: 30_000, limit: 5
resolve fn _, _ -> {:ok, "ok"} end
end

field :id, :string do
middleware Rajska.RateLimiter, id: :custom_id
resolve fn _, _ -> {:ok, "ok"} end
end

field :key, :string do
arg :id, :string

middleware Rajska.RateLimiter, keys: :id
resolve fn _, _ -> {:ok, "ok"} end
end

field :keys, :string do
arg :params, :keys_params
middleware Rajska.RateLimiter, keys: [:params, :id]
resolve fn _, _ -> {:ok, "ok"} end
end

field :id_and_key, :string do
middleware Rajska.RateLimiter, id: :id, keys: :keys
resolve fn _, _ -> {:ok, "ok"} end
end

field :error_msg, :string do
middleware Rajska.RateLimiter, error_msg: "Rate limit exceeded"
resolve fn _, _ -> {:ok, "ok"} end
end
end
end

@default_context [context: %{ip: "ip"}]

setup_with_mocks([{Hammer, [], [check_rate: fn _a, _b, _c -> {:allow, 1} end]}]) do
:ok
end

test "works with default configs" do
{:ok, _} = Absinthe.run(query(:default_config), __MODULE__.Schema, @default_context)
assert_called Hammer.check_rate("query:ip", 60_000, 10)
end

test "accepts scale and limit configuration" do
{:ok, _} = Absinthe.run(query(:scale_limit), __MODULE__.Schema, @default_context)
assert_called Hammer.check_rate("query:ip", 30_000, 5)
end

test "accepts id configuration" do
{:ok, _} = Absinthe.run(query(:id), __MODULE__.Schema, @default_context)
assert_called Hammer.check_rate("query:custom_id", 60_000, 10)
end

test "accepts key configuration" do
{:ok, _} = Absinthe.run(query(:key, :id, "id_key"), __MODULE__.Schema, @default_context)
assert_called Hammer.check_rate("query:id_key", 60_000, 10)
end

test "throws error if key is not present" do
assert_raise RuntimeError, ~r/Invalid configuration in Rate Limiter. Key not found in arguments./, fn ->
Absinthe.run(query(:key), __MODULE__.Schema, @default_context)
end
end

test "accepts key configuration for nested parameters" do
{:ok, _} = Absinthe.run(query(:keys, :params, %{id: "id_key"}), __MODULE__.Schema, @default_context)
assert_called Hammer.check_rate("query:id_key", 60_000, 10)
end

test "throws error when id and key are provided as configuration" do
assert_raise RuntimeError, ~r/Invalid configuration in Rate Limiter. If key is defined, then id must not be defined/, fn ->
Absinthe.run(query(:id_and_key), __MODULE__.Schema, @default_context)
end
end

test "accepts error msg configuration" do
with_mock Hammer, [check_rate: fn _a, _b, _c -> {:deny, 1} end] do
assert {:ok, %{errors: errors}} = Absinthe.run(query(:error_msg), __MODULE__.Schema, @default_context)
assert [
%{
locations: [%{column: 0, line: 1}],
message: "Rate limit exceeded",
path: ["error_msg"]
}
] == errors
end
end

defp query(name), do: "{ #{name} }"
defp query(name, key, value) when is_binary(value), do: "{ #{name}(#{key}: \"#{value}\") }"
defp query(name, key, %{} = value), do: "{ #{name}(#{key}: {#{build_arguments(value)}}) }"

defp build_arguments(arguments) do
arguments
|> Enum.map(fn {k, v} -> if is_nil(v), do: nil, else: "#{k}: #{inspect(v, [charlists: :as_lists])}" end)
|> Enum.reject(&is_nil/1)
|> Enum.join(", ")
end
end

0 comments on commit ea87f88

Please sign in to comment.