Skip to content

Commit 8b83efa

Browse files
authored
Merge commit from fork
The Policy Refactoring (#2365) introduced a bug where bypass policies were contributing only their condition to one_condition_matches instead of their complete expression (condition AND policies). This caused bypass policies to incorrectly satisfy the "at least one policy applies" requirement when their condition evaluates to true but their authorization checks fail. For example, with these policies: bypass always(), do: authorize_if(actor_attribute_equals(:is_admin, true)) policy action_type(:read), do: authorize_if(always()) The final authorization decision is: one_condition_matches AND all_policies_match When a bypass condition is true but bypass policies fail, and subsequent policies have non-matching conditions: 1. one_condition_matches = cond_expr (bypass condition) = true (bug - should check if bypass actually authorizes) 2. all_policies_match = (complete_expr OR NOT cond_expr) for each policy - For non-matching policies: (false OR NOT false) = true (policies don't apply) 3. Final: true AND true = true (incorrectly authorized) The bypass condition alone satisfies "at least one policy applies" even though the bypass fails to authorize. The fix ensures bypass policies contribute their complete expression (condition AND policies) to one_condition_matches.
1 parent b2e4d62 commit 8b83efa

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

lib/ash/policy/policy.ex

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,25 @@ defmodule Ash.Policy.Policy do
4949
{policy, cond_expr, complete_expr}
5050
end)
5151
|> List.foldr({false, true}, fn
52-
{%{bypass?: true}, cond_expr, complete_expr}, {one_condition_matches, true} ->
52+
{%{bypass?: true}, _cond_expr, complete_expr}, {one_condition_matches, true} ->
5353
{
54-
b(cond_expr or one_condition_matches),
55-
# Bypass can't relay to the next bypass if there is none
54+
b(complete_expr or one_condition_matches),
5655
complete_expr
5756
}
5857

5958
{%FieldPolicy{bypass?: true}, true, complete_expr},
6059
{one_condition_matches, all_policies_match} ->
6160
{
62-
# FieldPolicy Conditions are set to true by default and therefore
63-
# have to be ignore to not change the meaning of the
64-
# one_condition_matches condition
6561
one_condition_matches,
6662
b(complete_expr or all_policies_match)
6763
}
6864

69-
{%{bypass?: true}, cond_expr, complete_expr}, {one_condition_matches, all_policies_match} ->
65+
{%{bypass?: true}, _cond_expr, complete_expr},
66+
{one_condition_matches, all_policies_match} ->
7067
{
71-
b(cond_expr or one_condition_matches),
68+
# Bypass should only contribute to "at least one policy applies" if it actually authorizes.
69+
# Use complete_expr (condition AND policies) not just condition.
70+
b(complete_expr or one_condition_matches),
7271
b(complete_expr or all_policies_match)
7372
}
7473

test/policy/policy_test.exs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,4 +912,62 @@ defmodule Ash.Test.Policy.Policy do
912912
"""
913913
end
914914
end
915+
916+
describe "bypass policy with always-true condition" do
917+
test "denies when bypass condition is true but bypass policies fail" do
918+
# Scenario: non-admin user performs create action with these policies:
919+
#
920+
# bypass always() do
921+
# authorize_if actor_attribute_equals(:is_admin, true)
922+
# end
923+
#
924+
# policy action_type(:read) do
925+
# authorize_if always()
926+
# end
927+
#
928+
# Before fix: Used bypass condition (always) for one_condition_matches
929+
# → one_condition_matches = true (bypass condition matched)
930+
# → all_policies_match = true (no applicable policy)
931+
# → Final: true AND true = true (WRONG! - incorrectly authorized)
932+
#
933+
# After fix: Use bypass complete_expr (always AND is_admin) for one_condition_matches
934+
# → one_condition_matches = false (bypass failed to authorize)
935+
# → all_policies_match = true (no applicable policy)
936+
# → Final: false AND true = false (CORRECT! - properly denied)
937+
938+
policies = [
939+
# bypass always(), do: authorize_if(actor_attribute_equals(:is_admin, true))
940+
%Ash.Policy.Policy{
941+
bypass?: true,
942+
condition: [{Ash.Policy.Check.Static, result: true}],
943+
policies: [
944+
%Ash.Policy.Check{
945+
type: :authorize_if,
946+
check: {Ash.Policy.Check.Static, result: false},
947+
check_module: Ash.Policy.Check.Static,
948+
check_opts: [result: false]
949+
}
950+
]
951+
},
952+
# policy action_type(:read) do: authorize_if(always())
953+
%Ash.Policy.Policy{
954+
bypass?: false,
955+
condition: [{Ash.Policy.Check.Static, result: false}],
956+
policies: [
957+
%Ash.Policy.Check{
958+
type: :authorize_if,
959+
check: {Ash.Policy.Check.Static, result: true},
960+
check_module: Ash.Policy.Check.Static,
961+
check_opts: [result: true]
962+
}
963+
]
964+
}
965+
]
966+
967+
expression = Ash.Policy.Policy.expression(policies, %{})
968+
969+
# Should deny: bypass failed and no policy condition matched
970+
assert expression == false
971+
end
972+
end
915973
end

0 commit comments

Comments
 (0)