High severityOSV Advisory· Published Oct 10, 2025· Updated Apr 15, 2026
CVE-2025-48043
CVE-2025-48043
Description
Incorrect Authorization vulnerability in ash-project ash allows Authentication Bypass. This vulnerability is associated with program files lib/ash/policy/authorizer/authorizer.ex and program routines 'Elixir.Ash.Policy.Authorizer':strict_filters/2.
This issue affects ash: from pkg:hex/ash@0 before pkg:hex/ash@3.6.2, before 3.6.2, before 66d81300065b970da0d2f4528354835d2418c7ae.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ashHex | < 3.6.2 | 3.6.2 |
Affected products
1- Range: 3.0.3, 3.4.56, v0.1.1, …
Patches
266d81300065bMerge commit from fork
4 files changed · +1095 −60
lib/ash/policy/authorizer/authorizer.ex+58 −54 modified@@ -1398,66 +1398,70 @@ defmodule Ash.Policy.Authorizer do end) |> Ash.Policy.SatSolver.simplify_clauses() |> Enum.reduce([], fn scenario, or_filters -> - scenario - |> Enum.map(fn - {{check_module, check_opts}, true} -> - result = - try do - nil_to_false(check_module.auto_filter(authorizer.actor, authorizer, check_opts)) - rescue - e -> - reraise Ash.Error.to_ash_error(e, __STACKTRACE__, - bread_crumbs: - "Creating filter for check: #{check_module.describe(check_opts)} on resource: #{authorizer.resource}" - ), - __STACKTRACE__ - end + if scenario == %{} do + [false | or_filters] + else + scenario + |> Enum.map(fn + {{check_module, check_opts}, true} -> + result = + try do + nil_to_false(check_module.auto_filter(authorizer.actor, authorizer, check_opts)) + rescue + e -> + reraise Ash.Error.to_ash_error(e, __STACKTRACE__, + bread_crumbs: + "Creating filter for check: #{check_module.describe(check_opts)} on resource: #{authorizer.resource}" + ), + __STACKTRACE__ + end - if is_nil(result) do - false - else - result - end + if is_nil(result) do + false + else + result + end - {{check_module, check_opts}, false} -> - result = - try do - if :erlang.function_exported(check_module, :auto_filter_not, 3) do - nil_to_false( - check_module.auto_filter_not(authorizer.actor, authorizer, check_opts) - ) - else - [ - not: - nil_to_false( - check_module.auto_filter(authorizer.actor, authorizer, check_opts) - ) - ] + {{check_module, check_opts}, false} -> + result = + try do + if :erlang.function_exported(check_module, :auto_filter_not, 3) do + nil_to_false( + check_module.auto_filter_not(authorizer.actor, authorizer, check_opts) + ) + else + [ + not: + nil_to_false( + check_module.auto_filter(authorizer.actor, authorizer, check_opts) + ) + ] + end + rescue + e -> + reraise Ash.Error.to_ash_error(e, __STACKTRACE__, + bread_crumbs: + "Creating filter for check: #{check_module.describe(check_opts)} on resource: #{authorizer.resource}" + ), + __STACKTRACE__ end - rescue - e -> - reraise Ash.Error.to_ash_error(e, __STACKTRACE__, - bread_crumbs: - "Creating filter for check: #{check_module.describe(check_opts)} on resource: #{authorizer.resource}" - ), - __STACKTRACE__ - end - if is_nil(result) do - false - else - result - end - end) - |> case do - [] -> - or_filters + if is_nil(result) do + false + else + result + end + end) + |> case do + [] -> + or_filters - [single] -> - [single | or_filters] + [single] -> + [single | or_filters] - filters -> - [[and: filters] | or_filters] + filters -> + [[and: filters] | or_filters] + end end end) end
lib/ash/policy/policy.ex+12 −6 modified@@ -41,7 +41,7 @@ defmodule Ash.Policy.Policy do end) |> case do {:ok, scenarios} -> - {:ok, scenarios, authorizer} + {:ok, Enum.uniq(scenarios), authorizer} {:error, error} -> {:error, authorizer, error} @@ -80,7 +80,7 @@ defmodule Ash.Policy.Policy do @doc false def transform(policy) do cond do - Enum.empty?(policy.policies) -> + policy.policies |> List.wrap() |> Enum.empty?() -> {:error, "Policies must have at least one check."} policy.bypass? && @@ -96,7 +96,7 @@ defmodule Ash.Policy.Policy do never have an effect. """} - policy.condition in [nil, []] -> + policy.condition |> List.wrap() |> Enum.empty?() -> {:ok, %{policy | condition: [{Ash.Policy.Check.Static, result: true}]}} true -> @@ -267,12 +267,18 @@ defmodule Ash.Policy.Policy do false end - defp compile_policy_expression([%struct{condition: condition, policies: policies}]) + defp compile_policy_expression([ + %struct{condition: condition, policies: policies, bypass?: bypass?} + ]) when struct in [__MODULE__, Ash.Policy.FieldPolicy] do condition_expression = condition_expression(condition) compiled_policies = compile_policy_expression(policies) - {:or, {:and, condition_expression, compiled_policies}, {:not, condition_expression}} + if bypass? do + {:and, condition_expression, compiled_policies} + else + {:or, {:and, condition_expression, compiled_policies}, {:not, condition_expression}} + end end defp compile_policy_expression([ @@ -336,7 +342,7 @@ defmodule Ash.Policy.Policy do |> clean_constant_checks() |> do_debug_expr() |> Macro.to_string() - |> then(&"#{label}: \n\n #{&1}") + |> then(&"#{label}:\n\n#{&1}") end defp clean_constant_checks({combinator, left, right}) when combinator in [:and, :or] do
test/policy/filter_condition_test.exs+108 −0 modified@@ -31,6 +31,90 @@ defmodule Ash.Test.Policy.FilterConditionTest do end end + defmodule RuntimeFalsyCheck do + @moduledoc false + use Ash.Policy.Check + + @impl Ash.Policy.Check + def describe(_), do: "returns false at runtime" + + @impl Ash.Policy.Check + def strict_check(_, _, _), do: {:ok, :unknown} + + @impl Ash.Policy.Check + def check(_actor, _list, _map, _options), do: [] + end + + defmodule FilterFalsyCheck do + @moduledoc false + use Ash.Policy.Check + + @impl Ash.Policy.Check + def describe(_), do: "returns false in a filter" + + @impl Ash.Policy.Check + def strict_check(_, _, _), do: {:ok, false} + end + + defmodule FinalBypassResource do + @moduledoc false + use Ash.Resource, + domain: Ash.Test.Policy.FilterConditionTest.Domain, + data_layer: Ash.DataLayer.Ets, + authorizers: [Ash.Policy.Authorizer] + + ets do + private?(true) + end + + actions do + default_accept :* + defaults([:read, :destroy, create: :*, update: :*]) + end + + attributes do + uuid_primary_key :id + end + + policies do + bypass always() do + access_type :runtime + description "Bypass never active" + authorize_if FilterFalsyCheck + end + end + end + + defmodule RuntimeBypassResource do + @moduledoc false + use Ash.Resource, + domain: Ash.Test.Policy.FilterConditionTest.Domain, + data_layer: Ash.DataLayer.Ets, + authorizers: [Ash.Policy.Authorizer] + + ets do + private?(true) + end + + actions do + default_accept :* + defaults([:read, :destroy, create: :*, update: :*]) + end + + attributes do + uuid_primary_key :id + end + + policies do + default_access_type :filter + + bypass RuntimeFalsyCheck do + description "Bypass never active" + authorize_if always() + end + end + end + defmodule Domain do @moduledoc false use Ash.Domain @@ -41,6 +125,8 @@ defmodule Ash.Test.Policy.FilterConditionTest do resources do resource Resource + resource RuntimeBypassResource + resource FinalBypassResource end end @@ -177,4 +263,26 @@ defmodule Ash.Test.Policy.FilterConditionTest do |> Ash.Changeset.for_update(:update, %{title: "title 2"}, actor: author) |> Ash.update() end + + test "bypass works with filter policies" do + RuntimeBypassResource + |> Ash.Changeset.for_create(:create, %{}, authorize?: false) + |> Ash.create!() + + assert [] = + RuntimeBypassResource + |> Ash.Query.for_read(:read, %{}, actor: nil, authorize?: true) + |> Ash.read!() + end + + test "bypass at the end works with filter policies" do + FinalBypassResource + |> Ash.Changeset.for_create(:create, %{}, authorize?: false) + |> Ash.create!() + + assert [] = + FinalBypassResource + |> Ash.Query.for_read(:read, %{}, actor: nil, authorize?: true) + |> Ash.read!() + end end
test/policy/policy_test.exs+917 −0 added@@ -0,0 +1,917 @@ +defmodule Ash.Test.Policy.Policy do + use ExUnit.Case, async: true + + import Ash.SatSolver, only: [b: 1] + + defmodule RuntimeCheck do + @moduledoc false + use Ash.Policy.Check + + @impl Ash.Policy.Check + def describe(_), do: "returns true at runtime" + + @impl Ash.Policy.Check + def strict_check(_, _, _), do: {:ok, :unknown} + + @impl Ash.Policy.Check + def check(_actor, list, _map, _options), do: list + end + + defmodule ErrorCheck do + @moduledoc false + use Ash.Policy.SimpleCheck + + @impl Ash.Policy.Check + def describe(_), do: "always errors" + + @impl Ash.Policy.SimpleCheck + def match?(_actor, _authorizer, _options), do: {:error, :something_went_wrong} + end + + describe inspect(&Ash.Policy.Policy.solve/1) do + test "returns directly for runtime expandable policies" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + policies: [ + %Ash.Policy.Policy{ + condition: [ + {Ash.Policy.Check.ActorPresent, []} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, true, + %Ash.Policy.Authorizer{facts: %{{Ash.Policy.Check.ActorPresent, []} => true}}} = + Ash.Policy.Policy.solve(%{authorization | actor: :actor}) + + assert {:ok, false, + %Ash.Policy.Authorizer{facts: %{{Ash.Policy.Check.ActorPresent, []} => false}}} = + Ash.Policy.Policy.solve(%{authorization | actor: nil}) + end + + test "multiple non-bypass policies - all conditions match" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "multiple non-bypass policies - some conditions match" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: false]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "multiple non-bypass policies - no conditions match" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: nil, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "bypass policy alone - when it passes" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "bypass policy alone - when condition fails" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: nil, + policies: [ + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "mix of bypass and regular policies - bypass succeeds and short-circuits" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "mix of bypass and regular policies - bypass fails, continues to regular" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: nil, + policies: [ + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "mutually exclusive conditions in same policy" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [ + {Ash.Policy.Check.ActorPresent, []}, + {Ash.Policy.Check.Static, [result: false]} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "forbid_if with true check forbids" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :forbid_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "forbid_if with false check does not forbid" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :forbid_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "authorize_unless with true check does not authorize" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_unless + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "authorize_unless with false check authorizes" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :authorize_unless + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "forbid_unless with true check does not forbid" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :forbid_unless + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "forbid_unless with false check forbids" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :forbid_unless + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "bypass with only forbid_if has no effect" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :forbid_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "multiple bypass policies - first succeeds" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "bypass policy after failing normal policy still fails" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.ActorPresent, []}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "mixed check types in single policy" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + }, + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :forbid_if + } + ] + } + ] + } + + assert {:ok, true, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "bypass with only forbid_unless has no effect" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + bypass?: true, + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :forbid_unless + } + ] + }, + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "empty policies list" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "authorize_if with false check" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + policies: [ + %Ash.Policy.Policy{ + condition: [{Ash.Policy.Check.Static, [result: true]}], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, _authorizer} = Ash.Policy.Policy.solve(authorization) + end + + test "only checks conditions that are required" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + policies: [ + %Ash.Policy.Policy{ + condition: [ + {Ash.Policy.Check.ActorPresent, []}, + {Ash.Policy.Check.ActorAttributeEquals, [attribute: :role, value: :admin]} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, false, %Ash.Policy.Authorizer{facts: facts}} = + Ash.Policy.Policy.solve(%{authorization | actor: nil}) + + assert %{{Ash.Policy.Check.ActorPresent, []} => false} = facts + + refute Map.has_key?( + facts, + {Ash.Policy.Check.ActorAttributeEquals, [attribute: :role, value: :admin]} + ) + + assert {:ok, false, %Ash.Policy.Authorizer{facts: facts}} = + Ash.Policy.Policy.solve(%{authorization | actor: %{role: :owner}}) + + assert %{{Ash.Policy.Check.ActorPresent, []} => true} = facts + + assert %{ + {Ash.Policy.Check.ActorAttributeEquals, [attribute: :role, value: :admin]} => false + } = facts + end + + test "declares scenarios and their requirements for uncertain output" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + policies: [ + %Ash.Policy.Policy{ + condition: [ + {RuntimeCheck, []} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [ + {RuntimeCheck, []} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }, + %Ash.Policy.Policy{ + condition: [ + {RuntimeCheck, [some: :config]} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:ok, required_conditions, _authorizer} = + Ash.Policy.Policy.solve(%{authorization | actor: nil}) + + assert Enum.any?(required_conditions, fn facts -> + Map.get(facts, {Ash.Test.Policy.Policy.RuntimeCheck, []}) == true + end) + + assert Enum.any?(required_conditions, fn facts -> + Map.get(facts, {Ash.Test.Policy.Policy.RuntimeCheck, [some: :config]}) == true + end) + end + + test "returns error if check failed" do + authorization = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + policies: [ + %Ash.Policy.Policy{ + condition: [ + {ErrorCheck, []} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + ] + } + + assert {:error, _authorizer, + %Ash.Error.Unknown.UnknownError{ + class: :unknown, + error: "unknown error: :something_went_wrong" + }} = + Ash.Policy.Policy.solve(%{authorization | actor: nil}) + end + end + + describe inspect(&Ash.Policy.Policy.transform/1) do + test "keeps good policy untouched" do + policy = %Ash.Policy.Policy{ + condition: [ + {Ash.Policy.Check.Static, [result: false]} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + + assert {:ok, ^policy} = Ash.Policy.Policy.transform(policy) + + bypass_policy = %Ash.Policy.Policy{ + bypass?: true, + condition: [ + {Ash.Policy.Check.Static, [result: false]} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + } + + assert {:ok, ^bypass_policy} = Ash.Policy.Policy.transform(bypass_policy) + end + + test "errors with no checks" do + assert {:error, "Policies must have at least one check."} = + Ash.Policy.Policy.transform(%Ash.Policy.Policy{}) + end + + test "does not allow bypass with only forbid" do + assert {:error, msg} = + Ash.Policy.Policy.transform(%Ash.Policy.Policy{ + bypass?: true, + condition: [ + {Ash.Policy.Check.Static, [result: false]} + ], + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: false]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: false], + type: :forbid_if + } + ] + }) + + assert msg =~ + "This policy only contains `forbid_if` or `forbid_unless` check types therefore" + end + + test "adds empty condition if none given" do + assert {:ok, %{condition: [{Ash.Policy.Check.Static, result: true}]}} = + Ash.Policy.Policy.transform(%Ash.Policy.Policy{ + policies: [ + %Ash.Policy.Check{ + check: {Ash.Policy.Check.Static, [result: true]}, + check_module: Ash.Policy.Check.Static, + check_opts: [result: true], + type: :authorize_if + } + ] + }) + end + end + + describe inspect(&Ash.Policy.Policy.fetch_or_strict_check_fact/2) do + test "calculates and stores new check content" do + check = {Ash.Policy.Check.ActorPresent, []} + + authorizer = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + actor: :actor, + facts: %{} + } + + assert Ash.Policy.Policy.fetch_or_strict_check_fact(authorizer, check) == + {:ok, true, %{authorizer | facts: %{check => true}}} + end + + test "gives stored fact" do + check = {RuntimeCheck, []} + + authorizer = %Ash.Policy.Authorizer{ + resource: Resource, + action: :read, + facts: %{check => false} + } + + assert Ash.Policy.Policy.fetch_or_strict_check_fact(authorizer, check) == + {:ok, false, authorizer} + end + end + + describe inspect(&Ash.Policy.Policy.fetch_fact/2) do + test "gives stored fact" do + assert {:ok, false} = + Ash.Policy.Policy.fetch_fact(%{{RuntimeCheck, []} => false}, {RuntimeCheck, []}) + end + + test "ignores access_type field" do + assert {:ok, false} = + Ash.Policy.Policy.fetch_fact( + %{{RuntimeCheck, []} => false}, + {RuntimeCheck, [access_type: :runtime]} + ) + end + + test "gives error if missing" do + assert :error = Ash.Policy.Policy.fetch_fact(%{}, {RuntimeCheck, []}) + end + end + + describe inspect(&Ash.Policy.Policy.debug_expr/2) do + test "generates readable expression" do + assert Ash.Policy.Policy.debug_expr( + b({RuntimeCheck, []} and ({RuntimeCheck, [some: :config]} or not true)) + ) == """ + Expr: + + "returns true at runtime" and ("returns true at runtime" or not true)\ + """ + end + end +end
35c098cc3ab3Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-7r7f-9xpj-jmr7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-48043ghsaADVISORY
- cna.erlef.org/cves/CVE-2025-48043.htmlnvdWEB
- github.com/ash-project/ash/commit/66d81300065b970da0d2f4528354835d2418c7aenvdWEB
- github.com/ash-project/ash/releases/tag/v3.6.2ghsaWEB
- github.com/ash-project/ash/security/advisories/GHSA-7r7f-9xpj-jmr7nvdWEB
- osv.dev/vulnerability/EEF-CVE-2025-48043nvdWEB
News mentions
0No linked articles in our index yet.