Revert "improvement: make authorization failures behave consistently across reads"

This reverts commit ffa37d0c95.
This commit is contained in:
Zach Daniel 2024-08-30 19:51:46 -04:00
parent ffa37d0c95
commit 4adddcdd69
12 changed files with 66 additions and 125 deletions

View file

@ -299,12 +299,6 @@ defmodule Ash.Policy.Authorizer do
Ash.Expr Ash.Expr
], ],
schema: [ schema: [
statically_deniable_reads: [
type: {:one_of, [:error, :filter]},
default: :filter,
doc:
"When a read request is denied immediately, i.e we know its forbidden without ever running a query. `:filter` will filter all records out, and `:error` will result in a forbidden error."
],
default_access_type: [ default_access_type: [
type: {:one_of, [:strict, :filter, :runtime]}, type: {:one_of, [:strict, :filter, :runtime]},
default: :filter, default: :filter,
@ -1175,7 +1169,7 @@ defmodule Ash.Policy.Authorizer do
{_filters, _require_check} -> {_filters, _require_check} ->
case global_filters(authorizer) do case global_filters(authorizer) do
nil -> nil ->
maybe_forbid_strict(authorizer) |> IO.inspect() maybe_forbid_strict(authorizer)
{filters, scenarios_without_global} -> {filters, scenarios_without_global} ->
with {:ok, %Ash.Filter{expression: filter}} <- with {:ok, %Ash.Filter{expression: filter}} <-
@ -1504,30 +1498,12 @@ defmodule Ash.Policy.Authorizer do
end end
defp strict_check_result(authorizer, opts \\ []) do defp strict_check_result(authorizer, opts \\ []) do
case authorizer.policies do
[] ->
error =
Ash.Error.Forbidden.Policy.exception(
facts: authorizer.facts,
policies: authorizer.policies,
context_description: opts[:context_description],
for_fields: opts[:for_fields],
resource: Map.get(authorizer, :resource),
actor: Map.get(authorizer, :action),
action: Map.get(authorizer, :action),
scenarios: []
)
{:error, error}
_ ->
case Checker.strict_check_scenarios(authorizer) do case Checker.strict_check_scenarios(authorizer) do
{:ok, true, authorizer} -> {:ok, true, authorizer} ->
{:authorized, authorizer} {:authorized, authorizer}
{:ok, none, authorizer} when none in [false, []] -> {:ok, none, authorizer} when none in [false, []] ->
# we construct the error to log policy breakdown {:error,
error =
Ash.Error.Forbidden.Policy.exception( Ash.Error.Forbidden.Policy.exception(
facts: authorizer.facts, facts: authorizer.facts,
policies: authorizer.policies, policies: authorizer.policies,
@ -1537,15 +1513,7 @@ defmodule Ash.Policy.Authorizer do
actor: Map.get(authorizer, :action), actor: Map.get(authorizer, :action),
action: Map.get(authorizer, :action), action: Map.get(authorizer, :action),
scenarios: [] scenarios: []
) )}
if authorizer.action.type == :read and
Ash.Policy.Info.statically_deniable_reads(authorizer.resource) ==
:filter do
{:filter, authorizer, false}
else
{:error, error}
end
{:ok, scenarios, authorizer} -> {:ok, scenarios, authorizer} ->
case Checker.find_real_scenarios(scenarios, authorizer.facts) do case Checker.find_real_scenarios(scenarios, authorizer.facts) do
@ -1557,32 +1525,22 @@ defmodule Ash.Policy.Authorizer do
end end
{:error, authorizer, :unsatisfiable} -> {:error, authorizer, :unsatisfiable} ->
# we construct the error to log policy breakdown {:error,
error =
Ash.Error.Forbidden.Policy.exception( Ash.Error.Forbidden.Policy.exception(
facts: authorizer.facts, facts: authorizer.facts,
policies: authorizer.policies, policies: authorizer.policies,
context_description: opts[:context_description], context_description: opts[:context_description],
for_fields: opts[:for_fields], for_fields: opts[:for_fields],
resource: Map.get(authorizer, :resource), resource: Map.get(authorizer, :resource),
actor: Map.get(authorizer, :action),
action: Map.get(authorizer, :action), action: Map.get(authorizer, :action),
actor: Map.get(authorizer, :action),
scenarios: [] scenarios: []
) )}
if authorizer.action.type == :read and
Ash.Policy.Info.statically_deniable_reads(authorizer.resource) ==
:filter do
{:filter, authorizer, false}
else
{:error, error}
end
{:error, _authorizer, exception} -> {:error, _authorizer, exception} ->
{:error, Ash.Error.to_ash_error(exception)} {:error, Ash.Error.to_ash_error(exception)}
end end
end end
end
defp maybe_strict_filter(authorizer, scenarios) do defp maybe_strict_filter(authorizer, scenarios) do
strict_filter(%{authorizer | scenarios: scenarios}) strict_filter(%{authorizer | scenarios: scenarios})

View file

@ -188,10 +188,6 @@ defmodule Ash.Policy.Info do
Extension.get_opt(resource, [:policies], :default_access_type, :filter, false) Extension.get_opt(resource, [:policies], :default_access_type, :filter, false)
end end
def statically_deniable_reads(resource) do
Extension.get_opt(resource, [:policies], :statically_deniable_reads, :filter, false)
end
# This should be done at compile time # This should be done at compile time
defp set_access_type(policies, default) when is_list(policies) do defp set_access_type(policies, default) when is_list(policies) do
Enum.map(policies, &set_access_type(&1, default)) Enum.map(policies, &set_access_type(&1, default))

View file

@ -568,7 +568,7 @@ defmodule Ash.Query do
def timeout(query, timeout) do def timeout(query, timeout) do
query = new(query) query = new(query)
if is_nil(timeout) || Ash.DataLayer.data_layer_can?(query.resource, :timeout) do if Ash.DataLayer.data_layer_can?(query.resource, :timeout) || is_nil(timeout) do
%{query | timeout: timeout} %{query | timeout: timeout}
else else
add_error(query, TimeoutNotSupported.exception(resource: query.resource)) add_error(query, TimeoutNotSupported.exception(resource: query.resource))

View file

@ -336,8 +336,7 @@ defmodule Ash.MixProject do
defp deps do defp deps do
[ [
# DSLs # DSLs
# {:spark, "~> 2.1 and >= 2.2.22"}, {:spark, "~> 2.1 and >= 2.2.22"},
{:spark, path: "../spark", override: true},
# Ash resources are backed by ecto scheams # Ash resources are backed by ecto scheams
{:ecto, "~> 3.7"}, {:ecto, "~> 3.7"},
# Used by the ETS data layer # Used by the ETS data layer

View file

@ -792,8 +792,6 @@ defmodule Ash.Test.Actions.LoadTest do
assert %{campaign_upcase: "HELLO WORLD"} = author assert %{campaign_upcase: "HELLO WORLD"} = author
Application.put_env(:foo, :bar, true)
assert %{campaign_upcase: "HELLO WORLD"} = assert %{campaign_upcase: "HELLO WORLD"} =
Ash.load!(author, [:campaign_upcase], lazy?: true) Ash.load!(author, [:campaign_upcase], lazy?: true)
end end

View file

@ -76,7 +76,7 @@ defmodule Ash.Test.Policy.Actions.BelongsToTest do
}) })
|> Ash.create!() |> Ash.create!()
assert_raise Ash.Error.NotFound, fn -> assert_raise Ash.Error.Forbidden, fn ->
post post
|> Ash.Changeset.for_update(:update_with_reviewer, %{ |> Ash.Changeset.for_update(:update_with_reviewer, %{
reviewer_id: reviewer.id reviewer_id: reviewer.id

View file

@ -90,9 +90,12 @@ defmodule Ash.Test.Policy.ComplexTest do
end end
test "it applies policies from the domain", %{me: me} do test "it applies policies from the domain", %{me: me} do
assert [] == assert_raise Ash.Error.Forbidden,
~r/authorize unless: actor.forbidden_by_domain == true | ✓ |/,
fn ->
Ash.read!(Post, actor: %{me | forbidden_by_domain: true}) Ash.read!(Post, actor: %{me | forbidden_by_domain: true})
end end
end
test "it properly limits on reads of comments", %{ test "it properly limits on reads of comments", %{
me: me, me: me,
@ -184,7 +187,8 @@ defmodule Ash.Test.Policy.ComplexTest do
me |> Ash.load!([:bio_text], authorize?: true, actor: me) me |> Ash.load!([:bio_text], authorize?: true, actor: me)
assert [] == assert_raise Ash.Error.Forbidden, fn ->
Ash.read!(Bio, actor: me, authorize?: true) Ash.read!(Bio, actor: me, authorize?: true)
end end
end end
end

View file

@ -53,7 +53,9 @@ defmodule Ash.Test.Policy.RbacTest do
|> Ash.Query.filter(id == ^org.id) |> Ash.Query.filter(id == ^org.id)
|> Ash.Query.load(files: File |> Ash.Query.select([:forbidden])) |> Ash.Query.load(files: File |> Ash.Query.select([:forbidden]))
assert [%{files: []}] = Ash.read!(query, actor: user) assert_raise Ash.Error.Forbidden, fn ->
Ash.read!(query, actor: user) == []
end
# specify no select (everything is selected) # specify no select (everything is selected)
query = query =
@ -61,7 +63,9 @@ defmodule Ash.Test.Policy.RbacTest do
|> Ash.Query.filter(id == ^org.id) |> Ash.Query.filter(id == ^org.id)
|> Ash.Query.load([:files]) |> Ash.Query.load([:files])
assert [%{files: []}] = Ash.read!(query, actor: user) assert_raise Ash.Error.Forbidden, fn ->
Ash.read!(query, actor: user) == []
end
# select only an allowed field # select only an allowed field
query = query =

View file

@ -132,7 +132,7 @@ defmodule Ash.Test.Policy.SelectingTest do
refute is_nil(parent.owner_only_resource) refute is_nil(parent.owner_only_resource)
end end
test "guest is forbidden from seeing a forbidden field on the rel" do test "guest is forbidden from querying if selecting a forbidden field on the rel" do
parent = parent =
Parent Parent
|> Ash.Changeset.for_create(:create, %{owner_id: "owner", guest_id: "guest"}) |> Ash.Changeset.for_create(:create, %{owner_id: "owner", guest_id: "guest"})
@ -144,13 +144,11 @@ defmodule Ash.Test.Policy.SelectingTest do
|> Ash.Changeset.for_create(:create) |> Ash.Changeset.for_create(:create)
|> Ash.create!(authorize?: false) |> Ash.create!(authorize?: false)
assert {:ok, parent} = assert {:error, %Ash.Error.Forbidden{}} =
Parent Parent
|> Ash.Query.for_read(:read) |> Ash.Query.for_read(:read)
|> Ash.Query.load(:owner_only_resource) |> Ash.Query.load(:owner_only_resource)
|> Ash.Query.limit(1) |> Ash.Query.limit(1)
|> Ash.read_one(actor: %{id: "guest"}) |> Ash.read_one(actor: %{id: "guest"})
assert %Ash.ForbiddenField{} = parent.owner_only_resource
end end
end end

View file

@ -23,18 +23,6 @@ defmodule Ash.Test.Policy.SimpleTest do
] ]
end end
test "statically known policies will filter all results", %{user: user} do
Post
|> Ash.Changeset.for_create(:create, %{author: user.id, text: "aaa"})
|> Ash.create!(authorize?: false)
Post
|> Ash.Changeset.for_create(:create, %{author: user.id, text: "aaa"})
|> Ash.create!(authorize?: false)
Ash.read!(Post, action: :never_allowed, actor: user)
end
test "bypass with condition does not apply subsequent filters", %{admin: admin, user: user} do test "bypass with condition does not apply subsequent filters", %{admin: admin, user: user} do
Ash.create!(Ash.Changeset.for_create(Tweet, :create), authorize?: false) Ash.create!(Ash.Changeset.for_create(Tweet, :create), authorize?: false)
@ -332,10 +320,11 @@ defmodule Ash.Test.Policy.SimpleTest do
|> Ash.Changeset.for_create(:create, %{user_id: user.id}) |> Ash.Changeset.for_create(:create, %{user_id: user.id})
|> Ash.create!(authorize?: false) |> Ash.create!(authorize?: false)
assert [] = assert_raise Ash.Error.Forbidden, fn ->
Ash.Test.Support.PolicySimple.Always Ash.Test.Support.PolicySimple.Always
|> Ash.read!(authorize?: true, actor: user) |> Ash.read!(authorize?: true, actor: user)
end end
end
test "two filter condition checks combine properly" do test "two filter condition checks combine properly" do
user1 = Ash.create!(Ash.Changeset.for_create(User, :create), authorize?: false) user1 = Ash.create!(Ash.Changeset.for_create(User, :create), authorize?: false)

View file

@ -42,9 +42,10 @@ defmodule Ash.Test.Policy.StrictConditionTest do
|> Ash.Changeset.for_create(:create, %{visible: false}, authorize?: false) |> Ash.Changeset.for_create(:create, %{visible: false}, authorize?: false)
|> Ash.create!() |> Ash.create!()
assert [] == assert_raise Ash.Error.Forbidden, fn ->
Resource Resource
|> Ash.Query.for_read(:read, %{}, actor: %{id: "foo"}) |> Ash.Query.for_read(:read, %{}, actor: %{id: "foo"})
|> Ash.read!() |> Ash.read!()
end end
end end
end

View file

@ -25,10 +25,6 @@ defmodule Ash.Test.Support.PolicySimple.Post do
forbid_if expr(^arg(:from_an_admin?)) forbid_if expr(^arg(:from_an_admin?))
authorize_if always() authorize_if always()
end end
policy action(:never_allowed) do
authorize_if false
end
end end
ets do ets do
@ -69,8 +65,6 @@ defmodule Ash.Test.Support.PolicySimple.Post do
end end
end end
end end
read :never_allowed
end end
relationships do relationships do