Skip to content

Commit d291265

Browse files
committed
Inherit Tenanted::GlobalId::Locator from UnscopedLocator
`Tenanted::GlobalId::Locator` subclassed nothing, so it inherited none of GlobalID's locator behavior. It did not implement `#model_class`, which GlobalID 1.4.0 deprecated. It did not implement `#locate_many`, which `GlobalID::Locator.fetch` requires, so Active Job on Rails edge could not deserialize GlobalID arguments. It also applied a model's `default_scope` to GlobalID lookups, unlike Rails, whose default locator resolves GlobalIDs unscoped. `Tenanted::GlobalId::Locator` will inherit from `GlobalID::Locator::UnscopedLocator`, the locator Rails installs by default, and will keep only the tenant safety check. `#locate_many` will enforce the same tenant checks as `#locate`.
1 parent 4e5faff commit d291265

4 files changed

Lines changed: 223 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# `activerecord-tenanted` Changelog
22

3+
## next / unreleased
4+
5+
### Fixed
6+
7+
- `Tenanted::GlobalId::Locator` now inherits from `GlobalID::Locator::UnscopedLocator`, which is the locator Rails uses by default. Two behavior changes follow: GlobalID lookups no longer apply a model's `default_scope`, matching Rails; and `#locate_many` is now implemented, which Active Job on Rails edge requires to deserialize GlobalID arguments. `#locate_many` enforces the same tenant safety checks as `#locate`. @flavorjones
8+
- `Tenanted::GlobalId::Locator` no longer emits a deprecation warning from GlobalID 1.4.0 about the missing `model_class` method. @flavorjones
9+
10+
311
## v0.7.0 / 2026-06-08
412

513
### Security

lib/active_record/tenanted/global_id.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ def tenant
99
params && params[:tenant]
1010
end
1111

12-
class Locator
12+
class Locator < ::GlobalID::Locator::UnscopedLocator
1313
def locate(gid, options = {})
1414
ensure_tenant_context_safety(gid)
15-
gid.model_class.find(gid.model_id)
15+
super
16+
end
17+
18+
def locate_many(gids, options = {})
19+
gids.each { |gid| ensure_tenant_context_safety(gid) }
20+
super
1621
end
1722

1823
private

test/integration/test/active_job_test.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ class NoteCheerioJobTest < ActiveJob::TestCase
3535

3636
e = assert_raises(ActiveJob::DeserializationError) { perform_enqueued_jobs }
3737

38-
# this will be a RecordNotFound if the GlobalID locator is not installed correctly
39-
assert_kind_of(ActiveRecord::Tenanted::WrongTenantError, e.cause)
38+
assert_tenant_error(ActiveRecord::Tenanted::WrongTenantError, e)
4039
end
4140

4241
test "global id locator catches untenanted context" do
@@ -51,8 +50,16 @@ class NoteCheerioJobTest < ActiveJob::TestCase
5150

5251
e = assert_raises(ActiveJob::DeserializationError) { perform_enqueued_jobs }
5352

54-
# this will be a RecordNotFound if the GlobalID locator is not installed correctly
55-
# this will be a WrongTenantError if the active job test helper isn't installed correctly
56-
assert_kind_of(ActiveRecord::Tenanted::NoTenantError, e.cause)
53+
assert_tenant_error(ActiveRecord::Tenanted::NoTenantError, e)
5754
end
55+
56+
private
57+
# GlobalID::Locator.fetch may re-raise our error wrapped in RecordUnavailable.
58+
def assert_tenant_error(error_class, error)
59+
chain = [ error ]
60+
chain << chain.last.cause while chain.last.cause
61+
62+
assert(chain.any? { |e| e.is_a?(error_class) },
63+
"Expected #{error_class} in #{chain.map(&:class).inspect}")
64+
end
5865
end

test/unit/global_id_test.rb

Lines changed: 196 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,53 +55,222 @@
5555
end
5656

5757
describe ActiveRecord::Tenanted::GlobalId::Locator do
58+
with_scenario(:primary_db, :primary_record) do
59+
test "does not deprecate" do
60+
TenantedApplicationRecord.create_tenant("foo") do
61+
user = User.create!(email: "user1@example.org")
62+
63+
assert_not_deprecated(GlobalID.deprecator) do
64+
ActiveRecord::Tenanted::GlobalId::Locator.new.locate(user.to_global_id)
65+
end
66+
end
67+
end
68+
end
69+
5870
for_each_scenario do
59-
describe "given an untenanted GID" do
60-
test "raises MissingTenantError" do
61-
gid = GlobalID.parse("gid://dummy/User/1")
71+
describe "#locate" do
72+
describe "given an untenanted GID" do
73+
test "raises MissingTenantError" do
74+
gid = GlobalID.parse("gid://dummy/User/1")
6275

63-
TenantedApplicationRecord.create_tenant("foo") do
64-
assert_raises(ActiveRecord::Tenanted::MissingTenantError) do
65-
ActiveRecord::Tenanted::GlobalId::Locator.new.locate(gid)
76+
TenantedApplicationRecord.create_tenant("foo") do
77+
assert_raises(ActiveRecord::Tenanted::MissingTenantError) do
78+
ActiveRecord::Tenanted::GlobalId::Locator.new.locate(gid)
79+
end
6680
end
6781
end
6882
end
69-
end
7083

71-
describe "in correct tenanted context" do
72-
test "loads correctly" do
73-
TenantedApplicationRecord.create_tenant("foo") do
74-
original_user = User.create!(email: "user1@example.org")
75-
user = ActiveRecord::Tenanted::GlobalId::Locator.new.locate(original_user.to_global_id)
84+
describe "in correct tenanted context" do
85+
test "loads correctly" do
86+
TenantedApplicationRecord.create_tenant("foo") do
87+
original_user = User.create!(email: "user1@example.org")
88+
user = ActiveRecord::Tenanted::GlobalId::Locator.new.locate(original_user.to_global_id)
89+
90+
assert_equal(original_user, user)
91+
end
92+
end
93+
end
94+
95+
describe "in wrong tenanted context" do
96+
test "raises WrongTenantError" do
97+
original_user = TenantedApplicationRecord.create_tenant("foo") do
98+
User.create!(email: "user1@example.org")
99+
end
76100

77-
assert_equal(original_user, user)
101+
TenantedApplicationRecord.create_tenant("bar") do
102+
assert_raises(ActiveRecord::Tenanted::WrongTenantError) do
103+
ActiveRecord::Tenanted::GlobalId::Locator.new.locate(original_user.to_global_id)
104+
end
105+
end
106+
end
107+
end
108+
109+
describe "in untenanted context" do
110+
test "raises NoTenantError" do
111+
original_user = TenantedApplicationRecord.create_tenant("foo") do
112+
User.create!(email: "user1@example.org")
113+
end
114+
115+
TenantedApplicationRecord.without_tenant do
116+
assert_raises(ActiveRecord::Tenanted::NoTenantError) do
117+
ActiveRecord::Tenanted::GlobalId::Locator.new.locate(original_user.to_global_id)
118+
end
119+
end
120+
end
121+
end
122+
123+
describe "given a model with a default scope" do
124+
test "ignores the default scope" do
125+
TenantedApplicationRecord.create_tenant("foo") do
126+
original_user = User.create!(email: "user1@example.org")
127+
User.class_eval { default_scope { where(email: nil) } }
128+
129+
user = ActiveRecord::Tenanted::GlobalId::Locator.new.locate(original_user.to_global_id)
130+
131+
assert_equal(original_user, user)
132+
end
78133
end
79134
end
80135
end
81136

82-
describe "in wrong tenanted context" do
83-
test "raises WrongTenantError" do
84-
original_user = TenantedApplicationRecord.create_tenant("foo") do
85-
User.create!(email: "user1@example.org")
137+
describe "#locate_many" do
138+
describe "given an untenanted GID" do
139+
test "raises MissingTenantError" do
140+
gid = GlobalID.parse("gid://dummy/User/1")
141+
142+
TenantedApplicationRecord.create_tenant("foo") do
143+
assert_raises(ActiveRecord::Tenanted::MissingTenantError) do
144+
ActiveRecord::Tenanted::GlobalId::Locator.new.locate_many([ gid ])
145+
end
146+
end
147+
end
148+
end
149+
150+
describe "in correct tenanted context" do
151+
test "loads correctly" do
152+
TenantedApplicationRecord.create_tenant("foo") do
153+
original_users = [
154+
User.create!(email: "user1@example.org"),
155+
User.create!(email: "user2@example.org"),
156+
]
157+
users = ActiveRecord::Tenanted::GlobalId::Locator.new.locate_many(original_users.map(&:to_global_id))
158+
159+
assert_equal(original_users, users)
160+
end
161+
end
162+
end
163+
164+
describe "in wrong tenanted context" do
165+
test "raises WrongTenantError" do
166+
original_user = TenantedApplicationRecord.create_tenant("foo") do
167+
User.create!(email: "user1@example.org")
168+
end
169+
170+
TenantedApplicationRecord.create_tenant("bar") do
171+
assert_raises(ActiveRecord::Tenanted::WrongTenantError) do
172+
ActiveRecord::Tenanted::GlobalId::Locator.new.locate_many([ original_user.to_global_id ])
173+
end
174+
end
175+
end
176+
end
177+
178+
describe "in untenanted context" do
179+
test "raises NoTenantError" do
180+
original_user = TenantedApplicationRecord.create_tenant("foo") do
181+
User.create!(email: "user1@example.org")
182+
end
183+
184+
TenantedApplicationRecord.without_tenant do
185+
assert_raises(ActiveRecord::Tenanted::NoTenantError) do
186+
ActiveRecord::Tenanted::GlobalId::Locator.new.locate_many([ original_user.to_global_id ])
187+
end
188+
end
86189
end
190+
end
87191

88-
TenantedApplicationRecord.create_tenant("bar") do
89-
assert_raises(ActiveRecord::Tenanted::WrongTenantError) do
90-
ActiveRecord::Tenanted::GlobalId::Locator.new.locate(original_user.to_global_id)
192+
describe "given a model with a default scope" do
193+
test "ignores the default scope" do
194+
TenantedApplicationRecord.create_tenant("foo") do
195+
original_user = User.create!(email: "user1@example.org")
196+
User.class_eval { default_scope { where(email: nil) } }
197+
198+
users = ActiveRecord::Tenanted::GlobalId::Locator.new.locate_many([ original_user.to_global_id ])
199+
200+
assert_equal([ original_user ], users)
91201
end
92202
end
93203
end
94204
end
205+
end
206+
end
95207

96-
describe "in untenanted context" do
97-
test "raises NoTenantError" do
98-
original_user = TenantedApplicationRecord.create_tenant("foo") do
99-
User.create!(email: "user1@example.org")
208+
if GlobalID::Locator.respond_to?(:fetch)
209+
describe "GlobalID::Locator.fetch" do
210+
# GlobalID::Locator.fetch may re-raise our error wrapped in RecordUnavailable.
211+
def assert_tenant_error(error_class, error)
212+
chain = [ error ]
213+
chain << chain.last.cause while chain.last.cause
214+
215+
assert(chain.any? { |e| e.is_a?(error_class) },
216+
"Expected #{error_class} in #{chain.map(&:class).inspect}")
217+
end
218+
219+
for_each_scenario do
220+
describe "given an untenanted GID" do
221+
test "raises MissingTenantError" do
222+
gid = GlobalID.parse("gid://dummy/User/1")
223+
224+
TenantedApplicationRecord.create_tenant("foo") do
225+
error = assert_raises(StandardError) { GlobalID::Locator.fetch(gid) }
226+
assert_tenant_error(ActiveRecord::Tenanted::MissingTenantError, error)
227+
end
228+
end
229+
end
230+
231+
describe "in correct tenanted context" do
232+
test "loads correctly" do
233+
TenantedApplicationRecord.create_tenant("foo") do
234+
original_user = User.create!(email: "user1@example.org")
235+
236+
assert_equal(original_user, GlobalID::Locator.fetch(original_user.to_global_id))
237+
end
238+
end
239+
240+
test "raises RecordNotFound when the record is gone" do
241+
TenantedApplicationRecord.create_tenant("foo") do
242+
gid = User.create!(email: "user1@example.org").to_global_id
243+
User.delete_all
244+
245+
assert_raises(GlobalID::Locator::RecordNotFound) do
246+
GlobalID::Locator.fetch(gid)
247+
end
248+
end
100249
end
250+
end
251+
252+
describe "in wrong tenanted context" do
253+
test "raises WrongTenantError" do
254+
original_user = TenantedApplicationRecord.create_tenant("foo") do
255+
User.create!(email: "user1@example.org")
256+
end
257+
258+
TenantedApplicationRecord.create_tenant("bar") do
259+
error = assert_raises(StandardError) { GlobalID::Locator.fetch(original_user.to_global_id) }
260+
assert_tenant_error(ActiveRecord::Tenanted::WrongTenantError, error)
261+
end
262+
end
263+
end
264+
265+
describe "in untenanted context" do
266+
test "raises NoTenantError" do
267+
original_user = TenantedApplicationRecord.create_tenant("foo") do
268+
User.create!(email: "user1@example.org")
269+
end
101270

102-
TenantedApplicationRecord.without_tenant do
103-
assert_raises(ActiveRecord::Tenanted::NoTenantError) do
104-
ActiveRecord::Tenanted::GlobalId::Locator.new.locate(original_user.to_global_id)
271+
TenantedApplicationRecord.without_tenant do
272+
error = assert_raises(StandardError) { GlobalID::Locator.fetch(original_user.to_global_id) }
273+
assert_tenant_error(ActiveRecord::Tenanted::NoTenantError, error)
105274
end
106275
end
107276
end

0 commit comments

Comments
 (0)