Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom actions authorization #337

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 0 additions & 69 deletions lib/ex_admin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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)
|> Enum.reverse
_ ->
[]
end
end

@doc false
def default_page_title_actions(_conn, _) do
[]
Expand All @@ -325,46 +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, id \\ nil)
defp add_custom_actions(acc, _action, [], _id), do: acc
defp add_custom_actions(acc, action, [{action, button} | actions], id) 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
|> 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
{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),
Expand Down
4 changes: 2 additions & 2 deletions lib/ex_admin/register.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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

Expand Down
85 changes: 85 additions & 0 deletions lib/ex_admin/resource_title_actions.ex
Original file line number Diff line number Diff line change
@@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is the old code, just moved. But I'd appreciate if you could remove the nesting. Something like this (not tested):

  def action_button(conn, defn, name, _page, action, actions, id \\ nil) do
    with true <- action in actions, 
         true <- 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
  end

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
2 changes: 1 addition & 1 deletion lib/ex_admin/themes/admin_lte2/filter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 35 additions & 18 deletions test/ex_admin_test.exs → test/resource_title_actions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ 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

defmodule ExAdmin.ResourceTitleActionsTest do
use ExUnit.Case, async: true

setup config do
Expand All @@ -23,50 +33,57 @@ 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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aliasing ExAdmin.ResourceTitleActions would reduce a little noise here.

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)
assert result == [new: [{"New Simple", [href: "/admin/simples/new"]}], custom: [{"Custom Action", [href: "/custom"]}]]
result = ExAdmin.ResourceTitleActions.default(conn, defn)
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"]}]]
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]}],
custom: [{"Public Change", [href: "/admin/simples/1/member/public_change", "data-method": :put]}],
custom: [{"Custom Show", [href: "/custom/1"]}]
]

end

Expand Down
9 changes: 9 additions & 0 deletions test/support/authorization_impl.ex
Original file line number Diff line number Diff line change
@@ -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