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 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,13 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rake/testtask"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = false
11
+ end
12
+
13
+ task default: :test
@@ -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,6 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_column :<%= table_name %>, :domain, :string
4
+ add_column :<%= table_name %>, :subdomain, :string
5
+ end
6
+ end
@@ -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,8 @@
1
+ require 'active_support'
2
+
3
+ module MultiTenantSupport
4
+ class Current < ActiveSupport::CurrentAttributes
5
+ attribute :tenant_account,
6
+ :allow_read_across_tenant
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ module MultiTenantSupport
2
+ class Error < StandardError
3
+ end
4
+
5
+ class MissingTenantError < Error
6
+ end
7
+
8
+ class ImmutableTenantError < Error
9
+ end
10
+
11
+ class NilTenantError < Error
12
+ end
13
+
14
+ class InvalidTenantAccess < Error
15
+ end
16
+ 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,3 @@
1
+ module MultiTenantSupport
2
+ VERSION = '1.0.0'
3
+ 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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :multi_tenant_support do
3
+ # # Task goes here
4
+ # 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: []