From 1d52b6bc7399d2242f794333fae470d56f66656f Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Fri, 7 Apr 2023 14:15:58 -0400 Subject: [PATCH] feat: Introduce can_see?/2 to add field specific authorization --- lib/ex_teal/field.ex | 1 + lib/ex_teal/field_visibility.ex | 10 ++++++ lib/ex_teal/resource/fields.ex | 20 +++++++++--- test/ex_teal/resource/fields_test.exs | 45 +++++++++++++++++++-------- 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/lib/ex_teal/field.ex b/lib/ex_teal/field.ex index 1b29f851..89534190 100644 --- a/lib/ex_teal/field.ex +++ b/lib/ex_teal/field.ex @@ -26,6 +26,7 @@ defmodule ExTeal.Field do panel: nil, embed_field: nil, getter: nil, + can_see: nil, show_on_index: true, show_on_detail: true, show_on_new: true, diff --git a/lib/ex_teal/field_visibility.ex b/lib/ex_teal/field_visibility.ex index 7f720ca5..b40acc4c 100644 --- a/lib/ex_teal/field_visibility.ex +++ b/lib/ex_teal/field_visibility.ex @@ -105,4 +105,14 @@ defmodule ExTeal.FieldVisibility do """ @spec as_html(Field.t()) :: Field.t() def as_html(field), do: %{field | as_html: true} + + @doc """ + Conditionally render a field on a resource as a whole based on the current + request. Helpful for filtering out fields based on the current + users permissions. + """ + @spec can_see?(Field.t(), (Plug.Conn.t() -> boolean())) :: Field.t() + def can_see?(field, func) do + Map.put(field, :can_see, func) + end end diff --git a/lib/ex_teal/resource/fields.ex b/lib/ex_teal/resource/fields.ex index 8026f06a..67725291 100644 --- a/lib/ex_teal/resource/fields.ex +++ b/lib/ex_teal/resource/fields.ex @@ -21,10 +21,6 @@ defmodule ExTeal.Resource.Fields do """ @callback fields() :: list(Field.t()) - @doc """ - Used to decorate the fields before - """ - defmacro __using__(_) do quote do @behaviour ExTeal.Resource.Fields @@ -241,10 +237,26 @@ defmodule ExTeal.Resource.Fields do |> Map.put(:value, value) |> add_panel_key(panel) |> field.type.apply_options_for(model, conn, type) + |> apply_can_see(conn) end) |> Enum.reject(&is_nil/1) end + @doc """ + Apply the can_see function attached to a field to filter + out the field from serialized responses. + """ + @spec apply_can_see(Field.t(), Plug.Conn.t()) :: Field.t() | nil + def apply_can_see(%Field{can_see: nil} = field, _), do: field + + def apply_can_see(%Field{can_see: see} = field, conn) when is_function(see, 1) do + if see.(conn) do + field + else + nil + end + end + def add_panel_key(%Field{panel: panel} = field, _) when not is_nil(panel), do: field def add_panel_key(%Field{} = field, nil), do: field def add_panel_key(%Field{} = field, %Panel{key: key}), do: Map.put(field, :panel, key) diff --git a/test/ex_teal/resource/fields_test.exs b/test/ex_teal/resource/fields_test.exs index 1ea2b534..76b3ef39 100644 --- a/test/ex_teal/resource/fields_test.exs +++ b/test/ex_teal/resource/fields_test.exs @@ -1,7 +1,7 @@ defmodule ExTeal.Resource.FieldsTest do use TestExTeal.ConnCase - alias ExTeal.Fields.{Hidden, ManyToManyBelongsTo, Text} + alias ExTeal.Fields.{ManyToManyBelongsTo, Text} alias ExTeal.Resource.Fields alias TestExTeal.{PostResource, TagResource, UserResource} @@ -43,7 +43,9 @@ defmodule ExTeal.Resource.FieldsTest do Embedded.new(:location, [ Text.make(:street_line_1), Text.make(:city) - ]) + ]), + Text.make(:description) + |> can_see?(fn %{assigns: assigns} -> Map.get(assigns, :foo) == :bar end) ] end @@ -65,18 +67,20 @@ defmodule ExTeal.Resource.FieldsTest do test "returns all fields for an embedded resource" do fields = Fields.all_fields(EmbeddedPostResource) - as_panel_field = fn field -> - field - |> Map.put(:attribute, :"location.#{field.field}") - |> Map.put(:panel, :location) - |> Map.put(:embed_field, :location) - end + assert Enum.map(fields, & &1.field) == [ + :name, + :id, + :street_line_1, + :city, + :description + ] - assert fields == [ - Text.make(:name), - as_panel_field.(Hidden.make(:id)), - as_panel_field.(Text.make(:street_line_1)), - as_panel_field.(Text.make(:city)) + assert Enum.map(fields, & &1.attribute) == [ + "name", + :"location.id", + :"location.street_line_1", + :"location.city", + "description" ] end end @@ -141,6 +145,21 @@ defmodule ExTeal.Resource.FieldsTest do end end + test "apply_values_for/5 hides fields based on can_see?/1" do + p = insert(:post) + + conn = prep_conn(:get, "/post-embeds/#{p.id}") + fields = Fields.fields_for(:index, EmbeddedPostResource) + normal_fields = Fields.apply_values(fields, p, EmbeddedPostResource, conn, :index, nil) + + assert Enum.count(normal_fields) == 3 + + conn = Plug.Conn.assign(conn, :foo, :bar) + + fields = Fields.apply_values(fields, p, EmbeddedPostResource, conn, :index, nil) + assert Enum.count(fields) == 4 + end + def prep_conn(method, path, params \\ %{}) do params = Map.merge(params, %{"_format" => "json"})