fix: static checks with conditions could be overly or insufficiently restrictive

This commit is contained in:
Zach Daniel 2023-09-27 14:40:40 -04:00
parent 00a582fbc1
commit a00806eeb0
6 changed files with 106 additions and 186 deletions

View file

@ -682,8 +682,9 @@ defmodule Ash.Policy.Authorizer do
end) end)
# primary key doesn't have policies on it, and so is nil here # primary key doesn't have policies on it, and so is nil here
|> Map.drop([nil, []]) |> Map.drop([nil, []])
|> Enum.reduce({query_or_changeset, authorizer}, fn {policies, fields}, |> Enum.reduce(
{query_or_changeset, authorizer} -> {query_or_changeset, authorizer},
fn {policies, fields}, {query_or_changeset, authorizer} ->
{expr, authorizer} = {expr, authorizer} =
case strict_check_result( case strict_check_result(
%{ %{
@ -747,7 +748,8 @@ defmodule Ash.Policy.Authorizer do
calculation calculation
), authorizer} ), authorizer}
end end
end) end
)
|> then(fn {result, authorizer} -> |> then(fn {result, authorizer} ->
{:ok, result, authorizer} {:ok, result, authorizer}
end) end)
@ -785,15 +787,13 @@ defmodule Ash.Policy.Authorizer do
{filterable, require_check} = {filterable, require_check} =
authorizer.scenarios authorizer.scenarios
|> Enum.split_with(fn scenario -> |> Enum.split_with(fn scenario ->
scenario Enum.all?(scenario, fn {{check_module, opts}, _} ->
|> Enum.reject(fn {{check_module, opts}, _} ->
opts[:access_type] == :filter || opts[:access_type] == :filter ||
match?( match?(
{:ok, _}, {:ok, _},
Ash.Policy.Policy.fetch_fact(authorizer.facts, {check_module, opts}) Ash.Policy.Policy.fetch_fact(authorizer.facts, {check_module, opts})
) || check_module.type() == :filter ) || check_module.type() == :filter
end) end)
|> Enum.empty?()
end) end)
filter = strict_filters(filterable, authorizer) filter = strict_filters(filterable, authorizer)

View file

