Skip to content

Commit 69bfd0d

Browse files
committed
fix: don't globally set tenant from record metadata when its not present
fixes #2662
1 parent 5577509 commit 69bfd0d

File tree

2 files changed

+112
-3
lines changed

2 files changed

+112
-3
lines changed

lib/ash.ex

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2889,7 +2889,13 @@ defmodule Ash do
28892889
Ash.Helpers.expect_options!(opts)
28902890
%resource{} = record
28912891
id = record |> Map.take(Ash.Resource.Info.primary_key(resource)) |> Enum.to_list()
2892-
opts = Keyword.put_new(opts, :tenant, Map.get(record.__metadata__, :tenant))
2892+
2893+
opts =
2894+
case Map.fetch(record.__metadata__, :tenant) do
2895+
{:ok, tenant} when not is_nil(tenant) -> Keyword.put_new(opts, :tenant, tenant)
2896+
_ -> opts
2897+
end
2898+
28932899
get(resource, id, opts)
28942900
end
28952901

@@ -3704,7 +3710,11 @@ defmodule Ash do
37043710
Ash.Helpers.expect_options!(opts)
37053711
Ash.Helpers.expect_map_or_nil!(opts[:input])
37063712

3707-
opts = Keyword.put_new(opts, :tenant, Map.get(record.__metadata__, :tenant))
3713+
opts =
3714+
case Map.fetch(record.__metadata__, :tenant) do
3715+
{:ok, tenant} when not is_nil(tenant) -> Keyword.put_new(opts, :tenant, tenant)
3716+
_ -> opts
3717+
end
37083718

37093719
changeset_opts = Keyword.take(opts, Keyword.keys(Ash.Changeset.for_update_opts()))
37103720
update_opts = Keyword.take(opts, Keyword.keys(@update_opts_schema))
@@ -3810,7 +3820,12 @@ defmodule Ash do
38103820
record -> record
38113821
end
38123822

3813-
opts = Keyword.put_new(opts, :tenant, Map.get(data.__metadata__, :tenant))
3823+
opts =
3824+
case Map.fetch(data.__metadata__, :tenant) do
3825+
{:ok, tenant} when not is_nil(tenant) -> Keyword.put_new(opts, :tenant, tenant)
3826+
_ -> opts
3827+
end
3828+
38143829
Ash.Helpers.expect_options!(opts)
38153830

38163831
changeset =

test/scope_test.exs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,44 @@
55
defmodule Ash.ScopeTest do
66
use ExUnit.Case, async: true
77

8+
alias Ash.Test.Domain, as: Domain
9+
10+
defmodule MyScope do
11+
defstruct [:actor, :tenant]
12+
13+
defimpl Ash.Scope.ToOpts do
14+
def get_actor(%{actor: actor}), do: {:ok, actor}
15+
def get_tenant(%{tenant: tenant}), do: {:ok, tenant}
16+
def get_context(_), do: :error
17+
def get_tracer(_), do: :error
18+
def get_authorize?(_), do: {:ok, false}
19+
end
20+
end
21+
22+
defmodule MultiTenantResource do
23+
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets
24+
25+
multitenancy do
26+
strategy :attribute
27+
attribute :tenant_id
28+
end
29+
30+
ets do
31+
private?(true)
32+
end
33+
34+
attributes do
35+
uuid_primary_key :id
36+
attribute :name, :string, public?: true
37+
attribute :tenant_id, :string, allow_nil?: false, public?: true
38+
end
39+
40+
actions do
41+
default_accept :*
42+
defaults [:read, :destroy, create: :*, update: :*]
43+
end
44+
end
45+
846
describe "Ash.Scope.to_opts/1 with Map" do
947
test "handles nested shared context pattern" do
1048
context = %{
@@ -98,6 +136,62 @@ defmodule Ash.ScopeTest do
98136
end
99137
end
100138

139+
describe "scope with update (issue #2662)" do
140+
test "scope tenant is used when record metadata has no tenant" do
141+
record =
142+
MultiTenantResource
143+
|> Ash.Changeset.for_create(:create, %{name: "original", tenant_id: "tenant_1"},
144+
tenant: "tenant_1"
145+
)
146+
|> Ash.create!()
147+
148+
# Simulate a record whose metadata does not have a tenant set
149+
# (e.g. loaded from a context where tenant wasn't propagated)
150+
record = put_in(record.__metadata__[:tenant], nil)
151+
152+
scope = %MyScope{actor: nil, tenant: "tenant_1"}
153+
154+
assert {:ok, updated} =
155+
Ash.update(record, %{name: "updated"}, scope: scope)
156+
157+
assert updated.name == "updated"
158+
end
159+
160+
test "scope tenant is used via Ash.Scope.to_opts workaround" do
161+
record =
162+
MultiTenantResource
163+
|> Ash.Changeset.for_create(:create, %{name: "original", tenant_id: "tenant_1"},
164+
tenant: "tenant_1"
165+
)
166+
|> Ash.create!()
167+
168+
record = put_in(record.__metadata__[:tenant], nil)
169+
170+
scope = %MyScope{actor: nil, tenant: "tenant_1"}
171+
opts = Ash.Scope.to_opts(scope, action: :update)
172+
173+
assert {:ok, updated} = Ash.update(record, %{name: "updated"}, opts)
174+
assert updated.name == "updated"
175+
end
176+
end
177+
178+
describe "scope with destroy (issue #2662)" do
179+
test "scope tenant is used when record metadata has no tenant" do
180+
record =
181+
MultiTenantResource
182+
|> Ash.Changeset.for_create(:create, %{name: "to_delete", tenant_id: "tenant_1"},
183+
tenant: "tenant_1"
184+
)
185+
|> Ash.create!()
186+
187+
record = put_in(record.__metadata__[:tenant], nil)
188+
189+
scope = %MyScope{actor: nil, tenant: "tenant_1"}
190+
191+
assert :ok = Ash.destroy(record, scope: scope)
192+
end
193+
end
194+
101195
describe "Ash.Context.to_opts/1 (deprecated)" do
102196
test "delegates to Ash.Scope.to_opts/1" do
103197
context = %{

0 commit comments

Comments
 (0)