From 37535fb473c27e153bad28a17298681bb9123a32 Mon Sep 17 00:00:00 2001 From: Denis Tataurov Date: Wed, 29 Mar 2017 16:51:43 +0300 Subject: [PATCH 1/3] Authorize custom actions --- lib/ex_admin.ex | 34 ++++++++++++++++++++++-------- lib/ex_admin/register.ex | 2 +- test/ex_admin_test.exs | 27 +++++++++++++++++++----- test/support/authorization_impl.ex | 9 ++++++++ 4 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 test/support/authorization_impl.ex diff --git a/lib/ex_admin.ex b/lib/ex_admin.ex index 5dc90b48..8c6e7c8f 100644 --- a/lib/ex_admin.ex +++ b/lib/ex_admin.ex @@ -293,7 +293,7 @@ defmodule ExAdmin do acc end end) - |> add_custom_actions(:show, actions, id) + |> add_custom_actions(:show, actions, conn, resource_model, id) |> Enum.reverse action when action in [:index, :edit] -> if Utils.authorized_action?(conn, action, resource_model) do @@ -301,7 +301,7 @@ defmodule ExAdmin do else [] end - |> add_custom_actions(action, actions) + |> add_custom_actions(action, actions, conn, resource_model) |> Enum.reverse _ -> [] @@ -338,9 +338,29 @@ defmodule ExAdmin do end end - defp add_custom_actions(acc, action, actions, id \\ nil) - defp add_custom_actions(acc, _action, [], _id), do: acc - defp add_custom_actions(acc, action, [{action, button} | actions], id) do + defp add_custom_actions(acc, action, actions, conn, resource_model, id \\ nil) + defp add_custom_actions(acc, _action, [], _conn, _resource_model, _id), do: acc + defp add_custom_actions(acc, action, [{action, custom_action_name, button} | actions], conn, resource_model, id) do + if Utils.authorized_action?(%{action: custom_action_name}, action, resource_model) do + eval_custom_function(button, id, acc) + else + acc + end + |> add_custom_actions(action, actions, conn, resource_model, id) + end + defp add_custom_actions(acc, action, [{action, button} | actions], conn, resource_model, id) do + if Utils.authorized_action?(conn, action, resource_model) do + eval_custom_function(button, id, acc) + else + acc + end + |> add_custom_actions(action, actions, conn, resource_model, id) + end + defp add_custom_actions(acc, action, [_|actions], conn, resource_model, id) do + add_custom_actions(acc, action, actions, conn, resource_model, id) + end + + def eval_custom_function(button, id, acc) do import ExAdmin.ViewHelpers endpoint() # remove the compiler warning {fun, _} = Code.eval_quoted button, [id: id], __ENV__ @@ -349,10 +369,6 @@ defmodule ExAdmin do is_function(fun, 0) -> [fun.() | acc] true -> acc end - |> add_custom_actions(action, actions, id) - end - defp add_custom_actions(acc, action, [_|actions], id) do - add_custom_actions(acc, action, actions, id) end defp action_link(conn, name, :delete, _id) do diff --git a/lib/ex_admin/register.ex b/lib/ex_admin/register.ex index 4bc530e0..2af89e6d 100644 --- a/lib/ex_admin/register.ex +++ b/lib/ex_admin/register.ex @@ -310,7 +310,7 @@ defmodule ExAdmin.Register do end end action = if type == :member_actions, do: :show, else: :index - [{action, fun} | acc] + [{action, name, fun} | acc] end) end diff --git a/test/ex_admin_test.exs b/test/ex_admin_test.exs index 6026d685..a259213e 100644 --- a/test/ex_admin_test.exs +++ b/test/ex_admin_test.exs @@ -3,8 +3,18 @@ defmodule TestExAdmin.ExAdmin.SimpleCustom do register_resource TestExAdmin.Simple do action_item :index, fn -> action_item_link "Custom Action", href: "/custom" end action_item :show, fn id -> action_item_link "Custom Show", href: "/custom/#{id}" end + + member_action :public_change, &__MODULE__.custom_action/2 + member_action :private_change, &__MODULE__.custom_action/2 + collection_action :public_bulk, &__MODULE__.custom_action/2 + collection_action :private_bulk, &__MODULE__.custom_action/2 + end + + def custom_action(conn, _) do + Phoenix.Controller.json conn, %{done: true} end end + defmodule ExAdminTest do use ExUnit.Case, async: true @@ -59,14 +69,21 @@ defmodule ExAdminTest do test "default_resource_title_actions custom actions", %{defn: defn, conn: conn} do conn = struct(conn, private: %{phoenix_action: :index}) result = ExAdmin.default_resource_title_actions(conn, defn) - assert result == [new: [{"New Simple", [href: "/admin/simples/new"]}], custom: [{"Custom Action", [href: "/custom"]}]] + assert result == [ + new: [{"New Simple", [href: "/admin/simples/new"]}], + custom: [{"Public Bulk", [href: "/admin/simples/collection/public_bulk"]}], + custom: [{"Custom Action", [href: "/custom"]}] + ] conn = struct(conn, private: %{phoenix_action: :show}) result = ExAdmin.default_resource_title_actions(conn, defn) - assert result == [edit: [{"Edit Simple", [href: "/admin/simples/1/edit"]}], new: [{"New Simple", [href: "/admin/simples/new"]}], - delete: [{"Delete Simple", - [href: "/admin/simples/1", "data-confirm": "Are you sure you want to delete this?", "data-method": :delete, rel: :nofollow]}], - custom: [{"Custom Show", [href: "/custom/1"]}]] + assert result == [ + edit: [{"Edit Simple", [href: "/admin/simples/1/edit"]}], + new: [{"New Simple", [href: "/admin/simples/new"]}], + delete: [{"Delete Simple", [href: "/admin/simples/1", "data-confirm": "Are you sure you want to delete this?", "data-method": :delete, rel: :nofollow]}], + custom: [{"Public Change", [href: "/admin/simples/1/member/public_change", "data-method": :put]}], + custom: [{"Custom Show", [href: "/custom/1"]}] + ] end diff --git a/test/support/authorization_impl.ex b/test/support/authorization_impl.ex new file mode 100644 index 00000000..84a8862c --- /dev/null +++ b/test/support/authorization_impl.ex @@ -0,0 +1,9 @@ +defimpl ExAdmin.Authorization, for: TestExAdmin.Simple do + def authorize_query(_, _, query, _, _), do: query + + def authorize_action(_, %{action: :public_change}, :show), do: true + def authorize_action(_, %{action: :private_change}, :show), do: false + def authorize_action(_, %{action: :public_bulk}, :index), do: true + def authorize_action(_, %{action: :private_bulk}, :index), do: false + def authorize_action(_, _, _), do: true +end From 4d172ac38b55c9fda6aebef50b2efbb37ea69c10 Mon Sep 17 00:00:00 2001 From: Denis Tataurov Date: Wed, 29 Mar 2017 16:52:05 +0300 Subject: [PATCH 2/3] Fix filter sorting bug --- lib/ex_admin/themes/admin_lte2/filter.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex_admin/themes/admin_lte2/filter.ex b/lib/ex_admin/themes/admin_lte2/filter.ex index 2f67d1d0..dfc4c8f5 100644 --- a/lib/ex_admin/themes/admin_lte2/filter.ex +++ b/lib/ex_admin/themes/admin_lte2/filter.ex @@ -122,7 +122,7 @@ defmodule ExAdmin.Theme.AdminLte2.Filter do id = "q_#{owner_key}" name_label = field_label(name, defn) repo = Application.get_env :ex_admin, :repo - name_field = ExAdmin.Helpers.get_name_field(defn.resource_model) + name_field = ExAdmin.Helpers.get_name_field(assoc) resources = if is_nil(name_field) do repo.all(assoc) else From 50afd90e1a8e37cf305ed75ee835992fcd45c087 Mon Sep 17 00:00:00 2001 From: Denis Tataurov Date: Wed, 29 Mar 2017 18:22:50 +0300 Subject: [PATCH 3/3] Extract action functions from ExAdmin to ExAdmin.ResourceTitleActions --- lib/ex_admin.ex | 85 ------------------- lib/ex_admin/register.ex | 2 +- lib/ex_admin/resource_title_actions.ex | 85 +++++++++++++++++++ ...st.exs => resource_title_actions_test.exs} | 26 +++--- 4 files changed, 99 insertions(+), 99 deletions(-) create mode 100644 lib/ex_admin/resource_title_actions.ex rename test/{ex_admin_test.exs => resource_title_actions_test.exs} (75%) diff --git a/lib/ex_admin.ex b/lib/ex_admin.ex index 8c6e7c8f..14cb92f1 100644 --- a/lib/ex_admin.ex +++ b/lib/ex_admin.ex @@ -279,35 +279,6 @@ defmodule ExAdmin do end) end - @doc false - def default_resource_title_actions(%Plug.Conn{params: params} = conn, %{resource_model: resource_model} = defn) do - singular = ExAdmin.Utils.displayable_name_singular(conn) |> titleize - actions = defn.actions - case Utils.action_name(conn) do - :show -> - id = Map.get(params, "id") - Enum.reduce([:edit, :new, :delete], [], fn(action, acc) -> - if Utils.authorized_action?(conn, action, resource_model) do - [{action, action_button(conn, defn, singular, :show, action, actions, id)}|acc] - else - acc - end - end) - |> add_custom_actions(:show, actions, conn, resource_model, id) - |> Enum.reverse - action when action in [:index, :edit] -> - if Utils.authorized_action?(conn, action, resource_model) do - [{:new, action_button(conn, defn, singular, action, :new, actions)}] - else - [] - end - |> add_custom_actions(action, actions, conn, resource_model) - |> Enum.reverse - _ -> - [] - end - end - @doc false def default_page_title_actions(_conn, _) do [] @@ -325,62 +296,6 @@ defmodule ExAdmin do conn.assigns.theme.name end - def action_button(conn, defn, name, _page, action, actions, id \\ nil) do - if action in actions do - if ExAdmin.Utils.authorized_action?(conn, action, defn) do - action_name = defn.action_labels[action] || humanize(action) - [action_link(conn, "#{action_name} #{name}", action, id)] - else - [] - end - else - [] - end - end - - defp add_custom_actions(acc, action, actions, conn, resource_model, id \\ nil) - defp add_custom_actions(acc, _action, [], _conn, _resource_model, _id), do: acc - defp add_custom_actions(acc, action, [{action, custom_action_name, button} | actions], conn, resource_model, id) do - if Utils.authorized_action?(%{action: custom_action_name}, action, resource_model) do - eval_custom_function(button, id, acc) - else - acc - end - |> add_custom_actions(action, actions, conn, resource_model, id) - end - defp add_custom_actions(acc, action, [{action, button} | actions], conn, resource_model, id) do - if Utils.authorized_action?(conn, action, resource_model) do - eval_custom_function(button, id, acc) - else - acc - end - |> add_custom_actions(action, actions, conn, resource_model, id) - end - defp add_custom_actions(acc, action, [_|actions], conn, resource_model, id) do - add_custom_actions(acc, action, actions, conn, resource_model, id) - end - - def eval_custom_function(button, id, acc) do - import ExAdmin.ViewHelpers - endpoint() # remove the compiler warning - {fun, _} = Code.eval_quoted button, [id: id], __ENV__ - cond do - is_function(fun, 1) -> [fun.(id) | acc] - is_function(fun, 0) -> [fun.() | acc] - true -> acc - end - end - - defp action_link(conn, name, :delete, _id) do - {name, - [href: admin_resource_path(conn, :destroy), - "data-confirm": Utils.confirm_message, - "data-method": :delete, rel: :nofollow]} - end - defp action_link(conn, name, action, _id) do - {name, [href: admin_resource_path(conn, action)]} - end - @doc false def has_action?(conn, defn, action) do if ExAdmin.Utils.authorized_action?(conn, action, defn), diff --git a/lib/ex_admin/register.ex b/lib/ex_admin/register.ex index 2af89e6d..b67679c2 100644 --- a/lib/ex_admin/register.ex +++ b/lib/ex_admin/register.ex @@ -186,7 +186,7 @@ defmodule ExAdmin.Register do defstruct controller: @controller, controller_methods: Module.get_attribute(__MODULE__, :controller_methods), - title_actions: &ExAdmin.default_resource_title_actions/2, + title_actions: &ExAdmin.ResourceTitleActions.default/2, type: :resource, resource_model: module, resource_name: resource_name(module), diff --git a/lib/ex_admin/resource_title_actions.ex b/lib/ex_admin/resource_title_actions.ex new file mode 100644 index 00000000..a87d6440 --- /dev/null +++ b/lib/ex_admin/resource_title_actions.ex @@ -0,0 +1,85 @@ +defmodule ExAdmin.ResourceTitleActions do + alias ExAdmin.Utils + @doc false + def default(%Plug.Conn{params: params} = conn, %{resource_model: resource_model} = defn) do + singular = Utils.displayable_name_singular(conn) |> Utils.titleize + actions = defn.actions + case Utils.action_name(conn) do + :show -> + id = Map.get(params, "id") + Enum.reduce([:edit, :new, :delete], [], fn(action, acc) -> + if Utils.authorized_action?(conn, action, resource_model) do + [{action, action_button(conn, defn, singular, :show, action, actions, id)}|acc] + else + acc + end + end) + |> add_custom_actions(:show, actions, conn, resource_model, id) + |> Enum.reverse + action when action in [:index, :edit] -> + if Utils.authorized_action?(conn, action, resource_model) do + [{:new, action_button(conn, defn, singular, action, :new, actions)}] + else + [] + end + |> add_custom_actions(action, actions, conn, resource_model) + |> Enum.reverse + _ -> + [] + end + end + + @doc false + def action_button(conn, defn, name, _page, action, actions, id \\ nil) do + if action in actions do + if Utils.authorized_action?(conn, action, defn) do + action_name = defn.action_labels[action] || Utils.humanize(action) + [action_link(conn, "#{action_name} #{name}", action, id)] + else + [] + end + else + [] + end + end + + defp add_custom_actions(acc, action, actions, conn, resource_model, id \\ nil) + defp add_custom_actions(acc, _action, [], _conn, _resource_model, _id), do: acc + defp add_custom_actions(acc, action, [{action, custom_action_name, button} | actions], conn, resource_model, id) do + %{action: custom_action_name, id: id} + |> Utils.authorized_action?(action, resource_model) + |> eval_custom_function(button, id, acc) + |> add_custom_actions(action, actions, conn, resource_model, id) + end + defp add_custom_actions(acc, action, [{action, button} | actions], conn, resource_model, id) do + conn + |> Utils.authorized_action?(action, resource_model) + |> eval_custom_function(button, id, acc) + |> add_custom_actions(action, actions, conn, resource_model, id) + end + defp add_custom_actions(acc, action, [_|actions], conn, resource_model, id) do + add_custom_actions(acc, action, actions, conn, resource_model, id) + end + + defp eval_custom_function(false, _button, _id, acc), do: acc + defp eval_custom_function(true, button, id, acc) do + import ExAdmin.ViewHelpers + endpoint() # remove the compiler warning + {fun, _} = Code.eval_quoted button, [id: id], __ENV__ + cond do + is_function(fun, 1) -> [fun.(id) | acc] + is_function(fun, 0) -> [fun.() | acc] + true -> acc + end + end + + defp action_link(conn, name, :delete, _id) do + {name, + [href: Utils.admin_resource_path(conn, :destroy), + "data-confirm": Utils.confirm_message, + "data-method": :delete, rel: :nofollow]} + end + defp action_link(conn, name, action, _id) do + {name, [href: Utils.admin_resource_path(conn, action)]} + end +end diff --git a/test/ex_admin_test.exs b/test/resource_title_actions_test.exs similarity index 75% rename from test/ex_admin_test.exs rename to test/resource_title_actions_test.exs index a259213e..62df2620 100644 --- a/test/ex_admin_test.exs +++ b/test/resource_title_actions_test.exs @@ -15,7 +15,7 @@ defmodule TestExAdmin.ExAdmin.SimpleCustom do end end -defmodule ExAdminTest do +defmodule ExAdmin.ResourceTitleActionsTest do use ExUnit.Case, async: true setup config do @@ -33,42 +33,42 @@ defmodule ExAdminTest do @tag as_resource: %TestExAdmin.ExAdmin.Simple{} test "action_button", %{defn: defn, conn: conn} do - result = ExAdmin.action_button(conn, defn, "Simple", :show, :edit, defn.actions, "17") + result = ExAdmin.ResourceTitleActions.action_button(conn, defn, "Simple", :show, :edit, defn.actions, "17") assert result == [{"Edit Simple", [href: "/admin/simples/1/edit"]}] - result = ExAdmin.action_button(conn, defn, "Simple", :show, :new, defn.actions, "17") + result = ExAdmin.ResourceTitleActions.action_button(conn, defn, "Simple", :show, :new, defn.actions, "17") assert result == [{"New Simple", [href: "/admin/simples/new"]}] - result = ExAdmin.action_button(conn, defn, "Simple", :show, :delete, defn.actions, "17") + result = ExAdmin.ResourceTitleActions.action_button(conn, defn, "Simple", :show, :delete, defn.actions, "17") assert result == [{"Delete Simple", [href: "/admin/simples/1", "data-confirm": "Are you sure you want to delete this?", "data-method": :delete, rel: :nofollow]}] - result = ExAdmin.action_button(conn, defn, "Simple", :index, :new, defn.actions, "17") + result = ExAdmin.ResourceTitleActions.action_button(conn, defn, "Simple", :index, :new, defn.actions, "17") assert result == [{"New Simple", [href: "/admin/simples/new"]}] - result = ExAdmin.action_button(conn, defn, "Simple", :edit, :new, defn.actions, "17") + result = ExAdmin.ResourceTitleActions.action_button(conn, defn, "Simple", :edit, :new, defn.actions, "17") assert result == [{"New Simple", [href: "/admin/simples/new"]}] end @tag as_resource: %TestExAdmin.ExAdmin.Simple{} - test "default_resource_title_actions", %{defn: defn, conn: conn} do + test "default actions", %{defn: defn, conn: conn} do conn = struct(conn, private: %{phoenix_action: :show}) - result = ExAdmin.default_resource_title_actions(conn, defn) + result = ExAdmin.ResourceTitleActions.default(conn, defn) assert result == [edit: [{"Edit Simple", [href: "/admin/simples/1/edit"]}], new: [{"New Simple", [href: "/admin/simples/new"]}], delete: [{"Delete Simple", [href: "/admin/simples/1", "data-confirm": "Are you sure you want to delete this?", "data-method": :delete, rel: :nofollow]}]] conn = struct(conn, private: %{phoenix_action: :index}) - result = ExAdmin.default_resource_title_actions(conn, defn) + result = ExAdmin.ResourceTitleActions.default(conn, defn) assert result == [new: [{"New Simple", [href: "/admin/simples/new"]}]] conn = struct(conn, private: %{phoenix_action: :edit}) - result = ExAdmin.default_resource_title_actions(conn, defn) + result = ExAdmin.ResourceTitleActions.default(conn, defn) assert result == [new: [{"New Simple", [href: "/admin/simples/new"]}]] end @tag as_resource: %TestExAdmin.ExAdmin.SimpleCustom{} - test "default_resource_title_actions custom actions", %{defn: defn, conn: conn} do + test "default custom actions", %{defn: defn, conn: conn} do conn = struct(conn, private: %{phoenix_action: :index}) - result = ExAdmin.default_resource_title_actions(conn, defn) + result = ExAdmin.ResourceTitleActions.default(conn, defn) assert result == [ new: [{"New Simple", [href: "/admin/simples/new"]}], custom: [{"Public Bulk", [href: "/admin/simples/collection/public_bulk"]}], @@ -76,7 +76,7 @@ defmodule ExAdminTest do ] conn = struct(conn, private: %{phoenix_action: :show}) - result = ExAdmin.default_resource_title_actions(conn, defn) + result = ExAdmin.ResourceTitleActions.default(conn, defn) assert result == [ edit: [{"Edit Simple", [href: "/admin/simples/1/edit"]}], new: [{"New Simple", [href: "/admin/simples/new"]}],