ash/test/calculations/calculation_with_auth_test.exs
Zach Daniel b6c8b1c2ec fix: allow calculations to access what would forbidden fields of related items
context for this change:

this requires a potentially risky modification to how field policies are
applied for nested queries and embedded types, which is that we now
apply field policies for nested queries in a post-processing pass.

Bugs in this code could lead to forbidden fields being displayed
when they shouldn't be. Luckily we have tests for this kind of thing :)
2024-05-08 16:22:48 -04:00

261 lines
6.6 KiB
Elixir

defmodule Ash.Test.Resource.CalculationWithAuthTest do
@moduledoc false
use ExUnit.Case, async: true
alias Ash.Test.Domain, as: Domain
defmodule Name do
use Ash.Resource.Calculation
@impl true
def load(_, _, _), do: [:first_name, :last_name, :show_last_name]
@impl true
def calculate(records, _, _) do
{:ok,
records
|> Enum.map(fn
%{
first_name: first_name,
show_last_name: true,
last_name: last_name
} ->
"#{first_name} #{last_name}"
%{
first_name: first_name,
show_last_name: false
} ->
first_name
end)}
end
end
defmodule SelfName do
use Ash.Resource.Calculation
@impl true
def load(_, _, _), do: [self: [:first_name, :last_name, :show_last_name]]
@impl true
def calculate(records, _, _) do
{:ok,
records
|> Enum.map(fn
%{
self: %{
first_name: first_name,
show_last_name: true,
last_name: last_name
}
} ->
"#{first_name} #{last_name}"
%{
self: %{
first_name: first_name,
show_last_name: false
}
} ->
first_name
end)}
end
end
defmodule SelfSelfName do
use Ash.Resource.Calculation
@impl true
def load(_, _, _), do: [self: [self: [:first_name, :last_name, :show_last_name]]]
@impl true
def calculate(records, _, _) do
{:ok,
records
|> Enum.map(fn
%{
self: %{
self: %{
first_name: first_name,
show_last_name: true,
last_name: last_name
}
}
} ->
"#{first_name} #{last_name}"
%{
self: %{
self: %{
first_name: first_name,
show_last_name: false
}
}
} ->
first_name
end)}
end
end
defmodule EmbeddedNotAllowed do
use Ash.Resource.Calculation
@impl true
def load(_, _, _), do: [embedded: [:not_allowed]]
@impl true
def calculate(records, _, _) do
Enum.map(records, fn record ->
record.embedded && record.embedded.not_allowed
end)
end
end
defmodule Embedded do
use Ash.Resource,
data_layer: :embedded,
authorizers: [Ash.Policy.Authorizer]
resource do
require_primary_key? false
end
attributes do
attribute :allowed, :string, public?: true
attribute :not_allowed, :string, public?: true
end
policies do
policy always() do
authorize_if always()
end
end
field_policies do
field_policy :allowed do
authorize_if always()
end
field_policy :not_allowed do
forbid_if always()
end
end
end
defmodule Resource do
use Ash.Resource,
domain: Domain,
authorizers: [Ash.Policy.Authorizer],
data_layer: Ash.DataLayer.Ets
ets do
private? true
end
attributes do
uuid_primary_key :id
attribute :first_name, :string
attribute :last_name, :string
attribute :show_last_name, :boolean, public?: true
attribute :embedded, Embedded
end
code_interface do
domain Domain
define :create
define :read
end
calculations do
calculate :name, :string, Name, public?: true
calculate :self_name, :string, SelfName, public?: true
calculate :self_self_name, :string, SelfSelfName, public?: true
calculate :embedded_not_allowed, :string, EmbeddedNotAllowed, public?: true
end
policies do
policy always() do
authorize_if always()
end
end
field_policies do
field_policy :show_last_name do
forbid_if always()
end
field_policy [:name, :self_name, :self_self_name, :embedded_not_allowed] do
authorize_if always()
end
end
actions do
defaults [:read, create: [:first_name, :last_name, :show_last_name, :embedded]]
end
relationships do
has_one :self, __MODULE__ do
source_attribute :id
destination_attribute :id
end
end
end
test "can calculate even if calculate depends on forbidden field" do
Resource.create!(%{first_name: "Homer", last_name: "Simpson", show_last_name: false})
[%{name: "Homer", show_last_name: %Ash.ForbiddenField{}}] =
Resource.read!(%{}, load: [:name, :show_last_name], authorize?: true)
end
test "can calculate even if calculate depends on forbidden field from a relationship" do
Resource.create!(%{first_name: "Homer", last_name: "Simpson", show_last_name: false})
[%{self_name: "Homer"}] =
Resource.read!(%{}, load: [:self_name, self: [:show_last_name]], authorize?: true)
end
test "can calculate even if calculate depends on forbidden field from a relationsip, and the field will show as forbidden in the final data" do
Resource.create!(%{first_name: "Homer", last_name: "Simpson", show_last_name: false})
[%{self_name: "Homer", self: %{show_last_name: %Ash.ForbiddenField{}}}] =
Resource.read!(%{}, load: [:self_name, self: [:show_last_name]], authorize?: true)
end
test "can calculate even if calculate depends on forbidden field from a relationsip two levels deep, and the field will show as forbidden in the final data" do
Resource.create!(%{first_name: "Homer", last_name: "Simpson", show_last_name: false})
[%{self_self_name: "Homer", self: %{self: %{show_last_name: %Ash.ForbiddenField{}}}}] =
Resource.read!(%{},
load: [:self_self_name, self: [self: [:show_last_name]]],
authorize?: true
)
end
test "can calculate even if calculate depends on forbidden field from an embedded attribute" do
Resource.create!(%{
first_name: "Homer",
last_name: "Simpson",
show_last_name: false,
embedded: %{allowed: "shown", not_allowed: "hidden"}
})
[%{embedded_not_allowed: "hidden"}] =
Resource.read!(%{}, load: [:embedded_not_allowed], authorize?: true)
end
test "can calculate even if calculate depends on forbidden field from an embedded field, and the field will show as forbidden in the final data" do
Resource.create!(%{
first_name: "Homer",
last_name: "Simpson",
show_last_name: false,
embedded: %{allowed: "shown", not_allowed: "hidden"}
})
[%{embedded_not_allowed: "hidden", embedded: %{not_allowed: %Ash.ForbiddenField{}}}] =
Resource.read!(%{}, load: [:embedded_not_allowed], authorize?: true)
end
end