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)
|
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)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue