Skip to content

Commit cb0dbc5

Browse files
authored
fix: handle unrelated exists in authorizer ref replacement (#2556)
Prior to this commit, replace_refs in the policy authorizer did not handle a case for unrelated exists expressions (no :path or :at_path).
1 parent bfdfa55 commit cb0dbc5

File tree

2 files changed

+154
-7
lines changed

2 files changed

+154
-7
lines changed

lib/ash/policy/authorizer/authorizer.ex

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -991,18 +991,33 @@ defmodule Ash.Policy.Authorizer do
991991
{simple_expression, acc} = replace_refs(simple_expression, acc)
992992
{%{custom_expression | expression: expression, simple_expression: simple_expression}, acc}
993993

994+
%Ash.Query.Exists{expr: expr, resource: resource, related?: false} = exists ->
995+
original_stack = acc.stack
996+
[{_, _, _, domain} | _] = original_stack
997+
domain = Ash.Resource.Info.domain(resource) || domain
998+
action = Ash.Resource.Info.primary_action!(resource, :read)
999+
1000+
{expr, acc} =
1001+
replace_refs(expr, %{
1002+
acc
1003+
| stack: [{resource, [], action, domain} | original_stack]
1004+
})
1005+
1006+
{%{exists | expr: expr}, %{acc | stack: original_stack}}
1007+
9941008
%Ash.Query.Exists{expr: expr, at_path: at_path, path: path} = exists ->
9951009
full_path = at_path ++ path
996-
[{resource, current_path, _, domain} | _] = acc.stack
1010+
original_stack = acc.stack
1011+
[{resource, current_path, _, domain} | _] = original_stack
9971012
{resource, action} = related_with_action(resource, full_path)
9981013

9991014
{expr, acc} =
10001015
replace_refs(expr, %{
10011016
acc
1002-
| stack: [{resource, current_path ++ full_path, action, domain} | acc.stack]
1017+
| stack: [{resource, current_path ++ full_path, action, domain} | original_stack]
10031018
})
10041019

1005-
{%{exists | expr: expr}, %{acc | stack: tl(acc.stack)}}
1020+
{%{exists | expr: expr}, %{acc | stack: original_stack}}
10061021

10071022
%{__operator__?: true, left: left, right: right} = op ->
10081023
{left, acc} = replace_refs(left, acc)

test/resource/unrelated_exists_test.exs

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ defmodule Ash.Test.Resource.UnrelatedExistsTest do
8181

8282
defmodule User do
8383
@moduledoc false
84-
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets
84+
use Ash.Resource,
85+
domain: Domain,
86+
data_layer: Ash.DataLayer.Ets,
87+
authorizers: [Ash.Policy.Authorizer]
8588

8689
ets do
8790
private? true
@@ -93,11 +96,80 @@ defmodule Ash.Test.Resource.UnrelatedExistsTest do
9396
attribute :age, :integer, public?: true
9497
attribute :email, :string, public?: true
9598
attribute :role, :atom, default: :user, public?: true
99+
attribute :secret, :integer, public?: true
100+
end
101+
102+
actions do
103+
defaults [:read, :destroy, create: :*, update: :*]
104+
end
105+
106+
policies do
107+
policy always() do
108+
authorize_if always()
109+
end
110+
end
111+
112+
field_policies do
113+
field_policy :* do
114+
authorize_if always()
115+
end
116+
117+
field_policy :name do
118+
authorize_if action([:create, :read])
119+
end
120+
121+
field_policy :secret do
122+
authorize_if actor_attribute_equals(:role, :admin)
123+
authorize_if action([:create])
124+
end
125+
end
126+
127+
relationships do
128+
has_many :user_votes, Ash.Test.Resource.UnrelatedExistsTest.UserVote do
129+
public? true
130+
end
131+
end
132+
end
133+
134+
defmodule UserVote do
135+
@moduledoc false
136+
use Ash.Resource,
137+
domain: Domain,
138+
data_layer: Ash.DataLayer.Ets,
139+
authorizers: [Ash.Policy.Authorizer]
140+
141+
ets do
142+
private? true
143+
end
144+
145+
attributes do
146+
uuid_primary_key :id
147+
attribute :score, :integer, public?: true
96148
end
97149

98150
actions do
99151
defaults [:read, :destroy, create: :*, update: :*]
100152
end
153+
154+
policies do
155+
policy always() do
156+
authorize_if always()
157+
end
158+
end
159+
160+
field_policies do
161+
field_policy :* do
162+
authorize_if always()
163+
end
164+
165+
field_policy :score do
166+
authorize_if actor_attribute_equals(:role, :admin)
167+
end
168+
end
169+
170+
relationships do
171+
belongs_to :user, User, primary_key?: true, allow_nil?: false, public?: true
172+
end
101173
end
102174

103175
describe "basic unrelated exists expressions" do
@@ -212,9 +284,14 @@ defmodule Ash.Test.Resource.UnrelatedExistsTest do
212284

213285
describe "unrelated exists with filter_input" do
214286
setup do
215-
admin = Ash.create!(User, %{name: "Admin", email: "[email protected]", role: :admin})
216-
user1 = Ash.create!(User, %{name: "User1", email: "[email protected]", role: :user})
217-
user2 = Ash.create!(User, %{name: "User2", email: "[email protected]", role: :user})
287+
admin =
288+
Ash.create!(User, %{name: "Admin", email: "[email protected]", role: :admin, secret: 40})
289+
290+
user1 =
291+
Ash.create!(User, %{name: "User1", email: "[email protected]", role: :user, secret: 25})
292+
293+
user2 =
294+
Ash.create!(User, %{name: "User2", email: "[email protected]", role: :user, secret: 30})
218295

219296
Ash.create!(Profile, %{
220297
name: "Admin",
@@ -349,6 +426,61 @@ defmodule Ash.Test.Resource.UnrelatedExistsTest do
349426

350427
assert length(users) == 3
351428
end
429+
430+
test "filter_input with unrelated exists respects field level authorization", %{
431+
admin: admin,
432+
user1: user1
433+
} do
434+
user1_users =
435+
User
436+
|> Ash.Query.filter_input(expr(exists(Profile, age == parent(secret))))
437+
|> Ash.read!(actor: user1, authorize?: true)
438+
439+
assert user1_users == []
440+
441+
admin_users =
442+
User
443+
|> Ash.Query.filter_input(expr(exists(Profile, age == parent(secret))))
444+
|> Ash.read!(actor: admin, authorize?: true)
445+
446+
assert length(admin_users) == 3
447+
end
448+
449+
test "filter_input with nested unrelated exists respects field level authorization", %{
450+
admin: admin,
451+
user1: user1
452+
} do
453+
UserVote
454+
|> Ash.create!(%{user_id: user1.id, score: 2})
455+
456+
user1_users =
457+
User
458+
|> Ash.Query.filter_input(
459+
expr(
460+
exists(
461+
user_votes,
462+
score > 1 and exists(UserVote, score > 1)
463+
)
464+
)
465+
)
466+
|> Ash.read!(actor: user1, authorize?: true)
467+
468+
assert user1_users == []
469+
470+
admin_users =
471+
User
472+
|> Ash.Query.filter_input(
473+
expr(
474+
exists(
475+
user_votes,
476+
score > 1 and exists(UserVote, score > 1)
477+
)
478+
)
479+
)
480+
|> Ash.read!(actor: admin, authorize?: true)
481+
482+
assert length(admin_users) == 1
483+
end
352484
end
353485

354486
describe "unrelated exists in calculations" do

0 commit comments

Comments
 (0)