multi-tenant-support 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +325 -0
- data/Rakefile +13 -0
- data/lib/generators/multi_tenant_support/initializer_generator.rb +15 -0
- data/lib/generators/multi_tenant_support/migration_generator.rb +21 -0
- data/lib/generators/multi_tenant_support/templates/initializer.rb.tt +21 -0
- data/lib/generators/multi_tenant_support/templates/migration.rb.tt +6 -0
- data/lib/generators/override/active_record/migration/templates/create_table_migration.rb.tt +30 -0
- data/lib/generators/override/active_record/model/model.rb.tt +26 -0
- data/lib/multi_tenant_support/active_job.rb +48 -0
- data/lib/multi_tenant_support/concern/controller_concern.rb +30 -0
- data/lib/multi_tenant_support/concern/model_concern.rb +192 -0
- data/lib/multi_tenant_support/config/app.rb +26 -0
- data/lib/multi_tenant_support/config/controller.rb +26 -0
- data/lib/multi_tenant_support/config/model.rb +36 -0
- data/lib/multi_tenant_support/current.rb +8 -0
- data/lib/multi_tenant_support/errors.rb +16 -0
- data/lib/multi_tenant_support/find_tenant_account.rb +31 -0
- data/lib/multi_tenant_support/railtie.rb +13 -0
- data/lib/multi_tenant_support/sidekiq.rb +60 -0
- data/lib/multi_tenant_support/version.rb +3 -0
- data/lib/multi_tenant_support.rb +60 -0
- data/lib/tasks/multi_tenant_support_tasks.rake +4 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 655b0d26a6134f7799d3b6ac10502394769daba381e07f3908abac34550e8ca3
|
4
|
+
data.tar.gz: b5f4e15877e645c88292cb3f60d36b034e22e1954fa21a0e034376459e87ca32
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2242678363445f00ee88f4b84391dfce78dcf1c90110c420accbf2c4d2040de63c013975cba676dd6d831b6739f6d56545881bc2de1852254dfdd62533db8863
|
7
|
+
data.tar.gz: 14909240808be01f96b59c936b98b32dcfb78c1a333164995d0bd1bed1464bde92bc6f34eb4e1ff10ac8be8a58cf5cc76b804a5a48a0655a7f59df7bdcd4322f
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Hopper Gee
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
# MultiTenantSupport
|
2
|
+
|
3
|
+
[![Test](https://github.com/hoppergee/multi-tenant-support/actions/workflows/main.yaml/badge.svg?branch=main)](https://github.com/hoppergee/multi-tenant-support/actions/workflows/main.yaml)
|
4
|
+
|
5
|
+
![](https://raw.githubusercontent.com/hoppergee/multi-tenant-support/main/hero.png)
|
6
|
+
|
7
|
+
Build a highly secure, multi-tenant rails app without data leak.
|
8
|
+
|
9
|
+
Keep your data secure with multi-tenant-support. Prevent most ActiveRecord CRUD methods to action across tenant, ensuring no one can accidentally or intentionally access other tenants' data. This can be crucial for applications handling sensitive information like financial information, intellectual property, and so forth.
|
10
|
+
|
11
|
+
- Prevent most ActiveRecord CRUD methods from acting across tenants.
|
12
|
+
- Support Row-level Multitenancy
|
13
|
+
- Build on ActiveSupport::CurrentAttributes offered by rails
|
14
|
+
- Auto set current tenant through subdomain and domain in controller
|
15
|
+
- Support ActiveJob and Sidekiq
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
1. Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'multi-tenant-support'
|
23
|
+
```
|
24
|
+
|
25
|
+
2. And then execute:
|
26
|
+
|
27
|
+
```
|
28
|
+
bundle install
|
29
|
+
```
|
30
|
+
|
31
|
+
3. Add domain and subdomain to your tenant account table (Skip if your rails app already did this)
|
32
|
+
|
33
|
+
```
|
34
|
+
rails generate multi_tenant_support:migration YOUR_TENANT_ACCOUNT_TABLE_OR_MODEL_NAME
|
35
|
+
|
36
|
+
# Say your tenant account table is "accounts"
|
37
|
+
rails generate multi_tenant_support:migration accounts
|
38
|
+
|
39
|
+
# You can also run it with the tenant account model name
|
40
|
+
# rails generate multi_tenant_support:migration Account
|
41
|
+
|
42
|
+
rails db:migrate
|
43
|
+
```
|
44
|
+
|
45
|
+
4. Create an initializer
|
46
|
+
|
47
|
+
```
|
48
|
+
rails generate multi_tenant_support:initializer
|
49
|
+
```
|
50
|
+
|
51
|
+
5. Set `tenant_account_class_name` to your tenant account model name in `multi_tenant_support.rb`
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
- config.tenant_account_class_name = 'REPLACE_ME'
|
55
|
+
+ config.tenant_account_class_name = 'Account'
|
56
|
+
```
|
57
|
+
|
58
|
+
6. Set `host` to your app's domain in `multi_tenant_support.rb`
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
- config.host = 'REPLACE.ME'
|
62
|
+
+ config.host = 'your-app-domain.com'
|
63
|
+
```
|
64
|
+
|
65
|
+
7. Setup for ActiveJob or Sidekiq
|
66
|
+
|
67
|
+
If you are using ActiveJob
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
- # require 'multi_tenant_support/active_job'
|
71
|
+
+ require 'multi_tenant_support/active_job'
|
72
|
+
```
|
73
|
+
|
74
|
+
If you are using sidekiq without ActiveJob
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
- # require 'multi_tenant_support/sidekiq'
|
78
|
+
+ require 'multi_tenant_support/sidekiq'
|
79
|
+
```
|
80
|
+
|
81
|
+
8. Add `belongs_to_tenant` to all models which you want to scope under tenant
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class User < ApplicationRecord
|
85
|
+
belongs_to_tenant :account
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
## Usage
|
90
|
+
|
91
|
+
#### Get current
|
92
|
+
|
93
|
+
Get current tenant through:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
MultiTenantSupport.current_tenant
|
97
|
+
```
|
98
|
+
|
99
|
+
#### Switch tenant
|
100
|
+
|
101
|
+
You can switch to another tenant temporary through:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
MultiTenantSupport.under_tenant amazon do
|
105
|
+
# Do things under amazon account
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
#### Disallow read across tenant by default
|
110
|
+
|
111
|
+
This gem disallow read across tenant by default. You can check current state through:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
MultiTenantSupport.disallow_read_across_tenant?
|
115
|
+
```
|
116
|
+
|
117
|
+
#### Allow read across tenant for super admin
|
118
|
+
|
119
|
+
You can turn on the permission to read records across tenant through:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
MultiTenantSupport.allow_read_across_tenant
|
123
|
+
|
124
|
+
# Or
|
125
|
+
MultiTenantSupport.allow_read_across_tenant do
|
126
|
+
# ...
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
You can put it in a before action in SuperAdmin's controllers
|
131
|
+
|
132
|
+
#### Disallow modify records tenant
|
133
|
+
|
134
|
+
This gem disallow modify record across tenant no matter you are super admin or not.
|
135
|
+
|
136
|
+
If `MultiTenantSupport.current_tenant` exist, you can only modify those records under this tenant, otherwise, you will get some errors like:
|
137
|
+
|
138
|
+
- `MultiTenantSupport::MissingTenantError`
|
139
|
+
- `MultiTenantSupport::ImmutableTenantError`
|
140
|
+
- `MultiTenantSupport::NilTenantError`
|
141
|
+
- `MultiTenantSupport::InvalidTenantAccess`
|
142
|
+
- `ActiveRecord::RecordNotFound`
|
143
|
+
|
144
|
+
If `MultiTenantSupport.current_tenant` is missing, you cannot modify or create any tenanted records.
|
145
|
+
|
146
|
+
#### Set current tenant acccount in controller by default
|
147
|
+
|
148
|
+
This gem has set a before action `set_current_tenant_account` on ActionController. It search tenant by subdomain or domain. Do remember to `skip_before_action :set_current_tenant_account` in super admin controllers.
|
149
|
+
|
150
|
+
Feel free to override it, if the finder behaviour is not what you want.
|
151
|
+
|
152
|
+
#### upsert_all
|
153
|
+
|
154
|
+
Currently, we don't have a good way to protect this method. So please use `upser_all` carefully.
|
155
|
+
|
156
|
+
#### Unscoped
|
157
|
+
|
158
|
+
This gem has override `unscoped` to prevent the default tenant scope be scoped out. But if you really want to scope out the default tenant scope, you can use `unscope_tenant`.
|
159
|
+
|
160
|
+
## Code Example
|
161
|
+
|
162
|
+
#### Database Schema
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
create_table "accounts", force: :cascade do |t|
|
166
|
+
t.bigint "domain"
|
167
|
+
t.bigint "subdomain"
|
168
|
+
end
|
169
|
+
|
170
|
+
create_table "users", force: :cascade do |t|
|
171
|
+
t.bigint "account_id"
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
#### Initializer
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# config/initializers/multi_tenant_support.rb
|
179
|
+
|
180
|
+
MultiTenantSupport.configure do
|
181
|
+
model do |config|
|
182
|
+
config.tenant_account_class_name = 'Account'
|
183
|
+
config.tenant_account_primary_key = :id
|
184
|
+
end
|
185
|
+
|
186
|
+
controller do |config|
|
187
|
+
config.current_tenant_method_name = :current_tenant_account
|
188
|
+
end
|
189
|
+
|
190
|
+
app do |config|
|
191
|
+
config.excluded_subdomains = ['www']
|
192
|
+
config.host = 'example.com'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
#### Model
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
class Account < AppplicationRecord
|
201
|
+
has_many :users
|
202
|
+
end
|
203
|
+
|
204
|
+
class User < ApplicationRecord
|
205
|
+
belongs_to_tenant :account
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
#### Controler
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
class UsersController < ApplicationController
|
213
|
+
def show
|
214
|
+
@user = User.find(params[:id]) # This result is already scope under current_tenant_account
|
215
|
+
@you_can_get_account = current_tenant_account
|
216
|
+
end
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
## ActiveRecord proteced methods
|
221
|
+
|
222
|
+
<table>
|
223
|
+
<thead>
|
224
|
+
<tr>
|
225
|
+
<th colspan="8">ActiveRecord proteced methods</th>
|
226
|
+
</tr>
|
227
|
+
</thead>
|
228
|
+
<tbody>
|
229
|
+
<tr>
|
230
|
+
<td>count</td>
|
231
|
+
<td>🔒</td>
|
232
|
+
<td>save</td>
|
233
|
+
<td>🔒</td>
|
234
|
+
<td>account=</td>
|
235
|
+
<td>🔒</td>
|
236
|
+
<td>upsert</td>
|
237
|
+
<td>🔒</td>
|
238
|
+
</tr>
|
239
|
+
<tr>
|
240
|
+
<td>first</td>
|
241
|
+
<td>🔒</td>
|
242
|
+
<td>save!</td>
|
243
|
+
<td>🔒</td>
|
244
|
+
<td>account_id=</td>
|
245
|
+
<td>🔒</td>
|
246
|
+
<td>destroy</td>
|
247
|
+
<td>🔒</td>
|
248
|
+
</tr>
|
249
|
+
<tr>
|
250
|
+
<td>last</td>
|
251
|
+
<td>🔒</td>
|
252
|
+
<td>create</td>
|
253
|
+
<td>🔒</td>
|
254
|
+
<td>update</td>
|
255
|
+
<td>🔒</td>
|
256
|
+
<td>destroy!</td>
|
257
|
+
<td>🔒</td>
|
258
|
+
</tr>
|
259
|
+
<tr>
|
260
|
+
<td>where</td>
|
261
|
+
<td>🔒</td>
|
262
|
+
<td>create!</td>
|
263
|
+
<td>🔒</td>
|
264
|
+
<td>update_all</td>
|
265
|
+
<td>🔒</td>
|
266
|
+
<td>destroy_all</td>
|
267
|
+
<td>🔒</td>
|
268
|
+
</tr>
|
269
|
+
<tr>
|
270
|
+
<td>find_by</td>
|
271
|
+
<td>🔒</td>
|
272
|
+
<td>insert</td>
|
273
|
+
<td>🔒</td>
|
274
|
+
<td>update_attribute</td>
|
275
|
+
<td>🔒</td>
|
276
|
+
<td>destroy_by</td>
|
277
|
+
<td>🔒</td>
|
278
|
+
</tr>
|
279
|
+
<tr>
|
280
|
+
<td>reload</td>
|
281
|
+
<td>🔒</td>
|
282
|
+
<td>insert!</td>
|
283
|
+
<td>🔒</td>
|
284
|
+
<td>update_columns</td>
|
285
|
+
<td>🔒</td>
|
286
|
+
<td>delete_all</td>
|
287
|
+
<td>🔒</td>
|
288
|
+
</tr>
|
289
|
+
<tr>
|
290
|
+
<td>new</td>
|
291
|
+
<td>🔒</td>
|
292
|
+
<td>insert_all</td>
|
293
|
+
<td>🔒</td>
|
294
|
+
<td>update_column</td>
|
295
|
+
<td>🔒</td>
|
296
|
+
<td>delete_by</td>
|
297
|
+
<td>🔒</td>
|
298
|
+
</tr>
|
299
|
+
<tr>
|
300
|
+
<td>build</td>
|
301
|
+
<td>🔒</td>
|
302
|
+
<td>insert_all!</td>
|
303
|
+
<td>🔒</td>
|
304
|
+
<td>upsert_all</td>
|
305
|
+
<td>⚠️ (Partial)</td>
|
306
|
+
<td>unscoped</td>
|
307
|
+
<td>🔒</td>
|
308
|
+
</tr>
|
309
|
+
</tbody>
|
310
|
+
</table>
|
311
|
+
|
312
|
+
|
313
|
+
## Development
|
314
|
+
|
315
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
316
|
+
|
317
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
318
|
+
|
319
|
+
## Contributing
|
320
|
+
|
321
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hoppergee/multi_tenant_support.
|
322
|
+
|
323
|
+
## License
|
324
|
+
|
325
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module MultiTenantSupport
|
4
|
+
module Generators
|
5
|
+
class InitializerGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
7
|
+
|
8
|
+
desc "Create an initializer for multi-tenant-support"
|
9
|
+
|
10
|
+
def copy_initializer_file
|
11
|
+
copy_file "initializer.rb.tt", "config/initializers/multi_tenant_support.rb"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "rails/generators/active_record"
|
2
|
+
|
3
|
+
module MultiTenantSupport
|
4
|
+
module Generators
|
5
|
+
class MigrationGenerator < Rails::Generators::NamedBase
|
6
|
+
include ActiveRecord::Generators::Migration
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
8
|
+
|
9
|
+
desc "Create a migration for multi-tenant-support"
|
10
|
+
|
11
|
+
def copy_migration_file
|
12
|
+
migration_template "migration.rb.tt", "db/migrate/add_domain_and_subdomain_to_#{table_name}.rb"
|
13
|
+
puts "\nPlease run this migration:\n\n rails db:migrate"
|
14
|
+
end
|
15
|
+
|
16
|
+
def migration_version
|
17
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MultiTenantSupport.configure do
|
2
|
+
model do |config|
|
3
|
+
config.tenant_account_class_name = 'REPLACE_ME'
|
4
|
+
config.tenant_account_primary_key = :id
|
5
|
+
end
|
6
|
+
|
7
|
+
controller do |config|
|
8
|
+
config.current_tenant_method_name = :current_tenant_account
|
9
|
+
end
|
10
|
+
|
11
|
+
app do |config|
|
12
|
+
config.excluded_subdomains = ['www']
|
13
|
+
config.host = 'REPLACE.ME'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Uncomment if you are using sidekiq without ActiveJob
|
18
|
+
# require 'multi_tenant_support/sidekiq'
|
19
|
+
|
20
|
+
# Uncomment if you are using ActiveJob
|
21
|
+
# require 'multi_tenant_support/active_job'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
create_table :<%= table_name %><%= primary_key_type %> do |t|
|
4
|
+
<% tenant_account_class_name = MultiTenantSupport.model.tenant_account_class_name -%>
|
5
|
+
<% if tenant_account_class_name -%>
|
6
|
+
t.belongs_to :<%= tenant_account_class_name.underscore %>, null: false
|
7
|
+
<% end -%>
|
8
|
+
<% attributes.each do |attribute| -%>
|
9
|
+
<% if attribute.password_digest? -%>
|
10
|
+
t.string :password_digest<%= attribute.inject_options %>
|
11
|
+
<% elsif attribute.token? -%>
|
12
|
+
t.string :<%= attribute.name %><%= attribute.inject_options %>
|
13
|
+
<% elsif attribute.reference? -%>
|
14
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %><%= foreign_key_type %>
|
15
|
+
<% elsif !attribute.virtual? -%>
|
16
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
|
17
|
+
<% end -%>
|
18
|
+
<% end -%>
|
19
|
+
<% if options[:timestamps] %>
|
20
|
+
t.timestamps
|
21
|
+
<% end -%>
|
22
|
+
end
|
23
|
+
<% attributes.select(&:token?).each do |attribute| -%>
|
24
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
|
25
|
+
<% end -%>
|
26
|
+
<% attributes_with_index.each do |attribute| -%>
|
27
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
28
|
+
<% end -%>
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
class <%= class_name %> < <%= parent_class_name.classify %>
|
3
|
+
<% tenant_account = MultiTenantSupport.model.tenant_account_class_name&.underscore -%>
|
4
|
+
belongs_to_tenant :<%= tenant_account %>
|
5
|
+
<% attributes.select(&:reference?).each do |attribute| -%>
|
6
|
+
<% if attribute.name != tenant_account -%>
|
7
|
+
belongs_to :<%= attribute.name %><%= ", polymorphic: true" if attribute.polymorphic? %>
|
8
|
+
<% end -%>
|
9
|
+
<% end -%>
|
10
|
+
<% attributes.select(&:rich_text?).each do |attribute| -%>
|
11
|
+
has_rich_text :<%= attribute.name %>
|
12
|
+
<% end -%>
|
13
|
+
<% attributes.select(&:attachment?).each do |attribute| -%>
|
14
|
+
has_one_attached :<%= attribute.name %>
|
15
|
+
<% end -%>
|
16
|
+
<% attributes.select(&:attachments?).each do |attribute| -%>
|
17
|
+
has_many_attached :<%= attribute.name %>
|
18
|
+
<% end -%>
|
19
|
+
<% attributes.select(&:token?).each do |attribute| -%>
|
20
|
+
has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
|
21
|
+
<% end -%>
|
22
|
+
<% if attributes.any?(&:password_digest?) -%>
|
23
|
+
has_secure_password
|
24
|
+
<% end -%>
|
25
|
+
end
|
26
|
+
<% end -%>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
module ActiveJob
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
attr_accessor :current_tenant
|
7
|
+
end
|
8
|
+
|
9
|
+
def perform_now
|
10
|
+
MultiTenantSupport.under_tenant(current_tenant) do
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def serialize
|
16
|
+
if MultiTenantSupport.current_tenant
|
17
|
+
super.merge({
|
18
|
+
"multi_tenant_support" => {
|
19
|
+
"id" => MultiTenantSupport.current_tenant.id,
|
20
|
+
"class" => MultiTenantSupport.current_tenant.class.name
|
21
|
+
}
|
22
|
+
})
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def deserialize(job_data)
|
29
|
+
self.current_tenant = find_current_tenant(job_data)
|
30
|
+
MultiTenantSupport.under_tenant(current_tenant) do
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def find_current_tenant(data)
|
38
|
+
return unless data.has_key?("multi_tenant_support")
|
39
|
+
|
40
|
+
tenant_klass = data["multi_tenant_support"]["class"].constantize
|
41
|
+
tenant_id = data["multi_tenant_support"]["id"]
|
42
|
+
|
43
|
+
tenant_klass.find tenant_id
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
ActiveJob::Base.include(MultiTenantSupport::ActiveJob)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
module ControllerConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
helper_method MultiTenantSupport.current_tenant_account_method
|
7
|
+
|
8
|
+
before_action :set_current_tenant_account
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
define_method(MultiTenantSupport.current_tenant_account_method) do
|
13
|
+
instance_variable_get("@#{MultiTenantSupport.current_tenant_account_method}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_current_tenant_account
|
17
|
+
tenant_account = MultiTenantSupport::FindTenantAccount.call(
|
18
|
+
subdomains: request.subdomains,
|
19
|
+
domain: request.domain
|
20
|
+
)
|
21
|
+
MultiTenantSupport::Current.tenant_account = tenant_account
|
22
|
+
instance_variable_set("@#{MultiTenantSupport.current_tenant_account_method}", tenant_account)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveSupport.on_load(:action_controller) do |base|
|
29
|
+
base.include MultiTenantSupport::ControllerConcern
|
30
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
module ModelConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
|
7
|
+
def belongs_to_tenant(name, **options)
|
8
|
+
options[:foreign_key] ||= MultiTenantSupport.model.default_foreign_key
|
9
|
+
belongs_to name.to_sym, **options
|
10
|
+
|
11
|
+
set_default_scope_under_current_tenant(options[:foreign_key])
|
12
|
+
set_tenant_account_readonly(name, options[:foreign_key])
|
13
|
+
|
14
|
+
MultiTenantSupport.model.tenanted_models << self.name
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def set_default_scope_under_current_tenant(foreign_key)
|
20
|
+
default_scope lambda {
|
21
|
+
if MultiTenantSupport.disallow_read_across_tenant? || MultiTenantSupport.current_tenant
|
22
|
+
scope_under_current_tenant
|
23
|
+
else
|
24
|
+
where(nil)
|
25
|
+
end
|
26
|
+
}
|
27
|
+
|
28
|
+
scope :scope_under_current_tenant, lambda {
|
29
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
30
|
+
|
31
|
+
tenant_account_primary_key = MultiTenantSupport.model.tenant_account_primary_key
|
32
|
+
tenant_account_id = MultiTenantSupport.current_tenant.send(tenant_account_primary_key)
|
33
|
+
where(foreign_key => tenant_account_id)
|
34
|
+
}
|
35
|
+
|
36
|
+
scope :unscope_tenant, -> { unscope(where: foreign_key) }
|
37
|
+
|
38
|
+
override_unscoped = Module.new {
|
39
|
+
define_method :unscoped do |&block|
|
40
|
+
if MultiTenantSupport.disallow_read_across_tenant? || MultiTenantSupport.current_tenant
|
41
|
+
block ? relation.scope_under_current_tenant.scoping { block.call } : relation.scope_under_current_tenant
|
42
|
+
else
|
43
|
+
super(&block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
}
|
47
|
+
extend override_unscoped
|
48
|
+
|
49
|
+
override_insert_all = Module.new {
|
50
|
+
define_method :insert_all do |attributes, **arguments|
|
51
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
52
|
+
|
53
|
+
super(attributes, **arguments)
|
54
|
+
end
|
55
|
+
|
56
|
+
define_method :insert_all! do |attributes, **arguments|
|
57
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
58
|
+
|
59
|
+
super(attributes, **arguments)
|
60
|
+
end
|
61
|
+
}
|
62
|
+
extend override_insert_all
|
63
|
+
|
64
|
+
override_upsert_all = Module.new {
|
65
|
+
define_method :upsert_all do |attributes, **arguments|
|
66
|
+
warn "[WARNING] You are using upsert_all(or upsert) which may update records across tenants"
|
67
|
+
|
68
|
+
super(attributes, **arguments)
|
69
|
+
end
|
70
|
+
}
|
71
|
+
extend override_upsert_all
|
72
|
+
|
73
|
+
after_initialize do |object|
|
74
|
+
if MultiTenantSupport.disallow_read_across_tenant? || object.new_record?
|
75
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
76
|
+
raise InvalidTenantAccess if object.send(foreign_key) != MultiTenantSupport.current_tenant_id
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
before_save do |object|
|
81
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
82
|
+
raise NilTenantError if object.send(foreign_key).nil?
|
83
|
+
raise InvalidTenantAccess if object.send(foreign_key) != MultiTenantSupport.current_tenant_id
|
84
|
+
end
|
85
|
+
|
86
|
+
override_update_columns_module = Module.new {
|
87
|
+
define_method :update_columns do |attributes|
|
88
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
89
|
+
raise NilTenantError if send(foreign_key).nil?
|
90
|
+
raise InvalidTenantAccess if send(foreign_key) != MultiTenantSupport.current_tenant_id
|
91
|
+
|
92
|
+
super(attributes)
|
93
|
+
end
|
94
|
+
|
95
|
+
define_method :update_column do |name, value|
|
96
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
97
|
+
raise NilTenantError if send(foreign_key).nil?
|
98
|
+
raise InvalidTenantAccess if send(foreign_key) != MultiTenantSupport.current_tenant_id
|
99
|
+
|
100
|
+
super(name, value)
|
101
|
+
end
|
102
|
+
}
|
103
|
+
|
104
|
+
include override_update_columns_module
|
105
|
+
|
106
|
+
override_delete = Module.new {
|
107
|
+
define_method :delete do
|
108
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
109
|
+
raise InvalidTenantAccess if send(foreign_key) != MultiTenantSupport.current_tenant_id
|
110
|
+
|
111
|
+
super()
|
112
|
+
end
|
113
|
+
}
|
114
|
+
include override_delete
|
115
|
+
|
116
|
+
override_delete_by = Module.new {
|
117
|
+
define_method :delete_by do |*args|
|
118
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
119
|
+
|
120
|
+
super(*args)
|
121
|
+
end
|
122
|
+
}
|
123
|
+
extend override_delete_by
|
124
|
+
|
125
|
+
before_destroy do |object|
|
126
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
127
|
+
raise InvalidTenantAccess if object.send(foreign_key) != MultiTenantSupport.current_tenant_id
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def set_tenant_account_readonly(tenant_name, foreign_key)
|
132
|
+
readonly_tenant_module = Module.new {
|
133
|
+
|
134
|
+
define_method "#{tenant_name}=" do |tenant|
|
135
|
+
raise NilTenantError if tenant.nil?
|
136
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
137
|
+
|
138
|
+
if new_record? && tenant == MultiTenantSupport.current_tenant
|
139
|
+
super tenant
|
140
|
+
else
|
141
|
+
raise ImmutableTenantError
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
define_method "#{foreign_key}=" do |key|
|
146
|
+
raise NilTenantError if key.nil?
|
147
|
+
raise MissingTenantError unless MultiTenantSupport.current_tenant
|
148
|
+
|
149
|
+
if new_record? && key == MultiTenantSupport.current_tenant_id
|
150
|
+
super key
|
151
|
+
else
|
152
|
+
raise ImmutableTenantError
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
}
|
157
|
+
|
158
|
+
include readonly_tenant_module
|
159
|
+
|
160
|
+
attr_readonly foreign_key
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
ActiveSupport.on_load(:active_record) do |base|
|
169
|
+
base.include MultiTenantSupport::ModelConcern
|
170
|
+
|
171
|
+
override_delete_all = Module.new {
|
172
|
+
define_method :delete_all do
|
173
|
+
current_tenant_exist = MultiTenantSupport.current_tenant
|
174
|
+
is_global_model = !MultiTenantSupport.model.tenanted_models.include?(klass.name)
|
175
|
+
raise MultiTenantSupport::MissingTenantError unless current_tenant_exist || is_global_model
|
176
|
+
|
177
|
+
super()
|
178
|
+
end
|
179
|
+
}
|
180
|
+
ActiveRecord::Relation.prepend override_delete_all
|
181
|
+
|
182
|
+
override_update_all = Module.new {
|
183
|
+
define_method :update_all do |updates|
|
184
|
+
current_tenant_exist = MultiTenantSupport.current_tenant
|
185
|
+
is_global_model = !MultiTenantSupport.model.tenanted_models.include?(klass.name)
|
186
|
+
raise MultiTenantSupport::MissingTenantError unless current_tenant_exist || is_global_model
|
187
|
+
|
188
|
+
super(updates)
|
189
|
+
end
|
190
|
+
}
|
191
|
+
ActiveRecord::Relation.prepend override_update_all
|
192
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
|
3
|
+
module Config
|
4
|
+
class App
|
5
|
+
attr_writer :excluded_subdomains,
|
6
|
+
:host
|
7
|
+
|
8
|
+
def excluded_subdomains
|
9
|
+
@excluded_subdomains ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def host
|
13
|
+
@host || raise("host is missing")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module_function
|
19
|
+
def app
|
20
|
+
@app ||= Config::App.new
|
21
|
+
return @app unless block_given?
|
22
|
+
|
23
|
+
yield @app
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
|
3
|
+
module Config
|
4
|
+
class Controller
|
5
|
+
attr_writer :current_tenant_account_method
|
6
|
+
|
7
|
+
def current_tenant_account_method
|
8
|
+
@current_tenant_account_method ||= :current_tenant_account
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module_function
|
15
|
+
def controller
|
16
|
+
@controller ||= Config::Controller.new
|
17
|
+
return @controller unless block_given?
|
18
|
+
|
19
|
+
yield @controller
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_tenant_account_method
|
23
|
+
controller.current_tenant_account_method
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
module Config
|
3
|
+
|
4
|
+
class Model
|
5
|
+
attr_writer :tenant_account_class_name,
|
6
|
+
:tenant_account_primary_key,
|
7
|
+
:default_foreign_key,
|
8
|
+
:tenanted_models
|
9
|
+
|
10
|
+
def tenant_account_class_name
|
11
|
+
@tenant_account_class_name || raise("tenant_account_class_name is missing")
|
12
|
+
end
|
13
|
+
|
14
|
+
def tenant_account_primary_key
|
15
|
+
@tenant_account_primary_key ||= :id
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_foreign_key
|
19
|
+
@default_foreign_key ||= "#{tenant_account_class_name.underscore}_id".to_sym
|
20
|
+
end
|
21
|
+
|
22
|
+
def tenanted_models
|
23
|
+
@tenanted_models ||= []
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
module_function
|
30
|
+
def model
|
31
|
+
@model ||= Config::Model.new
|
32
|
+
return @model unless block_given?
|
33
|
+
|
34
|
+
yield @model
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
class FindTenantAccount
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def call(subdomains:, domain:)
|
6
|
+
subdomain = subdomains.select do |subdomain|
|
7
|
+
excluded_subdomains.none? do |excluded_subdomain|
|
8
|
+
excluded_subdomain.to_s.downcase == subdomain.to_s.downcase
|
9
|
+
end
|
10
|
+
end.last.presence
|
11
|
+
|
12
|
+
subdomain ? find_by(subdomain: subdomain) : find_by(domain: domain)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def find_by(params)
|
18
|
+
tenant_account_class.find_by(params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def tenant_account_class
|
22
|
+
MultiTenantSupport.model.tenant_account_class_name.constantize
|
23
|
+
end
|
24
|
+
|
25
|
+
def excluded_subdomains
|
26
|
+
MultiTenantSupport.app.excluded_subdomains
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
|
4
|
+
initializer :add_generator_templates do
|
5
|
+
override_templates = File.expand_path("../generators/override", __dir__)
|
6
|
+
config.app_generators.templates.unshift(override_templates)
|
7
|
+
|
8
|
+
active_record_templates = File.expand_path("../generators/override/active_record", __dir__)
|
9
|
+
config.app_generators.templates.unshift(active_record_templates)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module MultiTenantSupport
|
2
|
+
module Sidekiq
|
3
|
+
|
4
|
+
class Client
|
5
|
+
def call(worker_class, msg, queue, redis_pool)
|
6
|
+
if MultiTenantSupport.current_tenant.present?
|
7
|
+
msg["multi_tenant_support"] ||= {
|
8
|
+
"class" => MultiTenantSupport.current_tenant.class.name,
|
9
|
+
"id" => MultiTenantSupport.current_tenant.id
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Server
|
18
|
+
def call(worker_instance, msg, queue)
|
19
|
+
if msg.has_key?("multi_tenant_support")
|
20
|
+
tenant_klass = msg["multi_tenant_support"]["class"].constantize
|
21
|
+
tenant_id = msg["multi_tenant_support"]["id"]
|
22
|
+
|
23
|
+
tenant_account = nil
|
24
|
+
MultiTenantSupport.allow_read_across_tenant do
|
25
|
+
tenant_account = tenant_klass.find tenant_id
|
26
|
+
end
|
27
|
+
|
28
|
+
MultiTenantSupport.under_tenant tenant_account do
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
else
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Sidekiq.configure_client do |config|
|
41
|
+
config.client_middleware do |chain|
|
42
|
+
chain.add MultiTenantSupport::Sidekiq::Client
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
Sidekiq.configure_server do |config|
|
47
|
+
config.client_middleware do |chain|
|
48
|
+
chain.add MultiTenantSupport::Sidekiq::Client
|
49
|
+
end
|
50
|
+
|
51
|
+
config.server_middleware do |chain|
|
52
|
+
if defined?(Sidekiq::Middleware::Server::RetryJobs)
|
53
|
+
chain.insert_before Sidekiq::Middleware::Server::RetryJobs, MultiTenantSupport::Sidekiq::Server
|
54
|
+
elsif defined?(Sidekiq::Batch::Server)
|
55
|
+
chain.insert_before Sidekiq::Batch::Server, MultiTenantSupport::Sidekiq::Server
|
56
|
+
else
|
57
|
+
chain.add MultiTenantSupport::Sidekiq::Server
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "multi_tenant_support/version"
|
2
|
+
require 'multi_tenant_support/railtie'
|
3
|
+
require "multi_tenant_support/errors"
|
4
|
+
require "multi_tenant_support/config/app"
|
5
|
+
require "multi_tenant_support/config/controller"
|
6
|
+
require "multi_tenant_support/config/model"
|
7
|
+
require "multi_tenant_support/current"
|
8
|
+
require "multi_tenant_support/find_tenant_account"
|
9
|
+
require "multi_tenant_support/concern/controller_concern"
|
10
|
+
require "multi_tenant_support/concern/model_concern"
|
11
|
+
|
12
|
+
module MultiTenantSupport
|
13
|
+
|
14
|
+
module_function
|
15
|
+
|
16
|
+
def configure(&block)
|
17
|
+
instance_eval(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_tenant
|
21
|
+
Current.tenant_account
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_tenant_id
|
25
|
+
Current.tenant_account&.send(model.tenant_account_primary_key)
|
26
|
+
end
|
27
|
+
|
28
|
+
def under_tenant(tenant_account, &block)
|
29
|
+
raise ArgumentError, "block is missing" if block.nil?
|
30
|
+
|
31
|
+
Current.set(tenant_account: tenant_account) do
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def disallow_read_across_tenant?
|
37
|
+
!Current.allow_read_across_tenant
|
38
|
+
end
|
39
|
+
|
40
|
+
def disallow_read_across_tenant
|
41
|
+
if block_given?
|
42
|
+
Current.set(allow_read_across_tenant: false) do
|
43
|
+
yield
|
44
|
+
end
|
45
|
+
else
|
46
|
+
Current.allow_read_across_tenant = false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def allow_read_across_tenant
|
51
|
+
if block_given?
|
52
|
+
Current.set(allow_read_across_tenant: true) do
|
53
|
+
yield
|
54
|
+
end
|
55
|
+
else
|
56
|
+
Current.allow_read_across_tenant = true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multi-tenant-support
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hopper Gee
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
description: Build a highly secure, multi-tenant rails app without data leak.
|
28
|
+
email:
|
29
|
+
- hopper.gee@hey.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- MIT-LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- lib/generators/multi_tenant_support/initializer_generator.rb
|
38
|
+
- lib/generators/multi_tenant_support/migration_generator.rb
|
39
|
+
- lib/generators/multi_tenant_support/templates/initializer.rb.tt
|
40
|
+
- lib/generators/multi_tenant_support/templates/migration.rb.tt
|
41
|
+
- lib/generators/override/active_record/migration/templates/create_table_migration.rb.tt
|
42
|
+
- lib/generators/override/active_record/model/model.rb.tt
|
43
|
+
- lib/multi_tenant_support.rb
|
44
|
+
- lib/multi_tenant_support/active_job.rb
|
45
|
+
- lib/multi_tenant_support/concern/controller_concern.rb
|
46
|
+
- lib/multi_tenant_support/concern/model_concern.rb
|
47
|
+
- lib/multi_tenant_support/config/app.rb
|
48
|
+
- lib/multi_tenant_support/config/controller.rb
|
49
|
+
- lib/multi_tenant_support/config/model.rb
|
50
|
+
- lib/multi_tenant_support/current.rb
|
51
|
+
- lib/multi_tenant_support/errors.rb
|
52
|
+
- lib/multi_tenant_support/find_tenant_account.rb
|
53
|
+
- lib/multi_tenant_support/railtie.rb
|
54
|
+
- lib/multi_tenant_support/sidekiq.rb
|
55
|
+
- lib/multi_tenant_support/version.rb
|
56
|
+
- lib/tasks/multi_tenant_support_tasks.rake
|
57
|
+
homepage: https://github.com/hoppergee/multi-tenant-support
|
58
|
+
licenses:
|
59
|
+
- MIT
|
60
|
+
metadata:
|
61
|
+
homepage_uri: https://github.com/hoppergee/multi-tenant-support
|
62
|
+
source_code_uri: https://github.com/hoppergee/multi-tenant-support
|
63
|
+
changelog_uri: https://github.com/hoppergee/multi-tenant-support/blob/main/CHANGELOG.md
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '2.6'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubygems_version: 3.2.15
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Build a highly secure, multi-tenant rails app without data leak.
|
83
|
+
test_files: []
|