@ -66,7 +66,7 @@ defmodule Ash.Policy.Checker do
scenario scenario
|> Map.drop([true, false]) |> Map.drop([true, false])
|> Enum.reduce_while(:reality, fn {fact, requirement}, status -> |> Enum.reduce_while(:reality, fn {fact, requirement}, status ->
case Map.fetch(facts, fact) do case Policy.fetch_fact(facts, fact) do
{:ok, ^requirement} -> {:ok, ^requirement} ->
{:cont, status} {:cont, status}

View file

@ -152,8 +152,8 @@ defmodule Ash.Policy.Policy do
Enum.find_value(authorizer.facts, fn Enum.find_value(authorizer.facts, fn
{{fact_mod, fact_opts}, result} -> {{fact_mod, fact_opts}, result} ->
if check_module == fact_mod && if check_module == fact_mod &&
Keyword.delete(fact_opts, :access_type) == Keyword.drop(fact_opts, [:access_type, :ash_field_policy?]) ==
Keyword.delete(opts, :access_type) do Keyword.drop(opts, [:access_type, :ash_field_policy?]) do
{:ok, result} {:ok, result}
end end
@ -198,8 +198,8 @@ defmodule Ash.Policy.Policy do
Enum.find_value(facts, fn Enum.find_value(facts, fn
{{fact_mod, fact_opts}, result} -> {{fact_mod, fact_opts}, result} ->
if mod == fact_mod && if mod == fact_mod &&
Keyword.delete(fact_opts, :access_type) == Keyword.drop(fact_opts, [:access_type, :ash_field_policy?]) ==
Keyword.delete(opts, :access_type) do Keyword.drop(opts, [:access_type, :ash_field_policy?]) do
{:ok, result} {:ok, result}
end end
@ -275,13 +275,14 @@ defmodule Ash.Policy.Policy do
condition_expression -> condition_expression ->
case compile_policy_expression(policies, authorizer) do case compile_policy_expression(policies, authorizer) do
{true, authorizer} -> {true, authorizer} ->
{condition_expression, authorizer} {true, authorizer}
{false, authorizer} -> {false, authorizer} ->
{{:not, condition_expression}, authorizer} {{:not, condition_expression}, authorizer}
{compiled_policies, authorizer} -> {compiled_policies, authorizer} ->
{{:and, condition_expression, compiled_policies}, authorizer} {{:or, {:and, condition_expression, compiled_policies}, {:not, condition_expression}},
authorizer}
end end
end end
end end

View file

@ -100,7 +100,7 @@ defmodule Ash.Resource.Relationships.BelongsTo do
def opt_schema, do: @opt_schema def opt_schema, do: @opt_schema
@doc false @doc false
# sobelow_skip ["DOS.StringToAtom"] # sobelow_skip ["DOS.BinToAtom"]
def transform(%{source_attribute: source_attribute, name: name} = relationship) do def transform(%{source_attribute: source_attribute, name: name} = relationship) do
{:ok, %{relationship | source_attribute: source_attribute || :"#{name}_id"}} {:ok, %{relationship | source_attribute: source_attribute || :"#{name}_id"}}
end end

View file

@ -87,7 +87,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
def opt_schema, do: @opt_schema def opt_schema, do: @opt_schema
@doc false @doc false
# sobelow_skip ["DOS.StringToAtom"] # sobelow_skip ["DOS.BinToAtom"]
def transform(%{join_relationship: join_relationship, name: name} = relationship) do def transform(%{join_relationship: join_relationship, name: name} = relationship) do
{:ok, %{relationship | join_relationship: join_relationship || :"#{name}_join_assoc"}} {:ok, %{relationship | join_relationship: join_relationship || :"#{name}_join_assoc"}}
end end

View file

@ -10,55 +10,6 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
end end
end end
defmodule ResourceWithExprCondition do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
authorizers: [Ash.Policy.Authorizer]
attributes do
uuid_primary_key :id
attribute :name, :string
end
field_policies do
field_policy :name, [expr(name == ^actor(:name))] do
authorize_if always()
end
end
policies do
policy always() do
authorize_if always()
end
end
code_interface do
define_for Api
define :create
define :read
end
actions do
defaults [:create, :read]
end
end
test "expr condition forbids field if it does not match" do
ResourceWithExprCondition.create!(%{name: "foo"})
ResourceWithExprCondition.create!(%{name: "bar"})
assert [
%{name: "bar"},
%{name: %Ash.ForbiddenField{field: :name, type: :attribute}}
] =
ResourceWithExprCondition.read!(
actor: %{name: "bar"},
query: ResourceWithExprCondition |> Ash.Query.sort([:name])
)
end
defmodule ResourceWithMultiplePoliciesForOneField do defmodule ResourceWithMultiplePoliciesForOneField do
use Ash.Resource, use Ash.Resource,
data_layer: Ash.DataLayer.Ets, data_layer: Ash.DataLayer.Ets,
@ -68,6 +19,8 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
uuid_primary_key :id uuid_primary_key :id
attribute :name, :string attribute :name, :string
attribute :other_name, :string
attribute :other_other_name, :string
end end
field_policies do field_policies do
@ -78,10 +31,27 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
field_policy :name, expr(name == ^actor(:name)) do field_policy :name, expr(name == ^actor(:name)) do
authorize_if always() authorize_if always()
end end
field_policy :other_name, [actor_attribute_equals(:admin, true)] do
forbid_if always()
end
field_policy :other_name, expr(name == ^actor(:name)) do
forbid_if always()
end
field_policy :other_other_name, [actor_attribute_equals(:admin, true)] do
authorize_if always()
end
field_policy :other_other_name, expr(name == ^actor(:name)) do
forbid_if always()
end
end end
policies do policies do
policy always() do policy always() do
forbid_if never()
authorize_if always() authorize_if always()
end end
end end
@ -99,80 +69,29 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
end end
test "multiple field policies for the same field with different conditions work" do test "multiple field policies for the same field with different conditions work" do
ResourceWithMultiplePoliciesForOneField.create!(%{name: "foo"}) ResourceWithMultiplePoliciesForOneField.create!(%{
ResourceWithMultiplePoliciesForOneField.create!(%{name: "baz"}) name: "foo",
other_name: "foo",
other_other_name: "foo"
})
ResourceWithMultiplePoliciesForOneField.create!(%{
name: "baz",
other_name: "baz",
other_other_name: "bar"
})
assert [ assert [
%{name: "baz"}, %{
%{name: "foo"} name: "baz",
other_name: %Ash.ForbiddenField{},
other_other_name: %Ash.ForbiddenField{}
},
%{name: "foo", other_name: %Ash.ForbiddenField{}, other_other_name: "foo"}
] = ] =
ResourceWithMultiplePoliciesForOneField.read!( ResourceWithMultiplePoliciesForOneField.read!(
actor: %{name: "baz", admin: true}, actor: %{name: "baz", admin: true},
query: ResourceWithMultiplePoliciesForOneField |> Ash.Query.sort([:name]) query: ResourceWithMultiplePoliciesForOneField |> Ash.Query.sort([:name])
) )
end end
defmodule ResourceWithMultiplePoliciesForOneFieldWithExtraCheckOptions do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
authorizers: [Ash.Policy.Authorizer]
attributes do
uuid_primary_key :id
attribute :name, :string
end
field_policies do
field_policy :name, [actor_attribute_equals(:admin, true)] do
authorize_if always()
end
# I saw in the generated spark_dsl_config, that the extra check options are
# set for expr inside the field policy, but not for conditions I think.
# The behaviour is different if I set them here.
field_policy :name,
{Ash.Policy.Check.Expression,
[
expr: expr(name == ^actor(:name)),
ash_field_policy?: true,
access_type: :filter
]} do
authorize_if always()
end
end
policies do
policy always() do
authorize_if always()
end
end
code_interface do
define_for Api
define :create
define :read
end
actions do
defaults [:create, :read]
end
end
test "multiple field policies for the same field with different conditions work (extra check options)" do
ResourceWithMultiplePoliciesForOneFieldWithExtraCheckOptions.create!(%{name: "foo"})
ResourceWithMultiplePoliciesForOneFieldWithExtraCheckOptions.create!(%{name: "baz"})
assert [
%{name: "baz"},
%{name: "foo"}
] =
ResourceWithMultiplePoliciesForOneFieldWithExtraCheckOptions.read!(
actor: %{name: "baz", admin: true},
query:
ResourceWithMultiplePoliciesForOneFieldWithExtraCheckOptions
|> Ash.Query.sort([:name])
)
end
end end