From 4ea70aeedaca3cf38a711f38e4977ac98b5d1a57 Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Mon, 19 Feb 2018 15:06:51 -0800 Subject: [PATCH 1/7] pr fixes --- test/paginator_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/paginator_test.exs b/test/paginator_test.exs index 2006edd..9536675 100644 --- a/test/paginator_test.exs +++ b/test/paginator_test.exs @@ -290,12 +290,12 @@ defmodule PaginatorTest do payments: {_p1, _p2, _p3, _p4, _p5, p6, _p7, _p8, _p9, _p10, _p11, _p12} } do assert %Page{entries: [], metadata: _metadata} = - customer_payments_by_amount(c1) - |> Repo.paginate( - cursor_fields: [:amount, :charged_at, :id], - before: encode_cursor([p6.amount, p6.charged_at, p6.id]), - limit: 1 - ) + customer_payments_by_amount(c1) + |> Repo.paginate( + cursor_fields: [:amount, :charged_at, :id], + before: encode_cursor([p6.amount, p6.charged_at, p6.id]), + limit: 1 + ) end end From fd422876228192b6e2417c3691753d896c15e17b Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Mon, 19 Feb 2018 15:09:51 -0800 Subject: [PATCH 2/7] mix format change --- test/paginator_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/paginator_test.exs b/test/paginator_test.exs index 9536675..2006edd 100644 --- a/test/paginator_test.exs +++ b/test/paginator_test.exs @@ -290,12 +290,12 @@ defmodule PaginatorTest do payments: {_p1, _p2, _p3, _p4, _p5, p6, _p7, _p8, _p9, _p10, _p11, _p12} } do assert %Page{entries: [], metadata: _metadata} = - customer_payments_by_amount(c1) - |> Repo.paginate( - cursor_fields: [:amount, :charged_at, :id], - before: encode_cursor([p6.amount, p6.charged_at, p6.id]), - limit: 1 - ) + customer_payments_by_amount(c1) + |> Repo.paginate( + cursor_fields: [:amount, :charged_at, :id], + before: encode_cursor([p6.amount, p6.charged_at, p6.id]), + limit: 1 + ) end end From 867ce6cf080a4be1b349aa202f8a4b49870227e1 Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Sun, 18 Feb 2018 06:43:37 -0800 Subject: [PATCH 3/7] move cursor to module --- lib/paginator.ex | 6 +++--- lib/paginator/config.ex | 13 ++++++++---- lib/paginator/cursor.ex | 21 ++------------------ lib/paginator/cursors/unencrypted_cursor.ex | 22 +++++++++++++++++++++ test/paginator/config_test.exs | 3 ++- test/paginator_test.exs | 2 +- 6 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 lib/paginator/cursors/unencrypted_cursor.ex diff --git a/lib/paginator.ex b/lib/paginator.ex index 7bec24b..3c94982 100644 --- a/lib/paginator.ex +++ b/lib/paginator.ex @@ -32,7 +32,7 @@ defmodule Paginator do import Ecto.Query - alias Paginator.{Config, Cursor, Ecto.Query, Page, Page.Metadata} + alias Paginator.{Config, Ecto.Query, Page, Page.Metadata} defmacro __using__(opts) do quote do @@ -153,10 +153,10 @@ defmodule Paginator do end end - defp fetch_cursor_value(schema, %Config{cursor_fields: cursor_fields}) do + defp fetch_cursor_value(schema, %Config{cursor_fields: cursor_fields, cursor_module: cursor_module}) do cursor_fields |> Enum.map(fn field -> Map.get(schema, field) end) - |> Cursor.encode() + |> cursor_module.encode() end defp first_page?(sorted_entries, %Config{limit: limit}) do diff --git a/lib/paginator/config.ex b/lib/paginator/config.ex index e710f17..727aa38 100644 --- a/lib/paginator/config.ex +++ b/lib/paginator/config.ex @@ -1,8 +1,6 @@ defmodule Paginator.Config do @moduledoc false - alias Paginator.Cursor - @type t :: %__MODULE__{} defstruct [ @@ -11,6 +9,7 @@ defmodule Paginator.Config do :before, :before_values, :cursor_fields, + :cursor_module, :include_total_count, :limit, :maximum_limit, @@ -22,14 +21,16 @@ defmodule Paginator.Config do @minimum_limit 1 @maximum_limit 500 @default_total_count_limit 10_000 + @default_cursor_module Paginator.Cursors.UnencryptedCursor def new(opts \\ []) do %__MODULE__{ after: opts[:after], - after_values: Cursor.decode(opts[:after]), + after_values: cursor_module(opts).decode(opts[:after]), before: opts[:before], - before_values: Cursor.decode(opts[:before]), + before_values: cursor_module(opts).decode(opts[:before]), cursor_fields: opts[:cursor_fields], + cursor_module: cursor_module(opts), include_total_count: opts[:include_total_count] || false, limit: limit(opts), sort_direction: opts[:sort_direction] || :asc, @@ -37,6 +38,10 @@ defmodule Paginator.Config do } end + defp cursor_module(opts) do + opts[:cursor_module] || @default_cursor_module + end + defp limit(opts) do max(opts[:limit] || @default_limit, @minimum_limit) |> min(opts[:maximum_limit] || @maximum_limit) diff --git a/lib/paginator/cursor.ex b/lib/paginator/cursor.ex index 630cdc6..601a356 100644 --- a/lib/paginator/cursor.ex +++ b/lib/paginator/cursor.ex @@ -1,21 +1,4 @@ defmodule Paginator.Cursor do - @moduledoc false - - def decode(nil), do: nil - - def decode(encoded_cursor) do - encoded_cursor - |> Base.url_decode64!() - |> :erlang.binary_to_term() - end - - def encode(values) when is_list(values) do - values - |> :erlang.term_to_binary() - |> Base.url_encode64() - end - - def encode(value) do - encode([value]) - end + @callback decode(String.t()) :: {:ok, term :: term} | {:error, term :: term} + @callback encode(arg :: term) :: {:ok, encoded :: String.t()} | {:error, String.t()} end diff --git a/lib/paginator/cursors/unencrypted_cursor.ex b/lib/paginator/cursors/unencrypted_cursor.ex new file mode 100644 index 0000000..ccd2665 --- /dev/null +++ b/lib/paginator/cursors/unencrypted_cursor.ex @@ -0,0 +1,22 @@ +defmodule Paginator.Cursors.UnencryptedCursor do + @behaviour Paginator.Cursor + @moduledoc false + + def decode(nil), do: nil + + def decode(encoded_cursor) do + encoded_cursor + |> Base.url_decode64!() + |> :erlang.binary_to_term() + end + + def encode(values) when is_list(values) do + values + |> :erlang.term_to_binary() + |> Base.url_encode64() + end + + def encode(value) do + encode([value]) + end +end diff --git a/test/paginator/config_test.exs b/test/paginator/config_test.exs index 5e0f7e6..2cc0ca6 100644 --- a/test/paginator/config_test.exs +++ b/test/paginator/config_test.exs @@ -1,7 +1,8 @@ defmodule Paginator.ConfigTest do use ExUnit.Case, async: true - alias Paginator.{Config, Cursor} + alias Paginator.Config + alias Paginator.Cursors.UnencryptedCursor, as: Cursor describe "Config.new/2" do test "creates a new config" do diff --git a/test/paginator_test.exs b/test/paginator_test.exs index 2006edd..6d10ce0 100644 --- a/test/paginator_test.exs +++ b/test/paginator_test.exs @@ -3,7 +3,7 @@ defmodule PaginatorTest do alias Calendar.DateTime, as: DT - alias Paginator.Cursor + alias Paginator.Cursors.UnencryptedCursor, as: Cursor setup :create_customers_and_payments From 4aa30409ceba0b00a039c18762a387e0b1359a5c Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Thu, 22 Feb 2018 22:16:28 -0800 Subject: [PATCH 4/7] cursor module wip --- lib/paginator.ex | 4 +- lib/paginator/config.ex | 6 ++- lib/paginator/cursor.ex | 7 +++- lib/paginator/cursors/unencrypted_cursor.ex | 14 ++++--- mix.exs | 3 +- mix.lock | 4 ++ test/paginator/config_test.exs | 44 +++++++++++++++++++++ test/paginator_test.exs | 44 +++++++++++++++++++++ test/support/encrypted_cursor.ex | 29 ++++++++++++++ 9 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 test/support/encrypted_cursor.ex diff --git a/lib/paginator.ex b/lib/paginator.ex index 3c94982..3fc32db 100644 --- a/lib/paginator.ex +++ b/lib/paginator.ex @@ -153,10 +153,10 @@ defmodule Paginator do end end - defp fetch_cursor_value(schema, %Config{cursor_fields: cursor_fields, cursor_module: cursor_module}) do + defp fetch_cursor_value(schema, %Config{cursor_fields: cursor_fields, cursor_module: cursor_module, cursor_module_opts: cursor_module_opts}) do cursor_fields |> Enum.map(fn field -> Map.get(schema, field) end) - |> cursor_module.encode() + |> cursor_module.encode(cursor_module_opts) end defp first_page?(sorted_entries, %Config{limit: limit}) do diff --git a/lib/paginator/config.ex b/lib/paginator/config.ex index 727aa38..be7baef 100644 --- a/lib/paginator/config.ex +++ b/lib/paginator/config.ex @@ -10,6 +10,7 @@ defmodule Paginator.Config do :before_values, :cursor_fields, :cursor_module, + :cursor_module_opts, :include_total_count, :limit, :maximum_limit, @@ -26,11 +27,12 @@ defmodule Paginator.Config do def new(opts \\ []) do %__MODULE__{ after: opts[:after], - after_values: cursor_module(opts).decode(opts[:after]), + after_values: cursor_module(opts).decode(opts[:after], opts[:cursor_module_opts]), before: opts[:before], - before_values: cursor_module(opts).decode(opts[:before]), + before_values: cursor_module(opts).decode(opts[:before], opts[:cursor_module_opts]), cursor_fields: opts[:cursor_fields], cursor_module: cursor_module(opts), + cursor_module_opts: opts[:cursor_module_opts] || [], include_total_count: opts[:include_total_count] || false, limit: limit(opts), sort_direction: opts[:sort_direction] || :asc, diff --git a/lib/paginator/cursor.ex b/lib/paginator/cursor.ex index 601a356..f5504ef 100644 --- a/lib/paginator/cursor.ex +++ b/lib/paginator/cursor.ex @@ -1,4 +1,7 @@ defmodule Paginator.Cursor do - @callback decode(String.t()) :: {:ok, term :: term} | {:error, term :: term} - @callback encode(arg :: term) :: {:ok, encoded :: String.t()} | {:error, String.t()} + @callback decode(String.t(), opts :: list()) :: term + @callback encode(arg :: term, opts :: list()) :: String.t() + @callback decode(String.t()) :: term + @callback encode(arg :: term) :: String.t() + @optional_callbacks encode: 1, decode: 1 end diff --git a/lib/paginator/cursors/unencrypted_cursor.ex b/lib/paginator/cursors/unencrypted_cursor.ex index ccd2665..bf02e64 100644 --- a/lib/paginator/cursors/unencrypted_cursor.ex +++ b/lib/paginator/cursors/unencrypted_cursor.ex @@ -2,21 +2,25 @@ defmodule Paginator.Cursors.UnencryptedCursor do @behaviour Paginator.Cursor @moduledoc false - def decode(nil), do: nil + def decode(encoded_cursor), do: decode(encoded_cursor, nil) - def decode(encoded_cursor) do + def decode(nil, _opts), do: nil + + def decode(encoded_cursor, _opts) do encoded_cursor |> Base.url_decode64!() |> :erlang.binary_to_term() end - def encode(values) when is_list(values) do + def encode(value), do: encode(value, nil) + + def encode(values, _opts) when is_list(values) do values |> :erlang.term_to_binary() |> Base.url_encode64() end - def encode(value) do - encode([value]) + def encode(value, opts) do + encode([value], opts) end end diff --git a/mix.exs b/mix.exs index 3c0cfa3..4a8efb3 100644 --- a/mix.exs +++ b/mix.exs @@ -44,7 +44,8 @@ defmodule Paginator.Mixfile do {:ex_doc, "~> 0.18", only: :dev, runtime: false}, {:ex_machina, "~> 2.1", only: :test}, {:inch_ex, "~> 0.5", only: [:dev, :test]}, - {:postgrex, "~> 0.13", optional: true} + {:postgrex, "~> 0.13", optional: true}, + {:plug, "~> 1.4", optional: true} ] end diff --git a/mix.lock b/mix.lock index 8a577dd..8d14afc 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [], [], "hexpm"}, "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, @@ -11,8 +12,11 @@ "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "plug": {:hex, :plug, "1.4.5", "7b13869283fff6b8b21b84b8735326cc012c5eef8607095dc6ee24bd0a273d8e", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.4", "f58e319c5451bfda86ba6a45ce6dca311193d0a9861323d0d16e8d02e25adc41", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/paginator/config_test.exs b/test/paginator/config_test.exs index 2cc0ca6..6074d5d 100644 --- a/test/paginator/config_test.exs +++ b/test/paginator/config_test.exs @@ -3,6 +3,7 @@ defmodule Paginator.ConfigTest do alias Paginator.Config alias Paginator.Cursors.UnencryptedCursor, as: Cursor + alias Paginator.Cursors.EncryptedCursor describe "Config.new/2" do test "creates a new config" do @@ -13,6 +14,13 @@ defmodule Paginator.ConfigTest do assert config.limit == 10 assert config.cursor_fields == [:id] end + + test "creates a new config with custom cursor_module" do + config = Config.new(cursor_fields: [:id], cursor_module: EncryptedCursor, cursor_module_opts: [encryption_key: "123", signing_key: "321"]) + + assert config.cursor_module == EncryptedCursor + assert config.cursor_module_opts == [encryption_key: "123", signing_key: "321"] + end end describe "Config.new/2 applies min/max limit" do @@ -82,4 +90,40 @@ defmodule Paginator.ConfigTest do def simple_before, do: Cursor.encode("pay_789") def complex_after, do: Cursor.encode(["2036-02-09T20:00:00.000Z", "pay_123"]) def complex_before, do: Cursor.encode(["2036-02-09T20:00:00.000Z", "pay_789"]) + + # describe "custom cursor_module" do + # defmodule Paginator.Cursors.EncryptedCursor do + # @behaviour Paginator.Cursor + # @moduledoc false + # + # def decode(_encryption_key, _signing_key, nil), do: nil + # + # def decode(encryption_key, signing_key, encrypted_cursor) do + # {:ok, binary} = MessageEncryptor.decrypt(encrypted_cursor, encryption_key, signing_key) + # Plug.Crypto.safe_binary_to_term(binary) + # end + # + # def encode(encryption_key, signing_key, values) when is_list(values) do + # encrypt_cursor(encryption_key, signing_key, values) + # end + # + # def encode(encryption_key, signing_key, value) do + # encode(encryption_key, signing_key, [value]) + # end + # + # defp encrypt_cursor(encryption_key, signing_key, term) do + # MessageEncryptor.encrypt( + # :erlang.term_to_binary(term), + # encryption_key, # encryption key + # signing_key # signing key + # ) + # end + # end + # + # test "test" do + # secret_key_base = "PwGqIq85ScEflB9/3SpHjEHb44uTMFVGP+vleZTSr63NvPkWwHIAtGzgjpLvjDbA" + # salt = "USERID-65dc-4740-bba1-51ebddd58f39" + # key = KeyGenerator.generate(secret_key_base, salt, [iterations: 1000, length: 32, digest: :sha256]) + # end + # end end diff --git a/test/paginator_test.exs b/test/paginator_test.exs index 6d10ce0..3e58b86 100644 --- a/test/paginator_test.exs +++ b/test/paginator_test.exs @@ -4,6 +4,8 @@ defmodule PaginatorTest do alias Calendar.DateTime, as: DT alias Paginator.Cursors.UnencryptedCursor, as: Cursor + alias Paginator.Cursors.EncryptedCursor + alias Plug.Crypto.KeyGenerator setup :create_customers_and_payments @@ -606,6 +608,48 @@ defmodule PaginatorTest do end end + describe "with custom encrypted cursor_module" do + test "encrypted cursor", %{ + customers: {c1, _c2, _c3}, + payments: {_p1, _p2, _p3, _p4, p5, p6, p7, p8, _p9, _p10, _p11, _p12} + } do + secret_key_base = "PwGqIq85ScEflB9/3SpHjEHb44uTMFVGP+vleZTSr63NvPkWwHIAtGzgjpLvjDbA" + salt = c1.id |> Integer.to_string + key = KeyGenerator.generate(secret_key_base, salt, [iterations: 1000, length: 32, digest: :sha256]) + + %Page{entries: entries, metadata: %{after: after_cursor}} = + customer_payments_by_amount(c1) + |> Repo.paginate( + cursor_fields: [:amount, :charged_at, :id], + sort_direction: :asc, + limit: 1, + cursor_module: EncryptedCursor, + cursor_module_opts: [ + encryption_key: key, + signing_key: key + ]) + + assert to_ids(entries) == to_ids([p6]) + + %Page{entries: entries} = + customer_payments_by_amount(c1) + |> Repo.paginate( + after: after_cursor, + cursor_fields: [:amount, :charged_at, :id], + sort_direction: :asc, + limit: 3, + cursor_module: EncryptedCursor, + cursor_module_opts: [ + encryption_key: key, + signing_key: key + ]) + + assert to_ids(entries) == to_ids([p5, p7, p8]) + end + + test ":error if decoding fails" + end + defp to_ids(entries), do: Enum.map(entries, & &1.id) defp create_customers_and_payments(_context) do diff --git a/test/support/encrypted_cursor.ex b/test/support/encrypted_cursor.ex new file mode 100644 index 0000000..5170bfa --- /dev/null +++ b/test/support/encrypted_cursor.ex @@ -0,0 +1,29 @@ +defmodule Paginator.Cursors.EncryptedCursor do + @behaviour Paginator.Cursor + @moduledoc false + + alias Plug.Crypto.MessageEncryptor + + def decode(nil, _opts), do: nil + + def decode(encrypted_cursor, opts) do + {:ok, binary} = MessageEncryptor.decrypt(encrypted_cursor, opts[:encryption_key], opts[:signing_key]) + Plug.Crypto.safe_binary_to_term(binary) + end + + def encode(values, opts) when is_list(values) do + encrypt_cursor(values, opts) + end + + def encode(value, opts) do + encode([value], opts) + end + + defp encrypt_cursor(term, opts) do + MessageEncryptor.encrypt( + :erlang.term_to_binary(term), + opts[:encryption_key], # encryption key + opts[:signing_key] # signing key + ) + end +end From 790a2b7d736e19f7607b5be1d1306e1435a67c35 Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Fri, 23 Feb 2018 19:16:28 -0800 Subject: [PATCH 5/7] encrypted cursor --- lib/paginator.ex | 13 ++++-- lib/paginator/cursor.ex | 10 ++-- lib/paginator/cursors/unencrypted_cursor.ex | 31 ++++++++---- lib/paginator/ecto/query.ex | 16 +++---- test/paginator/config_test.exs | 52 ++++----------------- test/paginator_test.exs | 52 +++++++++++++++++---- test/support/encrypted_cursor.ex | 39 ++++++++++++---- 7 files changed, 127 insertions(+), 86 deletions(-) diff --git a/lib/paginator.ex b/lib/paginator.ex index 3fc32db..32537eb 100644 --- a/lib/paginator.ex +++ b/lib/paginator.ex @@ -42,8 +42,15 @@ defmodule Paginator do opts = Keyword.merge(@defaults, opts) config = Config.new(opts) - unless config.cursor_fields, - do: raise("expected `:cursor_fields` to be set in call to paginate/3") + case config do + %{cursor_fields: nil} -> + raise("expected `:cursor_fields` to be set in call to paginate/3") + %{after_values: {:error, err}} -> + raise("error decoding `:after` cursor") + %{before_values: {:error, err}} -> + raise("error decoding `:before` cursor") + _ -> nil + end Paginator.paginate(queryable, config, __MODULE__, repo_opts) end @@ -156,7 +163,7 @@ defmodule Paginator do defp fetch_cursor_value(schema, %Config{cursor_fields: cursor_fields, cursor_module: cursor_module, cursor_module_opts: cursor_module_opts}) do cursor_fields |> Enum.map(fn field -> Map.get(schema, field) end) - |> cursor_module.encode(cursor_module_opts) + |> cursor_module.encode!(cursor_module_opts) end defp first_page?(sorted_entries, %Config{limit: limit}) do diff --git a/lib/paginator/cursor.ex b/lib/paginator/cursor.ex index f5504ef..bd8ea9a 100644 --- a/lib/paginator/cursor.ex +++ b/lib/paginator/cursor.ex @@ -1,7 +1,7 @@ defmodule Paginator.Cursor do - @callback decode(String.t(), opts :: list()) :: term - @callback encode(arg :: term, opts :: list()) :: String.t() - @callback decode(String.t()) :: term - @callback encode(arg :: term) :: String.t() - @optional_callbacks encode: 1, decode: 1 + @callback decode(String.t(), opts :: list()) :: {:ok, term} | {:error, term} + @callback decode!(String.t(), opts :: list()) :: term + @callback encode(cursor_fields :: term, opts :: list()) :: + {:ok, encoded :: String.t()} | {:error, term} + @callback encode!(cursor_fields :: term, opts :: list()) :: encoded :: String.t() end diff --git a/lib/paginator/cursors/unencrypted_cursor.ex b/lib/paginator/cursors/unencrypted_cursor.ex index bf02e64..b88c402 100644 --- a/lib/paginator/cursors/unencrypted_cursor.ex +++ b/lib/paginator/cursors/unencrypted_cursor.ex @@ -2,25 +2,38 @@ defmodule Paginator.Cursors.UnencryptedCursor do @behaviour Paginator.Cursor @moduledoc false - def decode(encoded_cursor), do: decode(encoded_cursor, nil) - + def decode(cursor, opts \\ []) def decode(nil, _opts), do: nil def decode(encoded_cursor, _opts) do - encoded_cursor - |> Base.url_decode64!() - |> :erlang.binary_to_term() + {:ok, + encoded_cursor + |> Base.url_decode64!() + |> :erlang.binary_to_term()} + end + + def decode!(encoded_cursor, opts \\ []) do + with {:ok, decoded} <- decode(encoded_cursor, opts) do + decoded + end end - def encode(value), do: encode(value, nil) + def encode(values, opts \\ []) def encode(values, _opts) when is_list(values) do - values - |> :erlang.term_to_binary() - |> Base.url_encode64() + {:ok, + values + |> :erlang.term_to_binary() + |> Base.url_encode64()} end def encode(value, opts) do encode([value], opts) end + + def encode!(value, opts \\ []) do + with {:ok, encoded} <- encode(value, opts) do + encoded + end + end end diff --git a/lib/paginator/ecto/query.ex b/lib/paginator/ecto/query.ex index bb0f79f..93642b2 100644 --- a/lib/paginator/ecto/query.ex +++ b/lib/paginator/ecto/query.ex @@ -61,7 +61,7 @@ defmodule Paginator.Ecto.Query do end defp maybe_where(query, %Config{ - after_values: after_values, + after_values: {:ok, after_values}, before: nil, cursor_fields: cursor_fields, sort_direction: :asc @@ -72,7 +72,7 @@ defmodule Paginator.Ecto.Query do defp maybe_where(query, %Config{ after_values: nil, - before_values: before_values, + before_values: {:ok, before_values}, cursor_fields: cursor_fields, sort_direction: :asc }) do @@ -82,8 +82,8 @@ defmodule Paginator.Ecto.Query do end defp maybe_where(query, %Config{ - after_values: after_values, - before_values: before_values, + after_values: {:ok, after_values}, + before_values: {:ok, before_values}, cursor_fields: cursor_fields, sort_direction: :asc }) do @@ -101,7 +101,7 @@ defmodule Paginator.Ecto.Query do end defp maybe_where(query, %Config{ - after_values: after_values, + after_values: {:ok, after_values}, before: nil, cursor_fields: cursor_fields, sort_direction: :desc @@ -112,7 +112,7 @@ defmodule Paginator.Ecto.Query do defp maybe_where(query, %Config{ after: nil, - before_values: before_values, + before_values: {:ok, before_values}, cursor_fields: cursor_fields, sort_direction: :desc }) do @@ -122,8 +122,8 @@ defmodule Paginator.Ecto.Query do end defp maybe_where(query, %Config{ - after_values: after_values, - before_values: before_values, + after_values: {:ok, after_values}, + before_values: {:ok, before_values}, cursor_fields: cursor_fields, sort_direction: :desc }) do diff --git a/test/paginator/config_test.exs b/test/paginator/config_test.exs index 6074d5d..863cd5e 100644 --- a/test/paginator/config_test.exs +++ b/test/paginator/config_test.exs @@ -53,7 +53,7 @@ defmodule Paginator.ConfigTest do config = Config.new(limit: 10, cursor_fields: [:id], before: simple_before()) assert config.after_values == nil - assert config.before_values == ["pay_789"] + assert config.before_values == {:ok, ["pay_789"]} assert config.limit == 10 assert config.cursor_fields == [:id] end @@ -61,7 +61,7 @@ defmodule Paginator.ConfigTest do test "simple after" do config = Config.new(limit: 10, cursor_fields: [:id], after: simple_after()) - assert config.after_values == ["pay_123"] + assert config.after_values == {:ok, ["pay_123"]} assert config.before_values == nil assert config.limit == 10 assert config.cursor_fields == [:id] @@ -71,7 +71,7 @@ defmodule Paginator.ConfigTest do config = Config.new(limit: 10, cursor_fields: [:created_at, :id], before: complex_before()) assert config.after_values == nil - assert config.before_values == ["2036-02-09T20:00:00.000Z", "pay_789"] + assert config.before_values == {:ok, ["2036-02-09T20:00:00.000Z", "pay_789"]} assert config.limit == 10 assert config.cursor_fields == [:created_at, :id] end @@ -79,51 +79,15 @@ defmodule Paginator.ConfigTest do test "complex after" do config = Config.new(limit: 10, cursor_fields: [:created_at, :id], after: complex_after()) - assert config.after_values == ["2036-02-09T20:00:00.000Z", "pay_123"] + assert config.after_values == {:ok, ["2036-02-09T20:00:00.000Z", "pay_123"]} assert config.before_values == nil assert config.limit == 10 assert config.cursor_fields == [:created_at, :id] end end - def simple_after, do: Cursor.encode("pay_123") - def simple_before, do: Cursor.encode("pay_789") - def complex_after, do: Cursor.encode(["2036-02-09T20:00:00.000Z", "pay_123"]) - def complex_before, do: Cursor.encode(["2036-02-09T20:00:00.000Z", "pay_789"]) - - # describe "custom cursor_module" do - # defmodule Paginator.Cursors.EncryptedCursor do - # @behaviour Paginator.Cursor - # @moduledoc false - # - # def decode(_encryption_key, _signing_key, nil), do: nil - # - # def decode(encryption_key, signing_key, encrypted_cursor) do - # {:ok, binary} = MessageEncryptor.decrypt(encrypted_cursor, encryption_key, signing_key) - # Plug.Crypto.safe_binary_to_term(binary) - # end - # - # def encode(encryption_key, signing_key, values) when is_list(values) do - # encrypt_cursor(encryption_key, signing_key, values) - # end - # - # def encode(encryption_key, signing_key, value) do - # encode(encryption_key, signing_key, [value]) - # end - # - # defp encrypt_cursor(encryption_key, signing_key, term) do - # MessageEncryptor.encrypt( - # :erlang.term_to_binary(term), - # encryption_key, # encryption key - # signing_key # signing key - # ) - # end - # end - # - # test "test" do - # secret_key_base = "PwGqIq85ScEflB9/3SpHjEHb44uTMFVGP+vleZTSr63NvPkWwHIAtGzgjpLvjDbA" - # salt = "USERID-65dc-4740-bba1-51ebddd58f39" - # key = KeyGenerator.generate(secret_key_base, salt, [iterations: 1000, length: 32, digest: :sha256]) - # end - # end + def simple_after, do: Cursor.encode!("pay_123") + def simple_before, do: Cursor.encode!("pay_789") + def complex_after, do: Cursor.encode!(["2036-02-09T20:00:00.000Z", "pay_123"]) + def complex_before, do: Cursor.encode!(["2036-02-09T20:00:00.000Z", "pay_789"]) end diff --git a/test/paginator_test.exs b/test/paginator_test.exs index 3e58b86..1f19d6f 100644 --- a/test/paginator_test.exs +++ b/test/paginator_test.exs @@ -609,14 +609,28 @@ defmodule PaginatorTest do end describe "with custom encrypted cursor_module" do + setup :generate_keys + + defp generate_keys(_context) do + secret_key_base = "PwGqIq85ScEflB9/3SpHjEHb44uTMFVGP+vleZTSr63NvPkWwHIAtGzgjpLvjDbA" + salt = "usersalt" + + {:ok, + key: + KeyGenerator.generate( + secret_key_base, + salt, + iterations: 1000, + length: 32, + digest: :sha256 + )} + end + test "encrypted cursor", %{ customers: {c1, _c2, _c3}, - payments: {_p1, _p2, _p3, _p4, p5, p6, p7, p8, _p9, _p10, _p11, _p12} + payments: {_p1, _p2, _p3, _p4, p5, p6, p7, p8, _p9, _p10, _p11, _p12}, + key: key } do - secret_key_base = "PwGqIq85ScEflB9/3SpHjEHb44uTMFVGP+vleZTSr63NvPkWwHIAtGzgjpLvjDbA" - salt = c1.id |> Integer.to_string - key = KeyGenerator.generate(secret_key_base, salt, [iterations: 1000, length: 32, digest: :sha256]) - %Page{entries: entries, metadata: %{after: after_cursor}} = customer_payments_by_amount(c1) |> Repo.paginate( @@ -627,7 +641,8 @@ defmodule PaginatorTest do cursor_module_opts: [ encryption_key: key, signing_key: key - ]) + ] + ) assert to_ids(entries) == to_ids([p6]) @@ -642,12 +657,31 @@ defmodule PaginatorTest do cursor_module_opts: [ encryption_key: key, signing_key: key - ]) + ] + ) assert to_ids(entries) == to_ids([p5, p7, p8]) end - test ":error if decoding fails" + test "decoding cursor raises error", %{ + customers: {c1, _c2, _c3}, + key: key + } do + assert_raise RuntimeError, "error decoding `:after` cursor", fn -> + customer_payments_by_amount(c1) + |> Repo.paginate( + cursor_fields: [:amount, :charged_at, :id], + after: "badcursor", + sort_direction: :asc, + limit: 1, + cursor_module: EncryptedCursor, + cursor_module_opts: [ + encryption_key: key, + signing_key: key + ] + ) + end + end end defp to_ids(entries), do: Enum.map(entries, & &1.id) @@ -710,7 +744,7 @@ defmodule PaginatorTest do end defp encode_cursor(value) do - Cursor.encode(value) + Cursor.encode!(value) end defp days_ago(days) do diff --git a/test/support/encrypted_cursor.ex b/test/support/encrypted_cursor.ex index 5170bfa..438acbd 100644 --- a/test/support/encrypted_cursor.ex +++ b/test/support/encrypted_cursor.ex @@ -1,14 +1,28 @@ defmodule Paginator.Cursors.EncryptedCursor do @behaviour Paginator.Cursor - @moduledoc false + @moduledoc """ + Example encrypted cursor using plug MessageEncryptor + Modeled after Plug.Crypto.COOKIE + """ alias Plug.Crypto.MessageEncryptor def decode(nil, _opts), do: nil def decode(encrypted_cursor, opts) do - {:ok, binary} = MessageEncryptor.decrypt(encrypted_cursor, opts[:encryption_key], opts[:signing_key]) - Plug.Crypto.safe_binary_to_term(binary) + with {:ok, binary} <- + MessageEncryptor.decrypt(encrypted_cursor, opts[:encryption_key], opts[:signing_key]), + term <- Plug.Crypto.safe_binary_to_term(binary) do + {:ok, term} + else + _err -> {:error, "Could not decode encrypted cursor"} + end + end + + def decode!(encrypted_cursor, opts) do + with {:ok, decoded} <- decode(encrypted_cursor, opts) do + decoded + end end def encode(values, opts) when is_list(values) do @@ -19,11 +33,20 @@ defmodule Paginator.Cursors.EncryptedCursor do encode([value], opts) end + def encode!(value, opts) do + with {:ok, encoded} <- encode(value, opts) do + encoded + end + end + defp encrypt_cursor(term, opts) do - MessageEncryptor.encrypt( - :erlang.term_to_binary(term), - opts[:encryption_key], # encryption key - opts[:signing_key] # signing key - ) + with encrypted_cursor <- + MessageEncryptor.encrypt( + :erlang.term_to_binary(term), + opts[:encryption_key], + opts[:signing_key] + ) do + {:ok, encrypted_cursor} + end end end From 0a0ce81a304863870e97afaf891a0db6ed1fcfbc Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Sat, 24 Feb 2018 06:53:01 -0800 Subject: [PATCH 6/7] remove unused deps --- mix.lock | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mix.lock b/mix.lock index 8d14afc..0db05cf 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,4 @@ %{ - "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [], [], "hexpm"}, "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, @@ -12,9 +11,8 @@ "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [], [], "hexpm"}, + "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "plug": {:hex, :plug, "1.4.5", "7b13869283fff6b8b21b84b8735326cc012c5eef8607095dc6ee24bd0a273d8e", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, From 6af03ef71e396879eeaaf3d50f9b17dca92612d3 Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Sat, 24 Feb 2018 07:04:47 -0800 Subject: [PATCH 7/7] format --- lib/paginator.ex | 16 ++++++++++++---- test/paginator_test.exs | 30 ++++++++++++++++-------------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/paginator.ex b/lib/paginator.ex index 32537eb..35be525 100644 --- a/lib/paginator.ex +++ b/lib/paginator.ex @@ -45,11 +45,15 @@ defmodule Paginator do case config do %{cursor_fields: nil} -> raise("expected `:cursor_fields` to be set in call to paginate/3") + %{after_values: {:error, err}} -> - raise("error decoding `:after` cursor") + raise("error decoding `:after` cursor (#{err})") + %{before_values: {:error, err}} -> - raise("error decoding `:before` cursor") - _ -> nil + raise("error decoding `:before` cursor (#{err})") + + _ -> + nil end Paginator.paginate(queryable, config, __MODULE__, repo_opts) @@ -160,7 +164,11 @@ defmodule Paginator do end end - defp fetch_cursor_value(schema, %Config{cursor_fields: cursor_fields, cursor_module: cursor_module, cursor_module_opts: cursor_module_opts}) do + defp fetch_cursor_value(schema, %Config{ + cursor_fields: cursor_fields, + cursor_module: cursor_module, + cursor_module_opts: cursor_module_opts + }) do cursor_fields |> Enum.map(fn field -> Map.get(schema, field) end) |> cursor_module.encode!(cursor_module_opts) diff --git a/test/paginator_test.exs b/test/paginator_test.exs index 1f19d6f..6f01fe0 100644 --- a/test/paginator_test.exs +++ b/test/paginator_test.exs @@ -667,20 +667,22 @@ defmodule PaginatorTest do customers: {c1, _c2, _c3}, key: key } do - assert_raise RuntimeError, "error decoding `:after` cursor", fn -> - customer_payments_by_amount(c1) - |> Repo.paginate( - cursor_fields: [:amount, :charged_at, :id], - after: "badcursor", - sort_direction: :asc, - limit: 1, - cursor_module: EncryptedCursor, - cursor_module_opts: [ - encryption_key: key, - signing_key: key - ] - ) - end + assert_raise RuntimeError, + "error decoding `:after` cursor (Could not decode encrypted cursor)", + fn -> + customer_payments_by_amount(c1) + |> Repo.paginate( + cursor_fields: [:amount, :charged_at, :id], + after: "badcursor", + sort_direction: :asc, + limit: 1, + cursor_module: EncryptedCursor, + cursor_module_opts: [ + encryption_key: key, + signing_key: key + ] + ) + end end end