mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
fix: static checks with conditions could be overly or insufficiently restrictive
This commit is contained in:
parent
00a582fbc1
commit
a00806eeb0
6 changed files with 106 additions and 186 deletions
|
@ -682,8 +682,9 @@ defmodule Ash.Policy.Authorizer do
|
|||
end)
|
||||
# primary key doesn't have policies on it, and so is nil here
|
||||
|> Map.drop([nil, []])
|
||||
|> Enum.reduce({query_or_changeset, authorizer}, fn {policies, fields},
|
||||
{query_or_changeset, authorizer} ->
|
||||
|> Enum.reduce(
|
||||
{query_or_changeset, authorizer},
|
||||
fn {policies, fields}, {query_or_changeset, authorizer} ->
|
||||
{expr, authorizer} =
|
||||
case strict_check_result(
|
||||
%{
|
||||
|
@ -747,7 +748,8 @@ defmodule Ash.Policy.Authorizer do
|
|||
calculation
|
||||
), authorizer}
|
||||
end
|
||||
end)
|
||||
end
|
||||
)
|
||||
|> then(fn {result, authorizer} ->
|
||||
{:ok, result, authorizer}
|
||||
end)
|
||||
|
@ -785,15 +787,13 @@ defmodule Ash.Policy.Authorizer do
|
|||
{filterable, require_check} =
|
||||
authorizer.scenarios
|
||||
|> Enum.split_with(fn scenario ->
|
||||
scenario
|
||||
|> Enum.reject(fn {{check_module, opts}, _} ->
|
||||
Enum.all?(scenario, fn {{check_module, opts}, _} ->
|
||||
opts[:access_type] == :filter ||
|
||||
match?(
|
||||
{:ok, _},
|
||||
Ash.Policy.Policy.fetch_fact(authorizer.facts, {check_module, opts})
|
||||
) || check_module.type() == :filter
|
||||
end)
|
||||
|> Enum.empty?()
|
||||
end)
|
||||
|
||||
filter = strict_filters(filterable, authorizer)
|
||||
|
|
|
@ -66,7 +66,7 @@ defmodule Ash.Policy.Checker do
|
|||
scenario
|
||||
|> Map.drop([true, false])
|
||||
|> Enum.reduce_while(:reality, fn {fact, requirement}, status ->
|
||||
case Map.fetch(facts, fact) do
|
||||
case Policy.fetch_fact(facts, fact) do
|
||||
{:ok, ^requirement} ->
|
||||
{:cont, status}
|
||||
|
||||
|
|
|
@ -152,8 +152,8 @@ defmodule Ash.Policy.Policy do
|
|||
Enum.find_value(authorizer.facts, fn
|
||||
{{fact_mod, fact_opts}, result} ->
|
||||
if check_module == fact_mod &&
|
||||
Keyword.delete(fact_opts, :access_type) ==
|
||||
Keyword.delete(opts, :access_type) do
|
||||
Keyword.drop(fact_opts, [:access_type, :ash_field_policy?]) ==
|
||||
Keyword.drop(opts, [:access_type, :ash_field_policy?]) do
|
||||
{:ok, result}
|
||||
end
|
||||
|
||||
|
@ -198,8 +198,8 @@ defmodule Ash.Policy.Policy do
|
|||
Enum.find_value(facts, fn
|
||||
{{fact_mod, fact_opts}, result} ->
|
||||
if mod == fact_mod &&
|
||||
Keyword.delete(fact_opts, :access_type) ==
|
||||
Keyword.delete(opts, :access_type) do
|
||||
Keyword.drop(fact_opts, [:access_type, :ash_field_policy?]) ==
|
||||
Keyword.drop(opts, [:access_type, :ash_field_policy?]) do
|
||||
{:ok, result}
|
||||
end
|
||||
|
||||
|
@ -275,13 +275,14 @@ defmodule Ash.Policy.Policy do
|
|||
condition_expression ->
|
||||
case compile_policy_expression(policies, authorizer) do
|
||||
{true, authorizer} ->
|
||||
{condition_expression, authorizer}
|
||||
{true, authorizer}
|
||||
|
||||
{false, authorizer} ->
|
||||
{{:not, condition_expression}, authorizer}
|
||||
|
||||
{compiled_policies, authorizer} ->
|
||||
{{:and, condition_expression, compiled_policies}, authorizer}
|
||||
{{:or, {:and, condition_expression, compiled_policies}, {:not, condition_expression}},
|
||||
authorizer}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,7 +100,7 @@ defmodule Ash.Resource.Relationships.BelongsTo do
|
|||
def opt_schema, do: @opt_schema
|
||||
|
||||
@doc false
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
# sobelow_skip ["DOS.BinToAtom"]
|
||||
def transform(%{source_attribute: source_attribute, name: name} = relationship) do
|
||||
{:ok, %{relationship | source_attribute: source_attribute || :"#{name}_id"}}
|
||||
end
|
||||
|
|
|
@ -87,7 +87,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
def opt_schema, do: @opt_schema
|
||||
|
||||
@doc false
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
# sobelow_skip ["DOS.BinToAtom"]
|
||||
def transform(%{join_relationship: join_relationship, name: name} = relationship) do
|
||||
{:ok, %{relationship | join_relationship: join_relationship || :"#{name}_join_assoc"}}
|
||||
end
|
||||
|
|
|
@ -10,55 +10,6 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
|
|||
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
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets,
|
||||
|
@ -68,6 +19,8 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
|
|||
uuid_primary_key :id
|
||||
|
||||
attribute :name, :string
|
||||
attribute :other_name, :string
|
||||
attribute :other_other_name, :string
|
||||
end
|
||||
|
||||
field_policies do
|
||||
|
@ -78,10 +31,27 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
|
|||
field_policy :name, expr(name == ^actor(:name)) do
|
||||
authorize_if always()
|
||||
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
|
||||
|
||||
policies do
|
||||
policy always() do
|
||||
forbid_if never()
|
||||
authorize_if always()
|
||||
end
|
||||
end
|
||||
|
@ -99,80 +69,29 @@ defmodule Ash.Test.Policy.FieldPolicy.ExpressionConditionTest do
|
|||
end
|
||||
|
||||
test "multiple field policies for the same field with different conditions work" do
|
||||
ResourceWithMultiplePoliciesForOneField.create!(%{name: "foo"})
|
||||
ResourceWithMultiplePoliciesForOneField.create!(%{name: "baz"})
|
||||
ResourceWithMultiplePoliciesForOneField.create!(%{
|
||||
name: "foo",
|
||||
other_name: "foo",
|
||||
other_other_name: "foo"
|
||||
})
|
||||
|
||||
ResourceWithMultiplePoliciesForOneField.create!(%{
|
||||
name: "baz",
|
||||
other_name: "baz",
|
||||
other_other_name: "bar"
|
||||
})
|
||||
|
||||
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!(
|
||||
actor: %{name: "baz", admin: true},
|
||||
query: ResourceWithMultiplePoliciesForOneField |> Ash.Query.sort([:name])
|
||||
)
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue