acts_as_tenant 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +131 -23
- data/lib/acts_as_tenant/active_job_extensions.rb +20 -3
- data/lib/acts_as_tenant/configuration.rb +1 -0
- data/lib/acts_as_tenant/controller_extensions/filter.rb +0 -2
- data/lib/acts_as_tenant/controller_extensions/subdomain.rb +1 -1
- data/lib/acts_as_tenant/controller_extensions/subdomain_or_domain.rb +1 -1
- data/lib/acts_as_tenant/controller_extensions.rb +1 -2
- data/lib/acts_as_tenant/model_extensions.rb +122 -56
- data/lib/acts_as_tenant/sidekiq.rb +4 -12
- data/lib/acts_as_tenant/version.rb +1 -1
- data/lib/acts_as_tenant.rb +25 -37
- metadata +7 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8891ee294c7263049d6bb44c00b66231976574ca467fbf88496510a422b751d5
|
|
4
|
+
data.tar.gz: 580108fc430c92f9f124841243e1ac2d070c7a899e011e153d94543283ff938f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bfef7a385bdbb1d9264ef13a1f521d1538262858480c3c3869c3167a9761bba1c6a5fd02008948d04452c87acf49405eb0aa71630213f2d9a13982c32856605
|
|
7
|
+
data.tar.gz: ee45c099a0e26311071f3de62317bc9425e8c9f7deb3273220d65c446f0dbdb549840551ec903a6202a75777d2f491f3a7f1808bfa08c6432844ef48da0d1da5
|
data/README.md
CHANGED
|
@@ -158,14 +158,14 @@ Scoping your models
|
|
|
158
158
|
-------------------
|
|
159
159
|
|
|
160
160
|
```ruby
|
|
161
|
-
class
|
|
161
|
+
class AddAccountToProjects < ActiveRecord::Migration
|
|
162
162
|
def up
|
|
163
|
-
add_column :
|
|
164
|
-
add_index :
|
|
163
|
+
add_column :projects, :account_id, :integer
|
|
164
|
+
add_index :projects, :account_id
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
class
|
|
168
|
+
class Project < ActiveRecord::Base
|
|
169
169
|
acts_as_tenant(:account)
|
|
170
170
|
end
|
|
171
171
|
```
|
|
@@ -197,6 +197,19 @@ Project.tasks.all # => all tasks with account_id => 3
|
|
|
197
197
|
|
|
198
198
|
Acts_as_tenant uses Rails' `default_scope` method to scope models. Rails 3.1 changed the way `default_scope` works in a good way. A user defined `default_scope` should integrate seamlessly with the one added by `acts_as_tenant`.
|
|
199
199
|
|
|
200
|
+
Because the scoping comes from `default_scope`, queries built from the model are scoped when a tenant is set. This includes `update_all`, `delete_all` and `destroy_all`, and `insert_all` and `upsert_all` set the current tenant. These are **not** scoped or checked:
|
|
201
|
+
|
|
202
|
+
* Any query when no tenant is set, unless `require_tenant` is enabled
|
|
203
|
+
* Queries that remove the default scope, such as `unscoped`
|
|
204
|
+
* Raw SQL, such as `find_by_sql`, `connection.execute` and `exec_query`
|
|
205
|
+
* `update_column` and `update_columns`, which skip the check that prevents changing a record's tenant
|
|
206
|
+
|
|
207
|
+
New records are assigned the current tenant unless they already have one. A record assigned to a different tenant fails validation, so to create records for another tenant, wrap them in `ActsAsTenant.with_tenant(other_tenant) { ... }`.
|
|
208
|
+
|
|
209
|
+
`belongs_to` associations are validated against the current tenant regardless of whether they are declared before or after `acts_as_tenant`.
|
|
210
|
+
|
|
211
|
+
When no tenant is set (for example in an admin panel or inside `without_tenant`), `belongs_to` associations to tenanted models are validated to belong to the same tenant as the record. Associated records without a tenant, such as global records, are allowed.
|
|
212
|
+
|
|
200
213
|
### Validating attribute uniqueness
|
|
201
214
|
|
|
202
215
|
If you need to validate for uniqueness, chances are that you want to scope this validation to a tenant. You can do so by using:
|
|
@@ -259,47 +272,89 @@ ActsAsTenant.configure do |config|
|
|
|
259
272
|
end
|
|
260
273
|
```
|
|
261
274
|
|
|
262
|
-
* `config.require_tenant` when set to true will raise an ActsAsTenant::
|
|
275
|
+
* `config.require_tenant` when set to true will raise an `ActsAsTenant::Errors::NoTenantSet` error whenever a query is made without a tenant set.
|
|
263
276
|
|
|
264
|
-
`config.require_tenant` can also be assigned a lambda that is evaluated at run time. For example
|
|
277
|
+
`config.require_tenant` can also be assigned a lambda that is evaluated at run time. The lambda doesn't have access to the request, so store what it needs in `CurrentAttributes`, which Rails resets after each request. For example, to not require a tenant under `/admin`:
|
|
265
278
|
|
|
266
279
|
```ruby
|
|
280
|
+
# app/models/current.rb
|
|
281
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
282
|
+
attribute :request
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# app/controllers/application_controller.rb
|
|
286
|
+
class ApplicationController < ActionController::Base
|
|
287
|
+
before_action { Current.request = request }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# config/initializers/acts_as_tenant.rb
|
|
267
291
|
ActsAsTenant.configure do |config|
|
|
268
292
|
config.require_tenant = lambda do
|
|
269
|
-
|
|
270
|
-
return false if $request_env["REQUEST_PATH"].start_with?("/admin/")
|
|
271
|
-
end
|
|
293
|
+
!Current.request&.path&.start_with?("/admin/")
|
|
272
294
|
end
|
|
273
295
|
end
|
|
274
296
|
```
|
|
275
297
|
|
|
276
|
-
|
|
298
|
+
Outside of a request, such as in jobs or the console, `Current.request` is `nil` and a tenant is required.
|
|
277
299
|
|
|
278
|
-
|
|
300
|
+
The lambda can also optionally receive the relation being queried as an argument. This is useful for finer control over tenant requirements.
|
|
301
|
+
|
|
302
|
+
For example, if you wanted to require the tenant for every model except `User`, you could do the following:
|
|
279
303
|
|
|
280
304
|
```ruby
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
ActsAsTenant.current_tenant = Account.first
|
|
305
|
+
ActsAsTenant.configure do |config|
|
|
306
|
+
config.require_tenant = lambda do |relation|
|
|
307
|
+
relation.klass.name != "User"
|
|
285
308
|
end
|
|
286
309
|
end
|
|
310
|
+
```
|
|
287
311
|
|
|
312
|
+
`ActsAsTenant.should_require_tenant?` is used to determine if a tenant is required in the current context, either by evaluating the lambda provided, or by returning the boolean value assigned to `config.require_tenant`. It accepts the relation as an optional argument, which is passed to the lambda as `nil` when omitted.
|
|
313
|
+
|
|
314
|
+
With `require_tenant` enabled, ActiveStorage can raise `NoTenantSet` when it generates a preview or variant. After processing, Rails touches the attachment records, which loads their tenant-scoped models, and ActiveStorage requests don't set a tenant. Blobs are looked up by signed IDs, so it's safe to run these requests without a tenant:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
# config/initializers/acts_as_tenant.rb
|
|
318
|
+
Rails.application.config.to_prepare do
|
|
319
|
+
ActiveStorage::Representations::BaseController.prepend_around_action do |_controller, action|
|
|
320
|
+
ActsAsTenant.without_tenant(&action)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
When using `config.require_tenant` alongside the `rails console`, a nice quality of life tweak is to set the tenant in the console session in your initializer script. For example in `config/initializers/acts_as_tenant.rb`:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
288
328
|
Rails.application.configure do
|
|
289
|
-
if Rails.env.development?
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
329
|
+
if Rails.env.development? && defined?(Rails::Console)
|
|
330
|
+
# set the current_tenant during console startup and after calling reload!
|
|
331
|
+
# note: reload! calls the to_prepare callback twice
|
|
332
|
+
ActiveSupport::Reloader.to_prepare do
|
|
333
|
+
puts ">>> Setting ActsAsTenant.current_tenant = Account.first"
|
|
334
|
+
ActsAsTenant.current_tenant = Account.first
|
|
293
335
|
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Tenant change hook
|
|
341
|
+
|
|
342
|
+
`config.tenant_change_hook` is called with the new tenant whenever `current_tenant` is set, and with `nil` when Rails resets it at the end of a request or job. For example, to use Postgres row-level security:
|
|
294
343
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
344
|
+
```ruby
|
|
345
|
+
ActsAsTenant.configure do |config|
|
|
346
|
+
config.tenant_change_hook = lambda do |tenant|
|
|
347
|
+
if tenant
|
|
348
|
+
ActiveRecord::Base.connection.execute(ActiveRecord::Base.sanitize_sql_array(["SET rls.account_id = ?", tenant.id]))
|
|
349
|
+
else
|
|
350
|
+
ActiveRecord::Base.connection.execute("RESET rls.account_id")
|
|
298
351
|
end
|
|
299
352
|
end
|
|
300
353
|
end
|
|
301
354
|
```
|
|
302
355
|
|
|
356
|
+
Always handle `nil`, otherwise the setting stays on the database connection and the next request using it runs as the previous tenant. The hook isn't called for `default_tenant` or `test_tenant`.
|
|
357
|
+
|
|
303
358
|
belongs_to options
|
|
304
359
|
------------------
|
|
305
360
|
|
|
@@ -318,6 +373,32 @@ You can add the following `belongs_to` options to `acts_as_tenant`:
|
|
|
318
373
|
|
|
319
374
|
Example: `acts_as_tenant(:account, counter_cache: true)`
|
|
320
375
|
|
|
376
|
+
ActionCable
|
|
377
|
+
-----------
|
|
378
|
+
|
|
379
|
+
The controller helpers aren't available in ActionCable channels. Instead, find the tenant when the connection is made and set it around each command (subscribe, unsubscribe, and channel actions) with `around_command` (Rails 7.1+):
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
module ApplicationCable
|
|
383
|
+
class Connection < ActionCable::Connection::Base
|
|
384
|
+
identified_by :current_account
|
|
385
|
+
around_command :set_current_tenant
|
|
386
|
+
|
|
387
|
+
def connect
|
|
388
|
+
self.current_account = Account.find_by(subdomain: request.subdomain) || reject_unauthorized_connection
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
private
|
|
392
|
+
|
|
393
|
+
def set_current_tenant(&block)
|
|
394
|
+
ActsAsTenant.with_tenant(current_account, &block)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Setting the tenant in a channel's `before_subscribe` won't work, because Rails resets `current_tenant` after the subscription is created and before each channel action runs. Blocks passed to `stream_from` also run outside of commands, so wrap their contents in `ActsAsTenant.with_tenant(current_account) { ... }` if they query tenant-scoped models.
|
|
401
|
+
|
|
321
402
|
Background Processing libraries
|
|
322
403
|
-------------------------------
|
|
323
404
|
|
|
@@ -332,6 +413,18 @@ Add the following code to `config/initializers/acts_as_tenant.rb`:
|
|
|
332
413
|
require 'acts_as_tenant/sidekiq'
|
|
333
414
|
```
|
|
334
415
|
|
|
416
|
+
The tenant is set while the job runs, but not in `sidekiq_retries_exhausted` or death handlers, because Sidekiq calls them outside the middleware. The tenant is saved in the job hash, so you can set it yourself:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
sidekiq_retries_exhausted do |job, exception|
|
|
420
|
+
tenant = Account.find_by(id: job.dig("acts_as_tenant", "id"))
|
|
421
|
+
|
|
422
|
+
ActsAsTenant.with_tenant(tenant) do
|
|
423
|
+
# ...
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
335
428
|
- DelayedJob - [acts_as_tenant-delayed_job](https://github.com/nunommc/acts_as_tenant-delayed_job)
|
|
336
429
|
|
|
337
430
|
Testing
|
|
@@ -339,7 +432,7 @@ Testing
|
|
|
339
432
|
|
|
340
433
|
If you set the `current_tenant` in your tests, make sure to clean up the tenant after each test by calling `ActsAsTenant.current_tenant = nil`. Integration tests are more difficult: manually setting the `current_tenant` value will not survive across multiple requests, even if they take place within the same test. This can result in undesired boilerplate to set the desired tenant. Moreover, the efficacy of the test can be compromised because the set `current_tenant` value will carry over into the request-response cycle.
|
|
341
434
|
|
|
342
|
-
To address this issue, ActsAsTenant provides for a `test_tenant` value that can be set to allow for setup and post-request expectation testing. It should be used in conjunction with middleware that clears out this value while an integration test is processing. A typical Rails and RSpec setup might look like:
|
|
435
|
+
To address this issue, ActsAsTenant provides for a `test_tenant` value that can be set to allow for setup and post-request expectation testing. It should be used in conjunction with middleware that clears out this value while an integration test is processing. `test_tenant` is only intended for request/integration tests; in model and unit tests, set `current_tenant` or use `with_tenant` instead. A typical Rails and RSpec setup might look like:
|
|
343
436
|
|
|
344
437
|
```ruby
|
|
345
438
|
# test.rb
|
|
@@ -372,6 +465,21 @@ config.after(:each) do |example|
|
|
|
372
465
|
ActsAsTenant.test_tenant = nil
|
|
373
466
|
end
|
|
374
467
|
```
|
|
468
|
+
|
|
469
|
+
### Jobs performed inline
|
|
470
|
+
|
|
471
|
+
Rails resets `CurrentAttributes`, including `current_tenant`, after it performs a job inline ([rails/rails#49227](https://github.com/rails/rails/issues/49227)). The job runs with the tenant it was enqueued with, but the test loses its tenant afterwards when the job is performed inside a `perform_enqueued_jobs` block or with the `:inline` adapter. Call `perform_enqueued_jobs` without a block instead:
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
ActsAsTenant.current_tenant = account
|
|
475
|
+
ProcessOrderJob.perform_later(order)
|
|
476
|
+
perform_enqueued_jobs
|
|
477
|
+
|
|
478
|
+
expect(ActsAsTenant.current_tenant).to eq(account)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
`perform_now` also keeps the tenant.
|
|
482
|
+
|
|
375
483
|
Bug reports & suggested improvements
|
|
376
484
|
------------------------------------
|
|
377
485
|
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
module ActsAsTenant
|
|
2
2
|
module ActiveJobExtensions
|
|
3
|
+
def self.prepended(base)
|
|
4
|
+
# Resolve the tenant inside the perform callbacks so `rescue_from`, `retry_on`
|
|
5
|
+
# and `discard_on` can handle a tenant that no longer exists.
|
|
6
|
+
base.before_perform do
|
|
7
|
+
ActsAsTenant.current_tenant = GlobalID::Locator.locate(@acts_as_tenant_global_id) if @acts_as_tenant_global_id
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
def serialize
|
|
4
|
-
|
|
12
|
+
# A deserialized job keeps the tenant it was enqueued with, e.g. when it is retried.
|
|
13
|
+
tenant_global_id = defined?(@acts_as_tenant_global_id) ? @acts_as_tenant_global_id : ActsAsTenant.current_tenant&.to_global_id&.to_s
|
|
14
|
+
super.merge("current_tenant" => tenant_global_id)
|
|
5
15
|
end
|
|
6
16
|
|
|
7
17
|
def deserialize(job_data)
|
|
8
|
-
|
|
9
|
-
ActsAsTenant.current_tenant = tenant_global_id ? GlobalID::Locator.locate(tenant_global_id) : nil
|
|
18
|
+
@acts_as_tenant_global_id = job_data.delete("current_tenant")
|
|
10
19
|
super
|
|
11
20
|
end
|
|
21
|
+
|
|
22
|
+
def perform_now
|
|
23
|
+
return super unless defined?(@acts_as_tenant_global_id)
|
|
24
|
+
|
|
25
|
+
# Start without a tenant so a job enqueued without one never inherits the thread's,
|
|
26
|
+
# and restore the thread's tenant once the job is done.
|
|
27
|
+
ActsAsTenant.with_tenant(nil) { super }
|
|
28
|
+
end
|
|
12
29
|
end
|
|
13
30
|
end
|
|
@@ -11,7 +11,7 @@ module ActsAsTenant
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def find_tenant_by_subdomain
|
|
14
|
-
if (subdomain = request.subdomains.
|
|
14
|
+
if (subdomain = request.subdomains.public_send(subdomain_lookup))
|
|
15
15
|
ActsAsTenant.current_tenant = tenant_class.where(tenant_column => subdomain.downcase).first
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -11,7 +11,7 @@ module ActsAsTenant
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def find_tenant_by_subdomain_or_domain
|
|
14
|
-
subdomain = request.subdomains.
|
|
14
|
+
subdomain = request.subdomains.public_send(subdomain_lookup)
|
|
15
15
|
query = subdomain.present? ? {tenant_primary_column => subdomain.downcase} : {tenant_second_column => request.domain.downcase}
|
|
16
16
|
ActsAsTenant.current_tenant = tenant_class.where(query).first
|
|
17
17
|
end
|
|
@@ -15,7 +15,6 @@ module ActsAsTenant
|
|
|
15
15
|
self.subdomain_lookup = subdomain_lookup
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# 01/27/2014 Christian Yerena / @preth00nker
|
|
19
18
|
# this method adds the possibility of use the domain as a possible second argument to find
|
|
20
19
|
# the current_tenant.
|
|
21
20
|
def set_current_tenant_by_subdomain_or_domain(tenant = :account, primary_column = :subdomain, second_column = :domain, subdomain_lookup: :last)
|
|
@@ -28,7 +27,7 @@ module ActsAsTenant
|
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
# This method sets up a method that allows manual setting of the current_tenant. This method should
|
|
31
|
-
# be used in a before_action.
|
|
30
|
+
# be used in a before_action.
|
|
32
31
|
def set_current_tenant_through_filter
|
|
33
32
|
include Filter
|
|
34
33
|
end
|
|
@@ -5,35 +5,48 @@ module ActsAsTenant
|
|
|
5
5
|
class_methods do
|
|
6
6
|
def acts_as_tenant(tenant = :account, scope = nil, **options)
|
|
7
7
|
ActsAsTenant.set_tenant_klass(tenant)
|
|
8
|
-
ActsAsTenant.mutable_tenant!(false)
|
|
9
8
|
|
|
10
9
|
ActsAsTenant.add_global_record_model(self) if options[:has_global_records]
|
|
11
10
|
|
|
12
11
|
# Create the association
|
|
13
12
|
valid_options = options.slice(:foreign_key, :class_name, :inverse_of, :optional, :primary_key, :counter_cache, :polymorphic, :touch)
|
|
14
|
-
fkey = valid_options[:foreign_key] || ActsAsTenant.fkey
|
|
15
13
|
pkey = valid_options[:primary_key] || ActsAsTenant.pkey
|
|
16
|
-
polymorphic_type = valid_options[:foreign_type] || ActsAsTenant.polymorphic_type
|
|
17
14
|
belongs_to tenant, scope, **valid_options
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
tenant_reflection = reflect_on_association(tenant)
|
|
17
|
+
fkey = tenant_reflection.foreign_key.to_sym
|
|
18
|
+
polymorphic_type = tenant_reflection.foreign_type&.to_sym
|
|
19
|
+
|
|
20
|
+
# Polymorphic tenants are stored with polymorphic_name, like Rails does. Records saved before
|
|
21
|
+
# that used the class name, which differs for STI tenants, so match both when scoping.
|
|
22
|
+
# An OR is used because Rails 6.0 would assign an IN condition to new records as their type.
|
|
23
|
+
polymorphic_condition = lambda do |current_tenant|
|
|
24
|
+
polymorphic_name = current_tenant.class.polymorphic_name
|
|
25
|
+
class_name = current_tenant.class.name
|
|
26
|
+
|
|
27
|
+
if polymorphic_name == class_name
|
|
28
|
+
{polymorphic_type => polymorphic_name}
|
|
29
|
+
else
|
|
30
|
+
arel_table[polymorphic_type].eq(polymorphic_name).or(arel_table[polymorphic_type].eq(class_name))
|
|
22
31
|
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
default_scope lambda {
|
|
35
|
+
current_tenant = ActsAsTenant.current_tenant
|
|
23
36
|
|
|
24
|
-
if
|
|
25
|
-
keys = [
|
|
37
|
+
if current_tenant
|
|
38
|
+
keys = [current_tenant.public_send(pkey)].compact
|
|
26
39
|
keys.push(nil) if options[:has_global_records]
|
|
27
40
|
|
|
28
|
-
if options[:through]
|
|
29
|
-
|
|
30
|
-
query_criteria[polymorphic_type.to_sym] = ActsAsTenant.current_tenant.class.to_s if options[:polymorphic]
|
|
31
|
-
joins(options[:through]).where(query_criteria)
|
|
41
|
+
relation = if options[:through]
|
|
42
|
+
joins(options[:through]).where(options[:through] => {fkey => keys})
|
|
32
43
|
else
|
|
33
|
-
|
|
34
|
-
query_criteria[polymorphic_type.to_sym] = ActsAsTenant.current_tenant.class.to_s if options[:polymorphic]
|
|
35
|
-
where(query_criteria)
|
|
44
|
+
where(fkey => keys)
|
|
36
45
|
end
|
|
46
|
+
|
|
47
|
+
options[:polymorphic] ? relation.where(polymorphic_condition.call(current_tenant)) : relation
|
|
48
|
+
elsif !ActsAsTenant.unscoped? && ActsAsTenant.should_require_tenant?(self)
|
|
49
|
+
raise ActsAsTenant::Errors::NoTenantSet
|
|
37
50
|
else
|
|
38
51
|
all
|
|
39
52
|
end
|
|
@@ -43,61 +56,112 @@ module ActsAsTenant
|
|
|
43
56
|
# - new instances should have the tenant set
|
|
44
57
|
# - validate that associations belong to the tenant, currently only for belongs_to
|
|
45
58
|
#
|
|
59
|
+
# New records without a tenant get the current tenant. A record assigned to
|
|
60
|
+
# another tenant keeps it and fails the validation below.
|
|
46
61
|
before_validation proc { |m|
|
|
47
|
-
if ActsAsTenant.current_tenant
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
if (current_tenant = ActsAsTenant.current_tenant)
|
|
63
|
+
m.public_send(:"#{fkey}=", current_tenant.public_send(pkey)) if m.public_send(fkey).nil?
|
|
64
|
+
m.public_send(:"#{polymorphic_type}=", current_tenant.class.polymorphic_name) if options[:polymorphic] && m.public_send(polymorphic_type).nil?
|
|
65
|
+
end
|
|
66
|
+
}, on: :create
|
|
67
|
+
|
|
68
|
+
# Returns [tenant id, tenant class] for a record, or nil if it has no tenant.
|
|
69
|
+
# Classes are normalized with polymorphic_name so STI tenants compare equal.
|
|
70
|
+
tenant_identity = lambda do |model|
|
|
71
|
+
reflection = model.class.reflect_on_association(tenant)
|
|
72
|
+
id = model.read_attribute(reflection.foreign_key) if reflection
|
|
73
|
+
|
|
74
|
+
if id
|
|
75
|
+
type = if reflection.polymorphic?
|
|
76
|
+
stored_type = model.read_attribute(reflection.foreign_type)
|
|
77
|
+
stored_type&.safe_constantize&.polymorphic_name || stored_type
|
|
51
78
|
else
|
|
52
|
-
|
|
79
|
+
reflection.klass.polymorphic_name
|
|
53
80
|
end
|
|
81
|
+
[id, type]
|
|
54
82
|
end
|
|
55
|
-
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Without a current tenant the association lookup isn't scoped, so compare the tenants directly
|
|
86
|
+
tenant_mismatch = lambda do |record, associated|
|
|
87
|
+
if ActsAsTenant.current_tenant.nil? && associated.class.respond_to?(:scoped_by_tenant?)
|
|
88
|
+
record_tenant = tenant_identity.call(record)
|
|
89
|
+
associated_tenant = tenant_identity.call(associated)
|
|
56
90
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
record_tenant && associated_tenant && record_tenant != associated_tenant
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Records must belong to the current tenant, matching what the default scope would find
|
|
96
|
+
validate do |record|
|
|
97
|
+
current_tenant = ActsAsTenant.current_tenant
|
|
98
|
+
next unless current_tenant
|
|
99
|
+
|
|
100
|
+
tenant_attributes = [fkey, polymorphic_type].compact
|
|
101
|
+
next unless record.new_record? || tenant_attributes.any? { |attr| record.will_save_change_to_attribute?(attr) }
|
|
102
|
+
|
|
103
|
+
record_tenant = tenant_identity.call(record)
|
|
104
|
+
next if record_tenant.nil?
|
|
105
|
+
|
|
106
|
+
record_id, record_type = record_tenant
|
|
107
|
+
matches = record_id.to_s == current_tenant.public_send(pkey).to_s
|
|
108
|
+
matches &&= record_type == current_tenant.class.polymorphic_name if options[:polymorphic]
|
|
109
|
+
|
|
110
|
+
record.errors.add(fkey, "must be the current tenant [ActsAsTenant]") unless matches
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Associations are looked up at validation time so belongs_to associations
|
|
114
|
+
# declared after acts_as_tenant are validated too
|
|
115
|
+
validate do |record|
|
|
116
|
+
associations = record.class.reflect_on_all_associations(:belongs_to)
|
|
117
|
+
polymorphic_foreign_keys = associations.select(&:polymorphic?).map(&:foreign_key)
|
|
118
|
+
|
|
119
|
+
associations.each do |a|
|
|
120
|
+
attr = a.foreign_key.to_sym
|
|
121
|
+
next unless record.will_save_change_to_attribute?(attr)
|
|
122
|
+
next if a.name == tenant.to_sym || polymorphic_foreign_keys.include?(a.foreign_key)
|
|
123
|
+
|
|
124
|
+
value = record.read_attribute_for_validation(attr)
|
|
125
|
+
next if value.nil?
|
|
126
|
+
|
|
127
|
+
relation = a.scope ? a.klass.class_eval(&a.scope) : a.klass
|
|
128
|
+
associated = relation.find_by(a.association_primary_key => value)
|
|
129
|
+
|
|
130
|
+
if associated.nil? || tenant_mismatch.call(record, associated)
|
|
131
|
+
record.errors.add attr, "association is invalid [ActsAsTenant]"
|
|
74
132
|
end
|
|
75
133
|
end
|
|
76
134
|
end
|
|
77
135
|
|
|
78
|
-
#
|
|
79
|
-
# - Rewrite the accessors to make tenant immutable
|
|
80
|
-
# - Add an override to prevent unnecessary db hits
|
|
81
|
-
# - Add a helper method to verify if a model has been scoped by AaT
|
|
136
|
+
# Tenant writers raise if the tenant changes on a persisted record
|
|
82
137
|
to_include = Module.new {
|
|
83
|
-
define_method "#{fkey}=" do |integer|
|
|
84
|
-
write_attribute(fkey
|
|
85
|
-
|
|
138
|
+
define_method :"#{fkey}=" do |integer|
|
|
139
|
+
write_attribute(fkey, integer)
|
|
140
|
+
raise_if_tenant_changed
|
|
86
141
|
integer
|
|
87
142
|
end
|
|
88
143
|
|
|
89
|
-
define_method "#{
|
|
144
|
+
define_method :"#{tenant}=" do |model|
|
|
90
145
|
super(model)
|
|
91
|
-
|
|
146
|
+
raise_if_tenant_changed
|
|
92
147
|
model
|
|
93
148
|
end
|
|
94
149
|
|
|
150
|
+
define_method :raise_if_tenant_changed do
|
|
151
|
+
raise ActsAsTenant::Errors::TenantIsImmutable if !ActsAsTenant.mutable_tenant? && tenant_modified?
|
|
152
|
+
end
|
|
153
|
+
private :raise_if_tenant_changed
|
|
154
|
+
|
|
95
155
|
define_method :tenant_modified? do
|
|
96
156
|
will_save_change_to_attribute?(fkey) && persisted? && attribute_in_database(fkey).present?
|
|
97
157
|
end
|
|
98
158
|
}
|
|
99
159
|
include to_include
|
|
100
160
|
|
|
161
|
+
# Stored per model since ActsAsTenant.tenant_klass is whichever model called acts_as_tenant last
|
|
162
|
+
class_attribute :acts_as_tenant_foreign_key, instance_accessor: false
|
|
163
|
+
self.acts_as_tenant_foreign_key = fkey
|
|
164
|
+
|
|
101
165
|
class << self
|
|
102
166
|
def scoped_by_tenant?
|
|
103
167
|
true
|
|
@@ -105,10 +169,11 @@ module ActsAsTenant
|
|
|
105
169
|
end
|
|
106
170
|
end
|
|
107
171
|
|
|
108
|
-
def validates_uniqueness_to_tenant(fields
|
|
172
|
+
def validates_uniqueness_to_tenant(*fields)
|
|
173
|
+
args = fields.extract_options!
|
|
109
174
|
raise ActsAsTenant::Errors::ModelNotScopedByTenant unless respond_to?(:scoped_by_tenant?)
|
|
110
175
|
|
|
111
|
-
fkey =
|
|
176
|
+
fkey = acts_as_tenant_foreign_key
|
|
112
177
|
|
|
113
178
|
validation_args = args.deep_dup
|
|
114
179
|
validation_args[:scope] = if args[:scope]
|
|
@@ -118,25 +183,26 @@ module ActsAsTenant
|
|
|
118
183
|
end
|
|
119
184
|
|
|
120
185
|
# validating within tenant scope
|
|
121
|
-
validates_uniqueness_of(fields, validation_args)
|
|
186
|
+
validates_uniqueness_of(*fields, validation_args)
|
|
122
187
|
|
|
123
188
|
if ActsAsTenant.models_with_global_records.include?(self)
|
|
124
189
|
arg_if = args.delete(:if)
|
|
125
190
|
arg_condition = args.delete(:conditions)
|
|
191
|
+
arg_if_passes = ->(instance) { arg_if.blank? || arg_if.call(instance) }
|
|
126
192
|
|
|
127
193
|
# if tenant is not set (instance is global) - validating globally
|
|
128
194
|
global_validation_args = args.merge(
|
|
129
|
-
if: ->(instance) { instance[fkey].blank? &&
|
|
195
|
+
if: ->(instance) { instance[fkey].blank? && arg_if_passes.call(instance) }
|
|
130
196
|
)
|
|
131
|
-
validates_uniqueness_of(fields, global_validation_args)
|
|
197
|
+
validates_uniqueness_of(*fields, global_validation_args)
|
|
132
198
|
|
|
133
199
|
# if tenant is set (instance is not global) and records can be global - validating within records with blank tenant
|
|
134
|
-
blank_tenant_validation_args = args.merge(
|
|
200
|
+
blank_tenant_validation_args = args.merge(
|
|
135
201
|
conditions: -> { arg_condition.blank? ? where(fkey => nil) : arg_condition.call.where(fkey => nil) },
|
|
136
|
-
if: ->(instance) { instance[fkey].present? &&
|
|
137
|
-
|
|
202
|
+
if: ->(instance) { instance[fkey].present? && arg_if_passes.call(instance) }
|
|
203
|
+
)
|
|
138
204
|
|
|
139
|
-
validates_uniqueness_of(fields, blank_tenant_validation_args)
|
|
205
|
+
validates_uniqueness_of(*fields, blank_tenant_validation_args)
|
|
140
206
|
end
|
|
141
207
|
end
|
|
142
208
|
end
|
|
@@ -10,12 +10,8 @@ module ActsAsTenant::Sidekiq
|
|
|
10
10
|
include Sidekiq::ClientMiddleware if sidekiq_7_and_up?
|
|
11
11
|
|
|
12
12
|
def call(worker_class, msg, queue, redis_pool)
|
|
13
|
-
if ActsAsTenant.current_tenant
|
|
14
|
-
msg["acts_as_tenant"] ||=
|
|
15
|
-
{
|
|
16
|
-
"class" => ActsAsTenant.current_tenant.class.name,
|
|
17
|
-
"id" => ActsAsTenant.current_tenant.id
|
|
18
|
-
}
|
|
13
|
+
if (tenant = ActsAsTenant.current_tenant)
|
|
14
|
+
msg["acts_as_tenant"] ||= {"class" => tenant.class.name, "id" => tenant.id}
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
yield
|
|
@@ -31,9 +27,7 @@ module ActsAsTenant::Sidekiq
|
|
|
31
27
|
klass = msg["acts_as_tenant"]["class"].constantize
|
|
32
28
|
id = msg["acts_as_tenant"]["id"]
|
|
33
29
|
account = klass.class_eval(&ActsAsTenant.configuration.job_scope).find(id)
|
|
34
|
-
ActsAsTenant.with_tenant
|
|
35
|
-
yield
|
|
36
|
-
end
|
|
30
|
+
ActsAsTenant.with_tenant(account) { yield }
|
|
37
31
|
else
|
|
38
32
|
yield
|
|
39
33
|
end
|
|
@@ -52,9 +46,7 @@ Sidekiq.configure_server do |config|
|
|
|
52
46
|
chain.add ActsAsTenant::Sidekiq::Client
|
|
53
47
|
end
|
|
54
48
|
config.server_middleware do |chain|
|
|
55
|
-
if defined?(Sidekiq::
|
|
56
|
-
chain.insert_before Sidekiq::Middleware::Server::RetryJobs, ActsAsTenant::Sidekiq::Server
|
|
57
|
-
elsif defined?(Sidekiq::Batch::Server)
|
|
49
|
+
if defined?(Sidekiq::Batch::Server)
|
|
58
50
|
chain.insert_before Sidekiq::Batch::Server, ActsAsTenant::Sidekiq::Server
|
|
59
51
|
else
|
|
60
52
|
chain.add ActsAsTenant::Sidekiq::Server
|
data/lib/acts_as_tenant.rb
CHANGED
|
@@ -12,10 +12,16 @@ module ActsAsTenant
|
|
|
12
12
|
@@configuration = nil
|
|
13
13
|
@@tenant_klass = nil
|
|
14
14
|
@@models_with_global_records = []
|
|
15
|
-
@@mutable_tenant = false
|
|
16
15
|
|
|
17
16
|
class Current < ActiveSupport::CurrentAttributes
|
|
18
|
-
attribute :current_tenant, :acts_as_tenant_unscoped
|
|
17
|
+
attribute :current_tenant, :acts_as_tenant_unscoped, :acts_as_tenant_mutable
|
|
18
|
+
|
|
19
|
+
# Rails resets attributes directly at the end of a request or job, bypassing the writer below
|
|
20
|
+
resets { ActsAsTenant.configuration.tenant_change_hook&.call(nil) }
|
|
21
|
+
|
|
22
|
+
def current_tenant=(tenant)
|
|
23
|
+
super.tap { ActsAsTenant.configuration.tenant_change_hook&.call(tenant) }
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
class << self
|
|
@@ -93,59 +99,41 @@ module ActsAsTenant
|
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
def self.mutable_tenant!(toggle)
|
|
96
|
-
|
|
102
|
+
Current.acts_as_tenant_mutable = toggle
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
def self.mutable_tenant?
|
|
100
|
-
|
|
106
|
+
!!Current.acts_as_tenant_mutable
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
def self.with_tenant(tenant, &block)
|
|
104
|
-
if block.nil?
|
|
105
|
-
raise ArgumentError, "block required"
|
|
106
|
-
end
|
|
110
|
+
raise ArgumentError, "block required" if block.nil?
|
|
107
111
|
|
|
108
|
-
|
|
109
|
-
self.current_tenant = tenant
|
|
110
|
-
value = block.call
|
|
111
|
-
value
|
|
112
|
-
ensure
|
|
113
|
-
self.current_tenant = old_tenant
|
|
112
|
+
Current.set(current_tenant: tenant, &block)
|
|
114
113
|
end
|
|
115
114
|
|
|
116
115
|
def self.without_tenant(&block)
|
|
117
|
-
if block.nil?
|
|
118
|
-
raise ArgumentError, "block required"
|
|
119
|
-
end
|
|
116
|
+
raise ArgumentError, "block required" if block.nil?
|
|
120
117
|
|
|
121
|
-
old_tenant = current_tenant
|
|
122
118
|
old_test_tenant = test_tenant
|
|
123
|
-
old_unscoped = unscoped
|
|
124
|
-
|
|
125
|
-
self.current_tenant = nil
|
|
126
119
|
self.test_tenant = nil
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
self.test_tenant = old_test_tenant
|
|
133
|
-
self.unscoped = old_unscoped
|
|
120
|
+
begin
|
|
121
|
+
Current.set(current_tenant: nil, acts_as_tenant_unscoped: true, &block)
|
|
122
|
+
ensure
|
|
123
|
+
self.test_tenant = old_test_tenant
|
|
124
|
+
end
|
|
134
125
|
end
|
|
135
126
|
|
|
136
127
|
def self.with_mutable_tenant(&block)
|
|
137
|
-
|
|
138
|
-
without_tenant(&block)
|
|
139
|
-
ensure
|
|
140
|
-
ActsAsTenant.mutable_tenant!(false)
|
|
128
|
+
Current.set(acts_as_tenant_mutable: true) { without_tenant(&block) }
|
|
141
129
|
end
|
|
142
130
|
|
|
143
|
-
def self.should_require_tenant?
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
131
|
+
def self.should_require_tenant?(relation = nil)
|
|
132
|
+
config = configuration.require_tenant
|
|
133
|
+
return !!config unless config.respond_to?(:call)
|
|
134
|
+
|
|
135
|
+
arity = config.respond_to?(:arity) ? config.arity : config.method(:call).arity
|
|
136
|
+
arity.zero? ? !!config.call : !!config.call(relation)
|
|
149
137
|
end
|
|
150
138
|
end
|
|
151
139
|
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acts_as_tenant
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Erwin Matthijssen
|
|
8
8
|
- Chris Oliver
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: rails
|
|
@@ -51,9 +50,10 @@ files:
|
|
|
51
50
|
- lib/acts_as_tenant/test_tenant_middleware.rb
|
|
52
51
|
- lib/acts_as_tenant/version.rb
|
|
53
52
|
homepage: https://github.com/ErwinM/acts_as_tenant
|
|
54
|
-
licenses:
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
57
|
rdoc_options: []
|
|
58
58
|
require_paths:
|
|
59
59
|
- lib
|
|
@@ -68,8 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
69
|
version: '0'
|
|
70
70
|
requirements: []
|
|
71
|
-
rubygems_version:
|
|
72
|
-
signing_key:
|
|
71
|
+
rubygems_version: 4.0.21
|
|
73
72
|
specification_version: 4
|
|
74
73
|
summary: Add multi-tenancy to Rails applications using a shared db strategy
|
|
75
74
|
test_files: []
|