├── test ├── dummy │ ├── log │ │ └── .keep │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── apple-touch-icon.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── app │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── models │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── account.rb │ │ │ ├── country.rb │ │ │ ├── application_record.rb │ │ │ ├── tag.rb │ │ │ ├── user_tag.rb │ │ │ └── user.rb │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── super_admin │ │ │ │ ├── dashboard_controller.rb │ │ │ │ └── application_controller.rb │ │ │ ├── user_tags_controller.rb │ │ │ ├── application_controller.rb │ │ │ ├── api │ │ │ │ ├── users_controller.rb │ │ │ │ └── application_controller.rb │ │ │ └── users_controller.rb │ │ ├── views │ │ │ ├── users │ │ │ │ ├── new.html.erb │ │ │ │ ├── edit.html.erb │ │ │ │ ├── show.html.erb │ │ │ │ └── index.html.erb │ │ │ ├── layouts │ │ │ │ ├── mailer.text.erb │ │ │ │ ├── mailer.html.erb │ │ │ │ └── application.html.erb │ │ │ ├── user_tags │ │ │ │ └── index.html.erb │ │ │ └── super_admin │ │ │ │ └── dashboard │ │ │ │ └── show.html.erb │ │ ├── helpers │ │ │ ├── application_helper.rb │ │ │ └── super_admin │ │ │ │ └── dashboard_helper.rb │ │ ├── channels │ │ │ └── application_cable │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ ├── mailers │ │ │ └── application_mailer.rb │ │ ├── jobs │ │ │ ├── user_name_update_job.rb │ │ │ └── application_job.rb │ │ └── javascript │ │ │ └── packs │ │ │ └── application.js │ ├── bin │ │ ├── rake │ │ ├── rails │ │ └── setup │ ├── config │ │ ├── environment.rb │ │ ├── initializers │ │ │ ├── mime_types.rb │ │ │ ├── application_controller_renderer.rb │ │ │ ├── cookies_serializer.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── sidekiq.rb │ │ │ ├── permissions_policy.rb │ │ │ ├── multi_tenant_support.rb │ │ │ ├── wrap_parameters.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── inflections.rb │ │ │ └── content_security_policy.rb │ │ ├── cable.yml │ │ ├── boot.rb │ │ ├── routes.rb │ │ ├── application.rb │ │ ├── database.yml │ │ ├── locales │ │ │ └── en.yml │ │ ├── storage.yml │ │ ├── puma.rb │ │ └── environments │ │ │ ├── test.rb │ │ │ └── development.rb │ ├── config.ru │ ├── db │ │ └── migrate │ │ │ ├── 20211003103500_add_country_id_to_users.rb │ │ │ ├── 20211003061201_create_tags.rb │ │ │ ├── 20211003103228_create_countries.rb │ │ │ ├── 20211003061731_create_user_tags.rb │ │ │ ├── 20210924125351_create_accounts.rb │ │ │ └── 20210924125633_create_users.rb │ └── Rakefile ├── fixtures │ ├── countries.yml │ ├── tags.yml │ ├── accounts.yml │ ├── user_tags.yml │ └── users.yml ├── spec_helper.rb ├── multi_tenant_support │ ├── config │ │ ├── controller_test.rb │ │ ├── app_test.rb │ │ ├── console_test.rb │ │ └── model_test.rb │ ├── concern │ │ └── model_concern │ │ │ └── belongs_to_tenant │ │ │ ├── build_association_test.rb │ │ │ ├── finder_test.rb │ │ │ ├── unscoped_test.rb │ │ │ ├── delete │ │ │ ├── delete_all_on_global_records_test.rb │ │ │ ├── delete_all_test.rb │ │ │ ├── destroy_all_test.rb │ │ │ ├── delete_test.rb │ │ │ ├── delete_all_on_batch_test.rb │ │ │ ├── destroy_on_class_test.rb │ │ │ ├── destroy_all_on_batch_test.rb │ │ │ ├── delete_on_collection_test.rb │ │ │ ├── destroy_on_collection_test.rb │ │ │ ├── delete_all_on_collection_test.rb │ │ │ ├── delete_on_class_test.rb │ │ │ ├── destroy_all_on_collection_test.rb │ │ │ ├── delete_by_test.rb │ │ │ ├── destroy_by_test.rb │ │ │ └── destroy_test.rb │ │ │ ├── default_scope_setup_test.rb │ │ │ ├── counter_test.rb │ │ │ ├── update │ │ │ ├── update_all_on_global_records_test.rb │ │ │ ├── update_all_on_batch_test.rb │ │ │ ├── update_all_test.rb │ │ │ ├── save_without_validate_test.rb │ │ │ ├── save_after_write_attribute_test.rb │ │ │ ├── update_column_test.rb │ │ │ ├── update_columns_test.rb │ │ │ ├── update_attribute_test.rb │ │ │ ├── save_test.rb │ │ │ └── upsert_test.rb │ │ │ ├── new_test.rb │ │ │ ├── load_test.rb │ │ │ ├── tenant_account_cannot_be_nil_test.rb │ │ │ ├── readonly_tenant_account_test.rb │ │ │ └── create │ │ │ ├── save_test.rb │ │ │ └── create_test.rb │ └── sidekiq_test.rb ├── support │ ├── asserts.rb │ ├── dsl.rb │ └── sidekiq_jobs_manager.rb ├── integration │ ├── customize_current_tenant_finder_test.rb │ ├── current_tenant_test.rb │ ├── current_tenant_for_action_controller_api_test.rb │ ├── active_job │ │ ├── sidekiq_adapter │ │ │ ├── perform_now_test.rb │ │ │ ├── preconfigured_perform_now_test.rb │ │ │ ├── preconfigured_perform_later_test.rb │ │ │ └── perform_later_test.rb │ │ ├── async_adapter │ │ │ ├── perform_now_test.rb │ │ │ ├── preconfigured_perform_now_test.rb │ │ │ ├── perform_later_test.rb │ │ │ └── preconfigured_perform_later_test.rb │ │ ├── test_adapter │ │ │ ├── perform_now_test.rb │ │ │ ├── preconfigured_perform_now_test.rb │ │ │ ├── perform_later_test.rb │ │ │ └── preconfigured_perform_later_test.rb │ │ └── inline_adapter │ │ │ ├── perform_now_test.rb │ │ │ ├── preconfigured_perform_now_test.rb │ │ │ ├── perform_later_test.rb │ │ │ └── preconfigured_perform_later_test.rb │ ├── current_tenant_protect_during_integration_test.rb │ └── current_tenant_protect_during_integration_test_spec.rb ├── generators │ └── initializer_generator_test.rb ├── test_helper.rb └── system │ ├── current_tenant_protect_during_system_test.rb │ └── current_tenant_protect_during_system_test_spec.rb ├── hero.png ├── lib ├── multi_tenant_support │ ├── version.rb │ ├── errors.rb │ ├── minitest.rb │ ├── config │ │ ├── console.rb │ │ ├── app.rb │ │ ├── controller.rb │ │ └── model.rb │ ├── rspec.rb │ ├── current.rb │ ├── railtie.rb │ ├── test │ │ ├── integration.rb │ │ ├── system.rb │ │ └── capybara.rb │ ├── find_tenant_account.rb │ ├── concern │ │ └── controller_concern.rb │ ├── sidekiq.rb │ └── active_job.rb ├── tasks │ └── multi_tenant_support_tasks.rake ├── generators │ ├── multi_tenant_support │ │ ├── templates │ │ │ ├── migration.rb.tt │ │ │ └── initializer.rb.tt │ │ ├── initializer_generator.rb │ │ └── migration_generator.rb │ └── override │ │ └── active_record │ │ ├── model │ │ └── model.rb.tt │ │ └── migration │ │ └── templates │ │ └── create_table_migration.rb.tt └── multi-tenant-support.rb ├── bin ├── test ├── setup └── console ├── Rakefile ├── docker-compose-github-action.yml ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── main.yaml ├── Gemfile ├── multi_tenant_support.gemspec ├── MIT-LICENSE ├── matrixeval.yml └── CHANGELOG.md /test/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 |

User - New

-------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /test/dummy/app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 |

User - Edit

-------------------------------------------------------------------------------- /test/dummy/app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 |

User - Show

-------------------------------------------------------------------------------- /test/fixtures/countries.yml: -------------------------------------------------------------------------------- 1 | us: 2 | name: United State 3 | code: us -------------------------------------------------------------------------------- /hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoppergee/multi-tenant-support/HEAD/hero.png -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/version.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | VERSION = '1.5.0' 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/super_admin/dashboard_helper.rb: -------------------------------------------------------------------------------- 1 | module SuperAdmin::DashboardHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/account.rb: -------------------------------------------------------------------------------- 1 | class Account < ApplicationRecord 2 | has_many :users 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/tags.yml: -------------------------------------------------------------------------------- 1 | entrepreneur: 2 | name: entrepreneur 3 | 4 | engineer: 5 | name: engineer 6 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/country.rb: -------------------------------------------------------------------------------- 1 | class Country < ApplicationRecord 2 | has_many :users, dependent: :destroy 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ApplicationRecord 2 | has_many :user_tags 3 | has_many :users, through: :user_tags 4 | end 5 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path("../test", __dir__) 3 | 4 | require "bundler/setup" 5 | require "rails/plugin/test" 6 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/tasks/multi_tenant_support_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :multi_tenant_support do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/user_tag.rb: -------------------------------------------------------------------------------- 1 | class UserTag < ApplicationRecord 2 | belongs_to_tenant :account 3 | belongs_to :user 4 | belongs_to :tag 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/super_admin/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | module SuperAdmin 2 | class DashboardController < ApplicationController 3 | def show 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/user_name_update_job.rb: -------------------------------------------------------------------------------- 1 | class UserNameUpdateJob < ApplicationJob 2 | queue_as :integration_tests 3 | 4 | def perform(user) 5 | user.update(name: user.name + " UPDATE") 6 | end 7 | end -------------------------------------------------------------------------------- /test/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | belongs_to_tenant :account 3 | has_many :user_tags 4 | has_many :tags, through: :user_tags 5 | belongs_to :country, optional: true 6 | end 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20211003103500_add_country_id_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddCountryIdToUsers < ActiveRecord::Migration[ENV['MIGRATION_VESRION']] 2 | def change 3 | add_reference :users, :country 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/user_tags_controller.rb: -------------------------------------------------------------------------------- 1 | class UserTagsController < ApplicationController 2 | def index 3 | end 4 | 5 | private 6 | 7 | def find_current_tenant_account 8 | Account.find_by(domain: request.domain) 9 | end 10 | end -------------------------------------------------------------------------------- /test/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20211003061201_create_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateTags < ActiveRecord::Migration[ENV['MIGRATION_VESRION']] 2 | def change 3 | create_table :tags do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /test/fixtures/accounts.yml: -------------------------------------------------------------------------------- 1 | amazon: 2 | name: Amazon 3 | domain: amazon.com 4 | subdomain: amazon 5 | 6 | facebook: 7 | name: Facebook 8 | domain: facebook.com 9 | subdomain: facebook 10 | 11 | apple: 12 | name: Apple 13 | domain: apple.com 14 | subdomain: apple 15 | -------------------------------------------------------------------------------- /lib/generators/multi_tenant_support/templates/migration.rb.tt: -------------------------------------------------------------------------------- 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 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20211003103228_create_countries.rb: -------------------------------------------------------------------------------- 1 | class CreateCountries < ActiveRecord::Migration[ENV['MIGRATION_VESRION']] 2 | def change 3 | create_table :countries do |t| 4 | t.string :code 5 | t.string :name 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/user_tags.yml: -------------------------------------------------------------------------------- 1 | entrepreneur_zuck: 2 | account: amazon 3 | tag: entrepreneur 4 | user: zuck 5 | 6 | entrepreneur_bezos: 7 | account: amazon 8 | tag: entrepreneur 9 | user: bezos 10 | 11 | entrepreneur_steve: 12 | account: apple 13 | tag: entrepreneur 14 | user: steve 15 | -------------------------------------------------------------------------------- /test/dummy/app/views/user_tags/index.html.erb: -------------------------------------------------------------------------------- 1 |

UserTags#index

2 |
<%= current_tenant_account.id %>
3 |
<%= current_tenant_account.name %>
4 |
<%= current_tenant_account.domain %>
5 |
<%= current_tenant_account.subdomain %>
-------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /docker-compose-github-action.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | postgres: 6 | image: postgres:12.8 7 | environment: 8 | POSTGRES_HOST_AUTH_METHOD: trust 9 | ports: 10 | - '5432:5432' 11 | 12 | redis: 13 | image: redis:6.2-alpine 14 | ports: 15 | - '6379:6379' 16 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | before_action :super_admin_redirect 3 | 4 | private 5 | 6 | def super_admin_redirect 7 | return if current_tenant_account 8 | 9 | redirect_to super_admin_dashboard_path 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/errors.rb: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq' 2 | 3 | Sidekiq.configure_server do |config| 4 | config.redis = { url: "redis://#{ENV['REDIS_HOST'] || '127.0.0.1'}:6379/0" } 5 | end 6 | 7 | Sidekiq.configure_client do |config| 8 | config.redis = { url: "redis://#{ENV['REDIS_HOST'] || '127.0.0.1'}:6379/0" } 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 |

Users#index

2 |
<%= current_tenant_account.id %>
3 |
<%= current_tenant_account.name %>
4 |
<%= current_tenant_account.domain %>
5 |
<%= current_tenant_account.subdomain %>
6 | 7 | <%= link_to "Show", user_path(id: 1) %> -------------------------------------------------------------------------------- /test/dummy/db/migrate/20211003061731_create_user_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateUserTags < ActiveRecord::Migration[ENV['MIGRATION_VESRION']] 2 | def change 3 | create_table :user_tags do |t| 4 | t.belongs_to :account, null: false 5 | t.belongs_to :user 6 | t.belongs_to :tag 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class UsersController < ApplicationController 3 | def index 4 | render json: { 5 | tenant: current_tenant_account.name, 6 | domain: current_tenant_account.domain, 7 | subdomain: current_tenant_account.subdomain 8 | } 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /test/dummy/db/migrate/20210924125351_create_accounts.rb: -------------------------------------------------------------------------------- 1 | class CreateAccounts < ActiveRecord::Migration[ENV['MIGRATION_VESRION']] 2 | def change 3 | create_table :accounts do |t| 4 | t.column :name, :string 5 | t.column :subdomain, :string 6 | t.column :domain, :string 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | bezos: 2 | name: Jeff Bezos 3 | email: bezos@example.com 4 | account: amazon 5 | country: us 6 | 7 | zuck: 8 | name: Mark Zuckerberg 9 | email: zuck@example.com 10 | account: facebook 11 | country: us 12 | 13 | steve: 14 | name: Steve Jobs 15 | email: steve@example.com 16 | account: apple 17 | country: us 18 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/minitest.rb: -------------------------------------------------------------------------------- 1 | require_relative "./test/integration" 2 | require_relative "./test/system" 3 | require_relative "./test/capybara" 4 | 5 | ActionDispatch::IntegrationTest.prepend(MultiTenantSupport::Test::Integration) 6 | ActionDispatch::SystemTestCase.prepend(MultiTenantSupport::Test::System) 7 | Capybara::Node::Element.prepend(MultiTenantSupport::Test::Capybara) -------------------------------------------------------------------------------- /test/dummy/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | def index 3 | end 4 | 5 | def new 6 | end 7 | 8 | def edit 9 | end 10 | 11 | def create 12 | redirect_to users_path 13 | end 14 | 15 | def update 16 | redirect_to users_path 17 | end 18 | 19 | def destroy 20 | redirect_to users_path 21 | end 22 | end -------------------------------------------------------------------------------- /test/dummy/db/migrate/20210924125633_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[ENV['MIGRATION_VESRION']] 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :email, null: false 6 | t.integer :account_id 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :users, :email, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html 3 | resources :users 4 | resources :user_tags 5 | 6 | namespace :super_admin do 7 | resource :dashboard, controller: :dashboard 8 | end 9 | 10 | namespace :api do 11 | resources :users 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /log/*.log 4 | /pkg/ 5 | /tmp/ 6 | /test/dummy/db/*.sqlite3 7 | /test/dummy/db/*.sqlite3-* 8 | /test/dummy/db/schema.rb 9 | /test/dummy/log/*.log 10 | /test/dummy/storage/ 11 | /test/dummy/tmp/ 12 | .byebug_history 13 | Gemfile.lock 14 | /test/generators/tmp/ 15 | .DS_Store 16 | .matrixeval/docker-compose 17 | .matrixeval/gemfile_locks 18 | .matrixeval/schema 19 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag 'application', media: 'all' %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/super_admin/application_controller.rb: -------------------------------------------------------------------------------- 1 | module SuperAdmin 2 | class ApplicationController < ::ApplicationController 3 | before_action :allow_read_across_tenant 4 | skip_before_action :super_admin_redirect 5 | skip_before_action :set_current_tenant_account 6 | 7 | private 8 | 9 | def allow_read_across_tenant 10 | MultiTenantSupport.allow_read_across_tenant 11 | end 12 | 13 | end 14 | end -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "rails" 6 | require "multi-tenant-support" 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | # (If you use this, don't forget to add pry to your Gemfile!) 12 | # require "pry" 13 | # Pry.start 14 | 15 | require "irb" 16 | IRB.start(__FILE__) 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /lib/generators/multi_tenant_support/initializer_generator.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/config/console.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | 3 | module Config 4 | class Console 5 | attr_writer :allow_read_across_tenant_by_default 6 | 7 | def allow_read_across_tenant_by_default 8 | @allow_read_across_tenant_by_default ||= false 9 | end 10 | end 11 | end 12 | 13 | module_function 14 | def console 15 | @console ||= Config::Console.new 16 | return @console unless block_given? 17 | 18 | yield @console 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /test/dummy/app/views/super_admin/dashboard/show.html.erb: -------------------------------------------------------------------------------- 1 |

SuperAdmin Dashboard

2 | <% if current_tenant_account %> 3 |
Current tenant exist
4 | <% else %> 5 |
Current tenant does not exist
6 | <% end %> 7 |
<%= User.count %>
8 |
<%= User.find_by(email: 'bezos@example.com')&.name %>
9 |
<%= User.find_by(email: 'zuck@example.com')&.name %>
10 |
<%= User.find_by(email: 'steve@example.com')&.name %>
-------------------------------------------------------------------------------- /test/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require_relative "../test/dummy/config/environment" 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 6 | ActiveRecord::Migration.maintain_test_schema! 7 | 8 | require "rspec/rails" 9 | require 'multi_tenant_support/rspec' 10 | 11 | RSpec.configure do |config| 12 | config.fixture_path = "test/fixtures" 13 | config.global_fixtures = :all 14 | config.use_transactional_fixtures = true 15 | end 16 | 17 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/config/app.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/rspec.rb: -------------------------------------------------------------------------------- 1 | require_relative "./test/integration" 2 | require_relative "./test/system" 3 | require_relative "./test/capybara" 4 | 5 | RSpec.configure do |config| 6 | config.include MultiTenantSupport::Test::Integration, type: :request 7 | config.include MultiTenantSupport::Test::Integration, type: :controller 8 | 9 | config.include MultiTenantSupport::Test::System, type: :system 10 | config.include MultiTenantSupport::Test::System, type: :feature 11 | end 12 | 13 | Capybara::Node::Element.prepend(MultiTenantSupport::Test::Capybara) 14 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/api/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ApplicationController < ActionController::API 3 | before_action :check_current_tenant 4 | rescue_from MultiTenantSupport::MissingTenantError, with: :handle_error 5 | 6 | private 7 | 8 | def check_current_tenant 9 | raise MultiTenantSupport::MissingTenantError.new("Wrong domain or subdomain") unless current_tenant_account 10 | end 11 | 12 | def handle_error(error) 13 | render json: { 14 | error: error 15 | } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/multi_tenant_support.rb: -------------------------------------------------------------------------------- 1 | MultiTenantSupport.configure do |config| 2 | model do |config| 3 | config.tenant_account_class_name = 'Account' 4 | config.tenant_account_primary_key = :id 5 | end 6 | 7 | controller do |config| 8 | config.current_tenant_account_method = :current_tenant_account 9 | end 10 | 11 | app do |config| 12 | config.excluded_subdomains = ['www'] 13 | config.host = 'example.com' 14 | end 15 | 16 | console do |config| 17 | config.allow_read_across_tenant_by_default = false 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/current.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | 3 | module MultiTenantSupport 4 | 5 | # Scoped and proteced 6 | PROTECTED = 1 7 | 8 | # Scoped and protected except read across tenant 9 | PROTECTED_EXCEPT_READ = 2 10 | 11 | # Scoped but unprotected 12 | UNPROTECTED = 3 13 | 14 | # This class is for internal usage only 15 | class Current < ActiveSupport::CurrentAttributes 16 | attribute :tenant_account, 17 | :protection_state 18 | 19 | def protection_state 20 | attributes[:protection_state] ||= PROTECTED 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /test/multi_tenant_support/config/controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MultiTenantSupport::Config::ControllerTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | @controller_config = MultiTenantSupport::Config::Controller.new 7 | end 8 | 9 | test "#current_tenant_account_method and #current_tenant_account_method=" do 10 | assert_equal :current_tenant_account, @controller_config.current_tenant_account_method 11 | 12 | @controller_config.current_tenant_account_method = :current_account 13 | assert_equal :current_account, @controller_config.current_tenant_account_method 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /lib/multi_tenant_support/config/controller.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | if Gem::Version.new(Rails.version) < Gem::Version.new("7.0.0.alpha2") 5 | Rails.application.config.assets.version = '1.0' 6 | end 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /test/multi_tenant_support/config/app_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MultiTenantSupport::Config::AppTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | @app_config = MultiTenantSupport::Config::App.new 7 | end 8 | 9 | test "#excluded_subdomains and #excluded_subdomains=" do 10 | assert_equal [], @app_config.excluded_subdomains 11 | 12 | @app_config.excluded_subdomains = ["www"] 13 | assert_equal ["www"], @app_config.excluded_subdomains 14 | end 15 | 16 | test "#host and #host=" do 17 | assert_raise "host is missing" do 18 | @app_config.host 19 | end 20 | 21 | @app_config.host = "example.com" 22 | assert_equal "example.com", @app_config.host 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: hoppergee 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/multi_tenant_support/config/console_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MultiTenantSupport::Config::ConsoleTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | @console_config = MultiTenantSupport::Config::Console.new 7 | end 8 | 9 | test "#allow_read_across_tenant_by_default and #allow_read_across_tenant_by_default=" do 10 | assert_equal false, @console_config.allow_read_across_tenant_by_default 11 | 12 | @console_config.allow_read_across_tenant_by_default = true 13 | assert_equal true, @console_config.allow_read_across_tenant_by_default 14 | 15 | @console_config.allow_read_across_tenant_by_default = false 16 | assert_equal false, @console_config.allow_read_across_tenant_by_default 17 | end 18 | 19 | end -------------------------------------------------------------------------------- /test/support/asserts.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | module Asserts 3 | 4 | def multi_attempt_assert(failure_message) 5 | attempt = 0 6 | success = false 7 | 8 | loop do 9 | sleep 0.5 10 | 11 | success = yield 12 | 13 | attempt += 1 14 | break if success 15 | break if attempt >= 4 16 | end 17 | 18 | assert success, failure_message 19 | end 20 | 21 | def wait_until(interval = 0.5, max_attempt = 4) 22 | attempt = 0 23 | success = false 24 | 25 | loop do 26 | sleep interval 27 | 28 | success = yield 29 | 30 | attempt += 1 31 | break if success 32 | break if attempt >= max_attempt 33 | end 34 | end 35 | 36 | end 37 | end -------------------------------------------------------------------------------- /lib/generators/multi_tenant_support/templates/initializer.rb.tt: -------------------------------------------------------------------------------- 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_account_method = :current_tenant_account 9 | end 10 | 11 | app do |config| 12 | config.excluded_subdomains = ['www'] 13 | config.host = 'REPLACE.ME' 14 | end 15 | 16 | console do |config| 17 | config.allow_read_across_tenant_by_default = false 18 | end 19 | end 20 | 21 | # Uncomment if you are using sidekiq without ActiveJob 22 | # require 'multi_tenant_support/sidekiq' 23 | 24 | # Uncomment if you are using ActiveJob 25 | # require 'multi_tenant_support/active_job' 26 | -------------------------------------------------------------------------------- /lib/generators/multi_tenant_support/migration_generator.rb: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | require "multi-tenant-support" 9 | 10 | module Dummy 11 | class Application < Rails::Application 12 | config.load_defaults Rails::VERSION::STRING.to_f 13 | 14 | # Configuration for the application, engines, and railties goes here. 15 | # 16 | # These settings can be overridden in specific environments using the files 17 | # in config/environments, which are processed later. 18 | # 19 | # config.time_zone = "Central Time (US & Canada)" 20 | # config.eager_load_paths << Rails.root.join("extras") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/railtie.rb: -------------------------------------------------------------------------------- 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 | console do 13 | if ENV["ALLOW_READ_ACROSS_TENANT"] || MultiTenantSupport.console.allow_read_across_tenant_by_default 14 | MultiTenantSupport.allow_read_across_tenant 15 | else 16 | MultiTenantSupport.turn_on_full_protection 17 | end 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/build_association_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_BuildAssociationTest < ActiveSupport::TestCase 4 | 5 | test 'bezos.account is amazon' do 6 | MultiTenantSupport.under_tenant accounts(:amazon) do 7 | assert_equal accounts(:amazon), users(:bezos).account 8 | end 9 | end 10 | 11 | test 'zuck.account is facebook' do 12 | MultiTenantSupport.under_tenant accounts(:facebook) do 13 | assert_equal accounts(:facebook), users(:zuck).account 14 | end 15 | end 16 | 17 | test 'steve.account is apple' do 18 | MultiTenantSupport.under_tenant accounts(:apple) do 19 | assert_equal accounts(:apple), users(:steve).account 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | if rails_version = ENV['RAILS_VERSION'] 5 | gem 'rails', rails_version 6 | end 7 | 8 | # Specify your gem's dependencies in multi_tenant_support.gemspec. 9 | gemspec 10 | 11 | group :development do 12 | gem 'sqlite3' 13 | gem 'pg' 14 | end 15 | 16 | # To use a debugger 17 | group :development, :test do 18 | gem 'matrixeval-ruby' 19 | gem 'byebug' 20 | gem 'rspec' 21 | gem 'rspec-rails' 22 | gem 'capybara' 23 | gem 'sidekiq' 24 | 25 | # Ruby 3.1 split out the net-smtp gem 26 | # Necessary until https://github.com/mikel/mail/pull/1439 27 | # got merged and released. 28 | if Gem.ruby_version >= Gem::Version.new("3.1.0") 29 | gem "net-smtp", "~> 0.3.0", require: false 30 | end 31 | end 32 | 33 | group :test do 34 | gem 'minitest-focus' 35 | end 36 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: postgresql 9 | encoding: unicode 10 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 11 | host: <%= ENV['DATABASE_HOST'] || '127.0.0.1' %> 12 | username: <%= ENV['DATABASE_USERNAME'] %> 13 | 14 | development: 15 | <<: *default 16 | database: multi_tenant_support_development 17 | 18 | # Warning: The database defined as "test" will be erased and 19 | # re-generated from your development database when you run "rake". 20 | # Do not set this db to the same as development or production. 21 | test: 22 | <<: *default 23 | database: multi_tenant_support_test 24 | 25 | production: 26 | <<: *default 27 | database: multi_tenant_support_production 28 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/test/integration.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | module Test 3 | module Integration 4 | 5 | %i[get post patch put delete head options].each do |method| 6 | define_method method do |path, **args| 7 | keep_context_tenant_unchange do 8 | super(path, **args) 9 | end 10 | end 11 | end 12 | 13 | def follow_redirect(**args) 14 | keep_context_tenant_unchange do 15 | super(**args) 16 | end 17 | end 18 | 19 | def keep_context_tenant_unchange 20 | _current_tenant = MultiTenantSupport::Current.tenant_account 21 | MultiTenantSupport::Current.tenant_account = nil # Simulate real circumstance 22 | yield 23 | ensure 24 | MultiTenantSupport::Current.tenant_account = _current_tenant 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/find_tenant_account.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/multi_tenant_support/config/model.rb: -------------------------------------------------------------------------------- 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 37 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /multi_tenant_support.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/multi_tenant_support/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "multi-tenant-support" 5 | spec.version = MultiTenantSupport::VERSION 6 | spec.authors = ["Hopper Gee"] 7 | spec.email = ["hopper.gee@hey.com"] 8 | spec.homepage = "https://github.com/hoppergee/multi-tenant-support" 9 | spec.summary = "Build a highly secure, multi-tenant rails app without data leak." 10 | spec.description = "Build a highly secure, multi-tenant rails app without data leak." 11 | spec.license = "MIT" 12 | 13 | spec.metadata["homepage_uri"] = spec.homepage 14 | spec.metadata["source_code_uri"] = "https://github.com/hoppergee/multi-tenant-support" 15 | spec.metadata["changelog_uri"] = "https://github.com/hoppergee/multi-tenant-support/blob/main/CHANGELOG.md" 16 | 17 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 18 | 19 | spec.required_ruby_version = '>= 2.6' 20 | 21 | spec.add_dependency "rails", ">= 6.1" 22 | end 23 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /lib/generators/override/active_record/model/model.rb.tt: -------------------------------------------------------------------------------- 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 -%> 27 | -------------------------------------------------------------------------------- /test/integration/customize_current_tenant_finder_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CustomizeCurrentTenantFinderTest < ActionDispatch::IntegrationTest 4 | 5 | setup do 6 | @amazon = accounts(:amazon) 7 | end 8 | 9 | test "find correct tenant with subdomain" do 10 | host! "amazon.example.com" 11 | get user_tags_path 12 | 13 | assert_redirected_to super_admin_dashboard_path 14 | follow_redirect! 15 | assert_response :success 16 | assert_select "h1", text: "SuperAdmin Dashboard" 17 | end 18 | 19 | test "find correct tenant with domain" do 20 | host! "amazon.com" 21 | get user_tags_path 22 | 23 | assert_response :success 24 | assert_select "#id", text: @amazon.id.to_s 25 | assert_select "#name", text: "Amazon" 26 | assert_select "#domain", text: "amazon.com" 27 | assert_select "#subdomain", text: "amazon" 28 | end 29 | 30 | test "can't find tenant when visit the host domain" do 31 | host! "example.com" 32 | get user_tags_path 33 | 34 | assert_redirected_to super_admin_dashboard_path 35 | follow_redirect! 36 | assert_response :success 37 | assert_select "h1", text: "SuperAdmin Dashboard" 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/test/system.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | module Test 3 | module System 4 | 5 | %i[ 6 | visit refresh click_on go_back go_forward 7 | check choose click_button click_link 8 | fill_in uncheck check unselect select 9 | execute_script evaluate_script 10 | ].each do |method| 11 | if RUBY_VERSION >= '2.7' 12 | class_eval <<~METHOD, __FILE__, __LINE__ + 1 13 | def #{method}(...) 14 | keep_context_tenant_unchange do 15 | super(...) 16 | end 17 | end 18 | METHOD 19 | else 20 | define_method method do |*args, &block| 21 | keep_context_tenant_unchange do 22 | super(*args, &block) 23 | end 24 | end 25 | end 26 | end 27 | 28 | def keep_context_tenant_unchange 29 | _current_tenant = MultiTenantSupport::Current.tenant_account 30 | MultiTenantSupport::Current.tenant_account = nil # Simulate real circumstance 31 | yield 32 | ensure 33 | MultiTenantSupport::Current.tenant_account = _current_tenant 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/integration/current_tenant_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CurrentTenantTest < ActionDispatch::IntegrationTest 4 | 5 | setup do 6 | @amazon = accounts(:amazon) 7 | end 8 | 9 | test "find correct tenant with subdomain" do 10 | host! "amazon.example.com" 11 | get users_path 12 | 13 | assert_response :success 14 | assert_select "#id", text: @amazon.id.to_s 15 | assert_select "#name", text: "Amazon" 16 | assert_select "#domain", text: "amazon.com" 17 | assert_select "#subdomain", text: "amazon" 18 | end 19 | 20 | test "find correct tenant with domain" do 21 | host! "amazon.com" 22 | get users_path 23 | 24 | assert_response :success 25 | assert_select "#id", text: @amazon.id.to_s 26 | assert_select "#name", text: "Amazon" 27 | assert_select "#domain", text: "amazon.com" 28 | assert_select "#subdomain", text: "amazon" 29 | end 30 | 31 | test "can't find tenant when visit the host domain" do 32 | host! "example.com" 33 | get users_path 34 | 35 | assert_redirected_to super_admin_dashboard_path 36 | follow_redirect! 37 | assert_response :success 38 | assert_select "h1", text: "SuperAdmin Dashboard" 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/multi_tenant_support/sidekiq_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'sidekiq' 3 | require 'multi_tenant_support/sidekiq' 4 | 5 | class MultiTenantSupport::SidekiqTest < ActiveSupport::TestCase 6 | 7 | test "Client success set current tenant id to msg" do 8 | msg = {} 9 | MultiTenantSupport::Sidekiq::Client.new.call(nil, msg, nil, nil) {} 10 | assert msg["multi_tenant_support"].nil? 11 | 12 | MultiTenantSupport.under_tenant accounts(:amazon) do 13 | msg = {} 14 | MultiTenantSupport::Sidekiq::Client.new.call(nil, msg, nil, nil) {} 15 | assert "Account", msg["multi_tenant_support"]["class"] 16 | assert accounts(:amazon).id, msg["multi_tenant_support"]["id"] 17 | end 18 | end 19 | 20 | test "Server success set current tenant if need" do 21 | msg = {} 22 | MultiTenantSupport::Sidekiq::Server.new.call(nil, msg, nil) {} 23 | assert MultiTenantSupport.current_tenant.nil? 24 | 25 | msg = { 26 | "multi_tenant_support" => { 27 | "class" => "Account", 28 | "id" => accounts(:amazon).id 29 | } 30 | } 31 | current_tenant = nil 32 | MultiTenantSupport::Sidekiq::Server.new.call(nil, msg, nil) do 33 | current_tenant = MultiTenantSupport.current_tenant 34 | end 35 | assert_equal accounts(:amazon), current_tenant 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/finder_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_FinderTest < ActiveSupport::TestCase 4 | 5 | test "won't raise error on missing tenant when all read across tennat" do 6 | allow_read_across_tenant do 7 | refute User.first.nil? 8 | refute User.last.nil? 9 | refute User.where(name: 'Jeff Bezos').first.nil? 10 | refute User.find_by(name: 'Mark Zuckerberg').nil? 11 | end 12 | end 13 | 14 | test "raise error on missing tenant when turn on full protection" do 15 | turn_on_full_protection do 16 | assert_raise(MultiTenantSupport::MissingTenantError) { User.first } 17 | assert_raise(MultiTenantSupport::MissingTenantError) { User.last } 18 | assert_raise(MultiTenantSupport::MissingTenantError) { User.where(name: 'Jeff Bezos') } 19 | assert_raise(MultiTenantSupport::MissingTenantError) { User.find_by(name: 'Mark Zuckerberg') } 20 | end 21 | end 22 | 23 | test "won't raise error on missing tenant when turn off protection" do 24 | turn_off_protection do 25 | refute User.first.nil? 26 | refute User.last.nil? 27 | refute User.where(name: 'Jeff Bezos').first.nil? 28 | refute User.find_by(name: 'Mark Zuckerberg').nil? 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Set the nonce only to specific directives 23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 24 | 25 | # Report CSP violations to a specified URI 26 | # For further information see the following documentation: 27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 28 | # Rails.application.config.content_security_policy_report_only = true 29 | -------------------------------------------------------------------------------- /test/integration/current_tenant_for_action_controller_api_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CurrentTenantForActionControllerApiTest < ActionDispatch::IntegrationTest 4 | 5 | setup do 6 | @amazon = accounts(:amazon) 7 | end 8 | 9 | test "find correct tenant with subdomain" do 10 | host! "amazon.example.com" 11 | get api_users_path 12 | 13 | assert_response :success 14 | assert_json_response({ 15 | "tenant"=>"Amazon", 16 | "domain"=>"amazon.com", 17 | "subdomain"=>"amazon" 18 | }) 19 | end 20 | 21 | test "find correct tenant with domain" do 22 | host! "amazon.com" 23 | get api_users_path 24 | 25 | assert_response :success 26 | assert_json_response({ 27 | "tenant"=>"Amazon", 28 | "domain"=>"amazon.com", 29 | "subdomain"=>"amazon" 30 | }) 31 | end 32 | 33 | test "can't find tenant when visit the host domain" do 34 | host! "example.com" 35 | get api_users_path 36 | 37 | assert_response :success 38 | assert_json_response({ 39 | "error" => "Wrong domain or subdomain" 40 | }) 41 | end 42 | 43 | def assert_json_response(hash) 44 | body = JSON.parse(response.body) 45 | 46 | assert_kind_of Hash, body 47 | assert_equal hash.count, body.count 48 | hash.each do |key, expected| 49 | assert_equal expected, body[key] 50 | end 51 | end 52 | 53 | end -------------------------------------------------------------------------------- /lib/multi_tenant_support/concern/controller_concern.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | 3 | module ControllerConcern 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | include ViewHelper 8 | 9 | before_action :set_current_tenant_account 10 | 11 | private 12 | 13 | def set_current_tenant_account 14 | tenant_account = find_current_tenant_account 15 | MultiTenantSupport.set_current_tenant(tenant_account) 16 | instance_variable_set("@#{MultiTenantSupport.current_tenant_account_method}", tenant_account) 17 | end 18 | 19 | # A user can override this method, if he need a customize way 20 | def find_current_tenant_account 21 | MultiTenantSupport::FindTenantAccount.call( 22 | subdomains: request.subdomains, 23 | domain: request.domain 24 | ) 25 | end 26 | end 27 | end 28 | 29 | module ViewHelper 30 | extend ActiveSupport::Concern 31 | 32 | included do 33 | define_method(MultiTenantSupport.current_tenant_account_method) do 34 | instance_variable_get("@#{MultiTenantSupport.current_tenant_account_method}") 35 | end 36 | end 37 | end 38 | end 39 | 40 | ActiveSupport.on_load(:action_controller) do |base| 41 | base.include MultiTenantSupport::ControllerConcern 42 | end 43 | 44 | ActiveSupport.on_load(:action_view) do |base| 45 | base.include MultiTenantSupport::ViewHelper 46 | end -------------------------------------------------------------------------------- /lib/generators/override/active_record/migration/templates/create_table_migration.rb.tt: -------------------------------------------------------------------------------- 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 31 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/unscoped_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_UnscopedTest < ActiveSupport::TestCase 4 | 5 | test ".unscoped - won't scope other tenants' records when disallow read across tenant (default) and current tenant exits" do 6 | MultiTenantSupport.under_tenant accounts(:amazon) do 7 | assert_equal 1, User.unscoped.count 8 | end 9 | end 10 | 11 | test ".unscoped - raise error when disallow read across tenant (default) and missing current tenant" do 12 | assert_raise(MultiTenantSupport::MissingTenantError) { User.unscoped.count } 13 | end 14 | 15 | test ".unscoped - won't scope other tenants' records when allow read across tenant (default) and current tenant exits" do 16 | MultiTenantSupport.allow_read_across_tenant do 17 | MultiTenantSupport.under_tenant accounts(:amazon) do 18 | assert_equal 1, User.unscoped.count 19 | end 20 | end 21 | end 22 | 23 | test ".unscoped - scope all tenants' records when allow read across tenant (default) and missing current tenant" do 24 | MultiTenantSupport.allow_read_across_tenant do 25 | assert_equal 3, User.where(name: 'Jeff Bezos').unscoped.count 26 | end 27 | end 28 | 29 | test ".unscoped - scope all tenants' records when turn off protection" do 30 | MultiTenantSupport.turn_off_protection do 31 | assert_equal 3, User.where(name: 'Jeff Bezos').unscoped.count 32 | end 33 | end 34 | 35 | end -------------------------------------------------------------------------------- /lib/multi_tenant_support/test/capybara.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | module Test 3 | module Capybara 4 | 5 | def set(value, **options) 6 | keep_context_tenant_unchange do 7 | super(value, **options) 8 | end 9 | end 10 | 11 | def select_option(wait: nil) 12 | keep_context_tenant_unchange do 13 | super(wait: wait) 14 | end 15 | end 16 | 17 | def unselect_option(wait: nil) 18 | keep_context_tenant_unchange do 19 | super(wait: wait) 20 | end 21 | end 22 | 23 | def perform_click_action(keys, wait: nil, **options) 24 | keep_context_tenant_unchange do 25 | super 26 | end 27 | end 28 | 29 | def trigger(event) 30 | keep_context_tenant_unchange do 31 | super 32 | end 33 | end 34 | 35 | def evaluate_script(script, *args) 36 | keep_context_tenant_unchange do 37 | super 38 | end 39 | end 40 | 41 | def evaluate_async_script(script, *args) 42 | keep_context_tenant_unchange do 43 | super 44 | end 45 | end 46 | 47 | def keep_context_tenant_unchange 48 | _current_tenant = MultiTenantSupport::Current.tenant_account 49 | MultiTenantSupport::Current.tenant_account = nil # Simulate real circumstance 50 | yield 51 | ensure 52 | MultiTenantSupport::Current.tenant_account = _current_tenant 53 | end 54 | 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/generators/initializer_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/multi_tenant_support/initializer_generator" 3 | 4 | class InitializerGeneratorTest < Rails::Generators::TestCase 5 | tests MultiTenantSupport::Generators::InitializerGenerator 6 | destination File.expand_path('tmp/initializer', __dir__) 7 | setup :prepare_destination 8 | 9 | test "generator an initializer file" do 10 | assert Dir.children(destination_root).empty? 11 | run_generator 12 | initializer_file_content = File.read("#{destination_root}/config/initializers/multi_tenant_support.rb") 13 | expected_content = <<~INITIALIZER 14 | MultiTenantSupport.configure do 15 | model do |config| 16 | config.tenant_account_class_name = 'REPLACE_ME' 17 | config.tenant_account_primary_key = :id 18 | end 19 | 20 | controller do |config| 21 | config.current_tenant_account_method = :current_tenant_account 22 | end 23 | 24 | app do |config| 25 | config.excluded_subdomains = ['www'] 26 | config.host = 'REPLACE.ME' 27 | end 28 | 29 | console do |config| 30 | config.allow_read_across_tenant_by_default = false 31 | end 32 | end 33 | 34 | # Uncomment if you are using sidekiq without ActiveJob 35 | # require 'multi_tenant_support/sidekiq' 36 | 37 | # Uncomment if you are using ActiveJob 38 | # require 'multi_tenant_support/active_job' 39 | INITIALIZER 40 | 41 | assert_equal expected_content, initializer_file_content 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/multi_tenant_support/config/model_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MultiTenantSupport::Config::ModelTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | @model_config = MultiTenantSupport::Config::Model.new 7 | end 8 | 9 | test "#tenant_account_class_name and #tenant_account_class_name=" do 10 | assert_raise "tenant_account_class is missing" do 11 | @model_config.tenant_account_class_name 12 | end 13 | 14 | @model_config.tenant_account_class_name = 'Account' 15 | assert_equal 'Account', @model_config.tenant_account_class_name 16 | end 17 | 18 | test "#tenant_account_primary_key and #tenant_account_primary_key=" do 19 | assert_equal :id, @model_config.tenant_account_primary_key 20 | 21 | @model_config.tenant_account_primary_key = :uuid 22 | assert_equal :uuid, @model_config.tenant_account_primary_key 23 | end 24 | 25 | test "#default_foreign_key and #default_foreign_key=" do 26 | @model_config.tenant_account_class_name = 'Account' 27 | assert_equal :account_id, @model_config.default_foreign_key 28 | 29 | @model_config.default_foreign_key = :tenant_id 30 | assert_equal :tenant_id, @model_config.default_foreign_key 31 | 32 | @model_config.default_foreign_key = :tenant_account_id 33 | assert_equal :tenant_account_id, @model_config.default_foreign_key 34 | end 35 | 36 | test "#tenanted_models and #tenanted_models=" do 37 | assert_equal [], @model_config.tenanted_models 38 | 39 | @model_config.tenanted_models << 'User' 40 | assert_equal ['User'], @model_config.tenanted_models 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/integration/active_job/sidekiq_adapter/perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module SidekiqAdapter 5 | class PeroformNowTest < ActiveSupport::TestCase 6 | 7 | test 'update succes update user when tenant account match' do 8 | under_tenant(amazon) do 9 | assert_no_changes 'MultiTenantSupport.current_tenant' do 10 | UserNameUpdateJob.perform_now(bezos) 11 | end 12 | 13 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 14 | end 15 | end 16 | 17 | test 'fail to update user when tenant account is missing' do 18 | without_current_tenant do 19 | assert_no_changes 'MultiTenantSupport.current_tenant' do 20 | assert_raise(MultiTenantSupport::MissingTenantError) do 21 | UserNameUpdateJob.perform_now(bezos) 22 | end 23 | end 24 | end 25 | 26 | under_tenant(amazon) do 27 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 28 | end 29 | end 30 | 31 | test 'fail to update user when tenant account is not match' do 32 | under_tenant(apple) do 33 | assert_no_changes 'MultiTenantSupport.current_tenant' do 34 | assert_raise(MultiTenantSupport::InvalidTenantAccess) do 35 | UserNameUpdateJob.perform_now(bezos) 36 | end 37 | end 38 | end 39 | 40 | under_tenant(amazon) do 41 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 42 | end 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_all_on_global_records_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteAllOnGlobalRecordsProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .delete_all on global records like account, tag, country ... 8 | #### 9 | test "can delete_all global records by a tenant" do 10 | within_a_request_of amazon do 11 | assert_delete_all_on_global_records(-3) 12 | end 13 | end 14 | 15 | test "can delete_all global records when the tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | assert_delete_all_on_global_records(-3) 19 | end 20 | end 21 | end 22 | 23 | test 'can delete_all global records by super admin' do 24 | within_a_request_of super_admin do 25 | assert_delete_all_on_global_records(-3) 26 | end 27 | end 28 | 29 | test 'can delete_all global records by super admin event manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_delete_all_on_global_records(-3) 33 | end 34 | end 35 | end 36 | 37 | test 'can delete_all global records by super admin event manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_delete_all_on_global_records(-3) 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def assert_delete_all_on_global_records(number_change) 48 | assert_difference "Account.count", number_change do 49 | Account.delete_all 50 | end 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /test/integration/current_tenant_protect_during_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CurrentTenantProtectDuringIntegrationTeste < ActionDispatch::IntegrationTest 4 | 5 | setup do 6 | host! "amazon.example.com" 7 | MultiTenantSupport::Current.tenant_account = accounts(:amazon) 8 | end 9 | 10 | test "curren tenant won't reset by get" do 11 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 12 | get users_path 13 | assert_response :success 14 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 15 | end 16 | 17 | test "curren tenant won't reset by post" do 18 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 19 | post users_path 20 | assert_redirected_to users_path 21 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 22 | end 23 | 24 | test "curren tenant won't reset by patch" do 25 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 26 | patch user_path(id: 1) 27 | assert_redirected_to users_path 28 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 29 | end 30 | 31 | test "curren tenant won't reset by put" do 32 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 33 | put user_path(id: 1) 34 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 35 | end 36 | 37 | test "curren tenant won't reset by delete" do 38 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 39 | delete user_path(id: 1) 40 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | ENV['MIGRATION_VESRION'] ||= '6.1' 4 | require "minitest/autorun" 5 | require "minitest/focus" 6 | 7 | require_relative "../test/dummy/config/environment" 8 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 9 | require "rails/test_help" 10 | 11 | ActiveRecord::Migration.maintain_test_schema! 12 | 13 | require "rails/test_unit/reporter" 14 | Rails::TestUnitReporter.executable = 'bin/test' 15 | 16 | # Load fixtures from the engine 17 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 18 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) 19 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path 20 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" 21 | ActiveSupport::TestCase.fixtures :all 22 | end 23 | 24 | require 'support/dsl' 25 | require 'support/asserts' 26 | 27 | class ActiveSupport::TestCase 28 | include MultiTenantSupport::DSL 29 | include MultiTenantSupport::Asserts 30 | 31 | self.use_transactional_tests = false 32 | 33 | setup { setup_users } 34 | end 35 | 36 | require 'support/sidekiq_jobs_manager' 37 | require 'multi_tenant_support/active_job' 38 | SidekiqJobsManager.instance.start_workers 39 | 40 | Minitest.after_run do 41 | SidekiqJobsManager.instance.stop_workers 42 | SidekiqJobsManager.instance.clear_jobs 43 | 44 | ActiveRecord::Tasks::DatabaseTasks.truncate_all( 45 | ActiveSupport::StringInquirer.new("test") 46 | ) 47 | end 48 | 49 | require 'multi_tenant_support/minitest' -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/default_scope_setup_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_DefaultScopeSetupTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | MultiTenantSupport.turn_on_full_protection 7 | end 8 | 9 | test 'set default scope to under current tenant' do 10 | MultiTenantSupport.under_tenant accounts(:amazon) do 11 | assert_equal 1, User.all.to_a.count 12 | assert_equal 1, User.count 13 | assert_equal users(:bezos), User.first 14 | assert_equal users(:bezos), User.last 15 | assert_equal users(:bezos), User.find_by(name: 'Jeff Bezos') 16 | assert_equal users(:bezos), User.where(name: 'Jeff Bezos').first 17 | kate = User.new(name: 'kate', email: 'kate@example.com') 18 | assert kate.save 19 | assert_equal kate, User.where(name: 'kate').first 20 | end 21 | end 22 | 23 | test 'raise error when tenant is missing' do 24 | assert_raise(MultiTenantSupport::MissingTenantError) { User.all } 25 | assert_raise(MultiTenantSupport::MissingTenantError) { User.count } 26 | assert_raise(MultiTenantSupport::MissingTenantError) { User.first } 27 | assert_raise(MultiTenantSupport::MissingTenantError) { User.last } 28 | assert_raise(MultiTenantSupport::MissingTenantError) { User.new } 29 | assert_raise(MultiTenantSupport::MissingTenantError) { User.create(email: 'test@test.com') } 30 | assert_raise(MultiTenantSupport::MissingTenantError) { User.where(name: 'bezos') } 31 | assert_raise(MultiTenantSupport::MissingTenantError) { User.find_by(name: 'bezos') } 32 | end 33 | 34 | end -------------------------------------------------------------------------------- /test/integration/current_tenant_protect_during_integration_test_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | RSpec.describe "Current tenant protect during integration test", type: :request do 4 | 5 | before do 6 | host! "amazon.example.com" 7 | MultiTenantSupport::Current.tenant_account = accounts(:amazon) 8 | end 9 | 10 | it "get won't reset current tenant" do 11 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 12 | get users_path 13 | expect(response.status).to eq(200) 14 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 15 | end 16 | 17 | it "post won't reset current tenant" do 18 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 19 | post users_path 20 | assert_redirected_to users_path 21 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 22 | end 23 | 24 | it "patch won't reset current tenant" do 25 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 26 | patch user_path(id: 1) 27 | assert_redirected_to users_path 28 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 29 | end 30 | 31 | it "put won't reset current tenant" do 32 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 33 | put user_path(id: 1) 34 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 35 | end 36 | 37 | it "delete won't reset current tenant" do 38 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 39 | delete user_path(id: 1) 40 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_all_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteAllProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .delete_all 8 | #### 9 | test "can only delete records under the tenant" do 10 | within_a_request_of amazon do 11 | assert_delete_all(-1) 12 | end 13 | end 14 | 15 | test "fail to delete when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_delete_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot delete by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_delete_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can delete scoped records by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_delete_all(-1) 33 | end 34 | end 35 | end 36 | 37 | test 'can delete scoped records by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_delete_all(-3) 41 | end 42 | end 43 | end 44 | 45 | def assert_delete_all(number_change) 46 | assert_difference "User.unscope_tenant.count", number_change do 47 | User.delete_all 48 | end 49 | end 50 | 51 | def refute_delete_all(error = nil) 52 | assert_raise(error) { User.delete_all } 53 | 54 | as_super_admin do 55 | assert_equal 3, User.count 56 | end 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/counter_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_CounterTest < ActiveSupport::TestCase 4 | 5 | test ".count - won't raise error on missing tenant when allow read across tenant" do 6 | MultiTenantSupport.allow_read_across_tenant do 7 | assert_equal 3, User.count 8 | end 9 | end 10 | 11 | test ".count - won't raise error on missing tenant when turn off protection" do 12 | MultiTenantSupport.turn_off_protection do 13 | assert_equal 3, User.count 14 | end 15 | end 16 | 17 | test ".count - raise error on missing tenant when default scope is on" do 18 | MultiTenantSupport.turn_on_full_protection do 19 | assert_raise(MultiTenantSupport::MissingTenantError) { User.count } 20 | end 21 | end 22 | 23 | test ".count - won't count steve and zuck under amazon" do 24 | MultiTenantSupport.under_tenant accounts(:amazon) do 25 | assert_equal 1, User.count 26 | end 27 | end 28 | 29 | test ".count - won't count steve and zuck under amazon when wrap in allow_read_across_tenant " do 30 | MultiTenantSupport.allow_read_across_tenant do 31 | MultiTenantSupport.under_tenant accounts(:amazon) do 32 | assert_equal 1, User.count 33 | end 34 | end 35 | end 36 | 37 | test ".count - won't count steve and zuck under amazon when call allow_read_across_tenant withint it" do 38 | MultiTenantSupport.allow_read_across_tenant do 39 | MultiTenantSupport.under_tenant accounts(:amazon) do 40 | assert_equal 1, User.count 41 | end 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/destroy_all_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDestroyAllProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .destroy_all 8 | #### 9 | test "can only destroy records under the tenant" do 10 | within_a_request_of amazon do 11 | assert_destroy_all(-1) 12 | end 13 | end 14 | 15 | test "fail to destroy when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_destroy_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot destroy by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_destroy_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can destroy scoped records by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_destroy_all(-1) 33 | end 34 | end 35 | end 36 | 37 | test 'can destroy scoped records by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_destroy_all(-3) 41 | end 42 | end 43 | end 44 | 45 | def assert_destroy_all(number_change) 46 | assert_difference "User.unscope_tenant.count", number_change do 47 | User.destroy_all 48 | end 49 | end 50 | 51 | def refute_destroy_all(error) 52 | assert_raise(error) { User.destroy_all } 53 | 54 | as_super_admin do 55 | assert_equal 3, User.count 56 | end 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /test/integration/active_job/sidekiq_adapter/preconfigured_perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module SidekiqAdapter 5 | class PreconfiguredPeroformNowTest < ActiveSupport::TestCase 6 | 7 | test 'update succes update user when tenant account match' do 8 | under_tenant(amazon) do 9 | assert_no_changes 'MultiTenantSupport.current_tenant' do 10 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 11 | end 12 | 13 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 14 | end 15 | end 16 | 17 | test 'fail to update user when tenant account is missing' do 18 | without_current_tenant do 19 | assert_no_changes 'MultiTenantSupport.current_tenant' do 20 | assert_raise(MultiTenantSupport::MissingTenantError) do 21 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 22 | end 23 | end 24 | end 25 | 26 | under_tenant(amazon) do 27 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 28 | end 29 | end 30 | 31 | test 'fail to update user when tenant account is not match' do 32 | under_tenant(apple) do 33 | assert_no_changes 'MultiTenantSupport.current_tenant' do 34 | assert_raise(MultiTenantSupport::InvalidTenantAccess) do 35 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 36 | end 37 | end 38 | end 39 | 40 | under_tenant(amazon) do 41 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 42 | end 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/update_all_on_global_records_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateAllOnGlobalRecordsProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #update_all on global records 8 | #### 9 | test "can update_all global records by tenant" do 10 | within_a_request_of amazon do 11 | assert_update_all_on_global_records affect: 2 12 | end 13 | end 14 | 15 | test "can update_all global records even the tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | assert_update_all_on_global_records affect: 2 19 | end 20 | end 21 | end 22 | 23 | test 'can update_all global records by super admin' do 24 | within_a_request_of super_admin do 25 | assert_update_all_on_global_records affect: 2 26 | end 27 | end 28 | 29 | test 'can update_all global records by super admin even manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_update_all_on_global_records affect: 2 33 | end 34 | end 35 | end 36 | 37 | test 'can update_all global records by super admin even manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_update_all_on_global_records affect: 2 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def assert_update_all_on_global_records(affect:) 48 | Tag.update_all(name: 'NEW TAG NAME') 49 | 50 | as_super_admin do 51 | without_current_tenant do 52 | assert_equal affect, Tag.where(name: 'NEW TAG NAME').count 53 | end 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/new_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_NewTest < ActiveSupport::TestCase 4 | 5 | test ".new - auto set tenant account on new" do 6 | MultiTenantSupport.under_tenant accounts(:amazon) do 7 | kate = User.new(name: 'kate') 8 | assert_equal accounts(:amazon), kate.account 9 | end 10 | end 11 | 12 | test ".new - raise error on missing tenant missing when not allow read across tenant (default)" do 13 | assert_raise(MultiTenantSupport::MissingTenantError) do 14 | User.new 15 | end 16 | end 17 | 18 | test ".new - raise error on missing tenant when allow read across tenant" do 19 | MultiTenantSupport.allow_read_across_tenant do 20 | assert_raise(MultiTenantSupport::MissingTenantError) do 21 | User.new 22 | end 23 | 24 | assert_raise(MultiTenantSupport::MissingTenantError) do 25 | accounts(:amazon).users.build 26 | end 27 | end 28 | end 29 | 30 | test ".new - auto set tenant account on new when super admin manual set current tenant" do 31 | allow_read_across_tenant do 32 | under_tenant amazon do 33 | kate = User.new(name: 'kate') 34 | assert_equal amazon, kate.account 35 | 36 | john = tags(:engineer).users.build 37 | assert_equal amazon, john.account 38 | end 39 | end 40 | end 41 | 42 | test ".new - success without auto set tenant account on new when turn off protection" do 43 | turn_off_protection do 44 | kate = User.new(name: 'kate') 45 | assert kate.account.nil? 46 | 47 | john = tags(:engineer).users.build 48 | assert john.account.nil? 49 | end 50 | end 51 | 52 | end -------------------------------------------------------------------------------- /test/system/current_tenant_protect_during_system_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CurrentTenantProtectDuringSystemTeste < ActionDispatch::SystemTestCase 4 | driven_by :rack_test 5 | 6 | setup do 7 | Capybara.app_host = "http://amazon.example.com" 8 | MultiTenantSupport::Current.tenant_account = accounts(:amazon) 9 | end 10 | 11 | test "curren tenant won't reset by visit" do 12 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 13 | 14 | assert_no_changes 'MultiTenantSupport.current_tenant' do 15 | visit users_path 16 | end 17 | 18 | assert_content "Users#index" 19 | assert_selector "#id", text: amazon.id.to_s 20 | assert_selector "#name", text: "Amazon" 21 | assert_selector "#domain", text: "amazon.com" 22 | assert_selector "#subdomain", text: "amazon" 23 | end 24 | 25 | test "curren tenant won't reset by refresh" do 26 | visit users_path 27 | 28 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 29 | assert_no_changes 'MultiTenantSupport.current_tenant' do 30 | refresh 31 | end 32 | end 33 | 34 | test "curren tenant won't reset by click_on" do 35 | visit users_path 36 | 37 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 38 | assert_no_changes 'MultiTenantSupport.current_tenant' do 39 | click_on "Show" 40 | assert_content "User - Show" 41 | end 42 | end 43 | 44 | test "curren tenant won't reset by click" do 45 | visit users_path 46 | 47 | assert_equal accounts(:amazon), MultiTenantSupport.current_tenant 48 | assert_no_changes 'MultiTenantSupport.current_tenant' do 49 | find_link("Show").click 50 | assert_content "User - Show" 51 | end 52 | end 53 | 54 | end -------------------------------------------------------------------------------- /test/integration/active_job/async_adapter/perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module AsycnAdapter 5 | class PerformNowTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :async 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | UserNameUpdateJob.perform_now(bezos) 18 | end 19 | 20 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 21 | end 22 | end 23 | 24 | test 'fail to update user when tenant account is missing' do 25 | without_current_tenant do 26 | assert_no_changes 'MultiTenantSupport.current_tenant' do 27 | assert_raise MultiTenantSupport::MissingTenantError do 28 | UserNameUpdateJob.perform_now(bezos) 29 | end 30 | end 31 | end 32 | 33 | under_tenant(amazon) do 34 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 35 | end 36 | end 37 | 38 | test 'fail to update user when tenant account is not match' do 39 | under_tenant(apple) do 40 | assert_no_changes 'MultiTenantSupport.current_tenant' do 41 | assert_raise MultiTenantSupport::InvalidTenantAccess do 42 | UserNameUpdateJob.perform_now(bezos) 43 | end 44 | end 45 | end 46 | 47 | under_tenant(amazon) do 48 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 49 | end 50 | end 51 | 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /test/integration/active_job/test_adapter/perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module TestAdapter 5 | class PerformNowTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :test 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | UserNameUpdateJob.perform_now(bezos) 18 | end 19 | 20 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 21 | end 22 | end 23 | 24 | test 'fail to update user when tenant account is missing' do 25 | without_current_tenant do 26 | assert_no_changes 'MultiTenantSupport.current_tenant' do 27 | assert_raise MultiTenantSupport::MissingTenantError do 28 | UserNameUpdateJob.perform_now(bezos) 29 | end 30 | end 31 | end 32 | 33 | under_tenant(amazon) do 34 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 35 | end 36 | end 37 | 38 | test 'fail to update user when tenant account is not match' do 39 | under_tenant(apple) do 40 | assert_no_changes 'MultiTenantSupport.current_tenant' do 41 | assert_raise MultiTenantSupport::InvalidTenantAccess do 42 | UserNameUpdateJob.perform_now(bezos) 43 | end 44 | end 45 | end 46 | 47 | under_tenant(amazon) do 48 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 49 | end 50 | end 51 | 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/update_all_on_batch_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateAllOnBatchProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #update_all 8 | #### 9 | test "can update_all by tenant" do 10 | within_a_request_of amazon do 11 | assert_update_all affect: 1 12 | end 13 | end 14 | 15 | test "cannot update_all when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_update_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot update_all by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_update_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can update_all by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_update_all affect: 1 33 | end 34 | end 35 | end 36 | 37 | test 'can update_all by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_update_all affect: 3 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def assert_update_all(affect:) 48 | User.in_batches.update_all(name: 'NAME') 49 | 50 | as_super_admin do 51 | assert_equal 3, User.count 52 | assert_equal affect, User.where(name: 'NAME').count 53 | end 54 | end 55 | 56 | def refute_update_all(error) 57 | assert_raise(error) { User.in_batches.update_all(name: 'NAME') } 58 | 59 | as_super_admin do 60 | refute User.pluck(:name).include?('NAME') 61 | end 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /test/integration/active_job/inline_adapter/perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module InlineAdapter 5 | class PerformNowTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :inline 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | UserNameUpdateJob.perform_now(bezos) 18 | end 19 | 20 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 21 | end 22 | end 23 | 24 | test 'fail to update user when tenant account is missing' do 25 | without_current_tenant do 26 | assert_no_changes 'MultiTenantSupport.current_tenant' do 27 | assert_raise MultiTenantSupport::MissingTenantError do 28 | UserNameUpdateJob.perform_now(bezos) 29 | end 30 | end 31 | end 32 | 33 | under_tenant(amazon) do 34 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 35 | end 36 | end 37 | 38 | test 'fail to update user when tenant account is not match' do 39 | under_tenant(apple) do 40 | assert_no_changes 'MultiTenantSupport.current_tenant' do 41 | assert_raise MultiTenantSupport::InvalidTenantAccess do 42 | UserNameUpdateJob.perform_now(bezos) 43 | end 44 | end 45 | end 46 | 47 | under_tenant(amazon) do 48 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 49 | end 50 | end 51 | 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #delete 8 | #### 9 | test "can delete by tenant" do 10 | within_a_request_of amazon do 11 | assert_delete bezos 12 | end 13 | end 14 | 15 | test "cannot delete when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_delete bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot delete by other tenant" do 24 | within_a_request_of apple do 25 | refute_delete bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot delete by super admin default' do 30 | within_a_request_of super_admin do 31 | refute_delete bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can delete by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_delete bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can delete by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_delete bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_delete(user) 54 | assert_difference "User.unscope_tenant.count", -1 do 55 | user.delete 56 | end 57 | end 58 | 59 | def refute_delete(user, error:) 60 | assert_raise(error) { user.delete } 61 | 62 | under_tenant user.account do 63 | assert user.reload.persisted? 64 | end 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_all_on_batch_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteAllOnBatchProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .delete_all 8 | #### 9 | test "can only delete records under the tenant" do 10 | within_a_request_of amazon do 11 | assert_delete_all(-1) 12 | end 13 | end 14 | 15 | test "fail to delete when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_delete_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot delete by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_delete_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can delete scoped records by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_delete_all(-1) 33 | end 34 | end 35 | end 36 | 37 | test 'can delete all records by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_delete_all(-3) 41 | end 42 | end 43 | end 44 | 45 | def assert_delete_all(number_change) 46 | assert_difference "User.unscope_tenant.count", number_change do 47 | User.in_batches.delete_all 48 | end 49 | 50 | as_super_admin do 51 | assert_equal (3 + number_change), User.unscope_tenant.count 52 | end 53 | end 54 | 55 | def refute_delete_all(error = nil) 56 | assert_raise(error) { User.in_batches.delete_all } 57 | 58 | as_super_admin do 59 | assert_equal 3, User.count 60 | end 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/destroy_on_class_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ModelDestroyOnClassProtectTest < ActiveSupport::TestCase 4 | 5 | #### 6 | # .destroy 7 | #### 8 | test "can destroy by tenant" do 9 | within_a_request_of amazon do 10 | assert_destroy bezos 11 | end 12 | end 13 | 14 | test "cannot destroy when tenant is missing" do 15 | turn_on_full_protection do 16 | missing_tenant do 17 | refute_destroy bezos, error: MultiTenantSupport::MissingTenantError 18 | end 19 | end 20 | end 21 | 22 | test "cannot destroy by other tenant" do 23 | within_a_request_of apple do 24 | refute_destroy bezos, error: ActiveRecord::RecordNotFound 25 | end 26 | end 27 | 28 | test 'cannot destroy by super admin default' do 29 | within_a_request_of super_admin do 30 | refute_destroy bezos, error: MultiTenantSupport::MissingTenantError 31 | end 32 | end 33 | 34 | test 'can destroy by super admin through manual set current tenant' do 35 | within_a_request_of super_admin do 36 | under_tenant amazon do 37 | assert_destroy bezos 38 | end 39 | end 40 | end 41 | 42 | test 'can destroy by super admin through manual turn off protection' do 43 | within_a_request_of super_admin do 44 | turn_off_protection do 45 | assert_destroy bezos 46 | end 47 | end 48 | end 49 | 50 | private 51 | 52 | def assert_destroy(user) 53 | assert_difference "User.count", -1 do 54 | User.destroy(user.id) 55 | end 56 | end 57 | 58 | def refute_destroy(user, error:) 59 | assert_raise(error) { User.destroy(user.id) } 60 | 61 | under_tenant user.account do 62 | assert user.reload.persisted? 63 | end 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/destroy_all_on_batch_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDestroyAllOnBatchProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .destroy_all 8 | #### 9 | test "can only destroy records under the tenant" do 10 | within_a_request_of amazon do 11 | assert_destroy_all(-1) 12 | end 13 | end 14 | 15 | test "fail to destroy when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_destroy_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot destroy by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_destroy_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can destroy scoped records by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_destroy_all(-1) 33 | end 34 | end 35 | end 36 | 37 | test 'can destroy scoped records by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_destroy_all(-3) 41 | end 42 | end 43 | end 44 | 45 | def assert_destroy_all(number_change) 46 | assert_difference "User.unscope_tenant.count", number_change do 47 | User.in_batches.destroy_all 48 | end 49 | 50 | as_super_admin do 51 | assert_equal (3 + number_change), User.unscope_tenant.count 52 | end 53 | end 54 | 55 | def refute_destroy_all(error = nil) 56 | assert_raise(error) { User.in_batches.destroy_all } 57 | 58 | as_super_admin do 59 | assert_equal 3, User.count 60 | end 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/update_all_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateAllProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #update_all 8 | #### 9 | test "can update_all by tenant" do 10 | within_a_request_of amazon do 11 | assert_update_all affect: 1 12 | end 13 | end 14 | 15 | test "cannot update_all when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_update_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot update_all by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_update_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can update_all by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_update_all affect: 1 33 | end 34 | end 35 | end 36 | 37 | test 'can update_all by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_update_all affect: 3 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def assert_update_all(affect:) 48 | User.update_all(name: 'NAME') 49 | 50 | as_super_admin do 51 | without_current_tenant do 52 | assert_equal 3, User.count 53 | assert_equal affect, User.where(name: 'NAME').count 54 | end 55 | end 56 | end 57 | 58 | def refute_update_all(error) 59 | assert_raise(error) { User.update_all(name: 'NAME') } 60 | 61 | as_super_admin do 62 | without_current_tenant do 63 | refute User.pluck(:name).include?('NAME') 64 | end 65 | end 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_on_collection_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteOnCollectionProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #delete 8 | #### 9 | test "can delete by tenant" do 10 | within_a_request_of amazon do 11 | assert_delete bezos 12 | end 13 | end 14 | 15 | test "cannot delete when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_delete bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot delete by other tenant" do 24 | within_a_request_of apple do 25 | refute_delete bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot delete by super admin default' do 30 | within_a_request_of super_admin do 31 | refute_delete bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can delete by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_delete bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can delete by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_delete bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_delete(user) 54 | assert_difference "User.unscope_tenant.count", -1 do 55 | countries(:us).users.delete(user) 56 | end 57 | end 58 | 59 | def refute_delete(user, error:) 60 | assert_raise(error) { countries(:us).users.delete(user) } 61 | 62 | under_tenant user.account do 63 | assert user.reload.persisted? 64 | end 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/destroy_on_collection_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ModelDestroyOnCollectionProtectTest < ActiveSupport::TestCase 4 | 5 | #### 6 | # #destroy 7 | #### 8 | test "can destroy by tenant" do 9 | within_a_request_of amazon do 10 | assert_destroy bezos 11 | end 12 | end 13 | 14 | test "cannot destroy when tenant is missing" do 15 | turn_on_full_protection do 16 | missing_tenant do 17 | refute_destroy bezos, error: MultiTenantSupport::MissingTenantError 18 | end 19 | end 20 | end 21 | 22 | test "cannot destroy by other tenant" do 23 | within_a_request_of apple do 24 | refute_destroy bezos, error: MultiTenantSupport::InvalidTenantAccess 25 | end 26 | end 27 | 28 | test 'cannot destroy by super admin default' do 29 | within_a_request_of super_admin do 30 | refute_destroy bezos, error: MultiTenantSupport::MissingTenantError 31 | end 32 | end 33 | 34 | test 'can destroy by super admin through manual set current tenant' do 35 | within_a_request_of super_admin do 36 | under_tenant amazon do 37 | assert_destroy bezos 38 | end 39 | end 40 | end 41 | 42 | test 'can destroy by super admin through manual turn off protection' do 43 | within_a_request_of super_admin do 44 | turn_off_protection do 45 | assert_destroy bezos 46 | end 47 | end 48 | end 49 | 50 | private 51 | 52 | def assert_destroy(user) 53 | assert_difference "User.unscope_tenant.count", -1 do 54 | countries(:us).users.destroy(user) 55 | end 56 | end 57 | 58 | def refute_destroy(user, error:) 59 | assert_raise(error) { countries(:us).users.destroy(user) } 60 | 61 | under_tenant user.account do 62 | assert user.reload.persisted? 63 | end 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/sidekiq.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /test/system/current_tenant_protect_during_system_test_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | RSpec.describe "current tenant protect during system test", type: :system do 4 | 5 | before do 6 | driven_by :rack_test 7 | Capybara.app_host = "http://amazon.example.com" 8 | MultiTenantSupport::Current.tenant_account = accounts(:amazon) 9 | end 10 | 11 | it "visit won't reset current tenant" do 12 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 13 | 14 | expect{ 15 | visit users_path 16 | }.not_to change{ MultiTenantSupport.current_tenant } 17 | 18 | expect(page).to have_content("Users#index") 19 | expect(page).to have_selector("#id", text: accounts(:amazon).id.to_s) 20 | expect(page).to have_selector("#name", text: "Amazon") 21 | expect(page).to have_selector("#domain", text: "amazon.com") 22 | expect(page).to have_selector("#subdomain", text: "amazon") 23 | end 24 | 25 | it "refresh won't reset current tenant" do 26 | visit users_path 27 | 28 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 29 | 30 | expect{ 31 | refresh 32 | }.not_to change{ MultiTenantSupport.current_tenant } 33 | end 34 | 35 | it "click_on won't reset current tenant" do 36 | visit users_path 37 | 38 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 39 | 40 | expect{ 41 | click_on "Show" 42 | }.not_to change{ MultiTenantSupport.current_tenant } 43 | 44 | expect(page).to have_content("User - Show") 45 | end 46 | 47 | it "Element#click won't reset current tenant" do 48 | visit users_path 49 | 50 | expect(MultiTenantSupport.current_tenant).to eq(accounts(:amazon)) 51 | 52 | expect{ 53 | find_link("Show").click 54 | }.not_to change{ MultiTenantSupport.current_tenant } 55 | 56 | expect(page).to have_content("User - Show") 57 | end 58 | 59 | end -------------------------------------------------------------------------------- /test/integration/active_job/async_adapter/preconfigured_perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module AsycnAdapter 5 | class PreconfiguredPerformNowTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :async 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 18 | end 19 | 20 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 21 | end 22 | end 23 | 24 | test 'fail to update user when tenant account is missing' do 25 | without_current_tenant do 26 | assert_no_changes 'MultiTenantSupport.current_tenant' do 27 | assert_raise MultiTenantSupport::MissingTenantError do 28 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 29 | end 30 | end 31 | end 32 | 33 | under_tenant(amazon) do 34 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 35 | end 36 | end 37 | 38 | test 'fail to update user when tenant account is not match' do 39 | under_tenant(apple) do 40 | assert_no_changes 'MultiTenantSupport.current_tenant' do 41 | assert_raise MultiTenantSupport::InvalidTenantAccess do 42 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 43 | end 44 | end 45 | end 46 | 47 | under_tenant(amazon) do 48 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 49 | end 50 | end 51 | 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /test/integration/active_job/test_adapter/preconfigured_perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module TestAdapter 5 | class PreconfiguredPerformNowTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :test 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 18 | end 19 | 20 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 21 | end 22 | end 23 | 24 | test 'fail to update user when tenant account is missing' do 25 | without_current_tenant do 26 | assert_no_changes 'MultiTenantSupport.current_tenant' do 27 | assert_raise MultiTenantSupport::MissingTenantError do 28 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 29 | end 30 | end 31 | end 32 | 33 | under_tenant(amazon) do 34 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 35 | end 36 | end 37 | 38 | test 'fail to update user when tenant account is not match' do 39 | under_tenant(apple) do 40 | assert_no_changes 'MultiTenantSupport.current_tenant' do 41 | assert_raise MultiTenantSupport::InvalidTenantAccess do 42 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 43 | end 44 | end 45 | end 46 | 47 | under_tenant(amazon) do 48 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 49 | end 50 | end 51 | 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /test/integration/active_job/inline_adapter/preconfigured_perform_now_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module InlineAdapter 5 | class PreconfiguredPerformNowTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :inline 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 18 | end 19 | 20 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 21 | end 22 | end 23 | 24 | test 'fail to update user when tenant account is missing' do 25 | without_current_tenant do 26 | assert_no_changes 'MultiTenantSupport.current_tenant' do 27 | assert_raise MultiTenantSupport::MissingTenantError do 28 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 29 | end 30 | end 31 | end 32 | 33 | under_tenant(amazon) do 34 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 35 | end 36 | end 37 | 38 | test 'fail to update user when tenant account is not match' do 39 | under_tenant(apple) do 40 | assert_no_changes 'MultiTenantSupport.current_tenant' do 41 | assert_raise MultiTenantSupport::InvalidTenantAccess do 42 | UserNameUpdateJob.set(queue: :integration_tests).perform_now(bezos) 43 | end 44 | end 45 | end 46 | 47 | under_tenant(amazon) do 48 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 49 | end 50 | end 51 | 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/save_without_validate_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateBySaveWithoutValidateProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #save(validate: false) 8 | #### 9 | test "can save by tenant" do 10 | within_a_request_of amazon do 11 | assert_save bezos 12 | end 13 | end 14 | 15 | test "cannot save when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_save bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot save by other tenant" do 24 | within_a_request_of apple do 25 | refute_save bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot save by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_save bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can save by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_save bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can save by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_save bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_save(bezos) 54 | bezos.name = "JEFF BEZOS" 55 | assert bezos.save(validate: false) 56 | assert_equal "JEFF BEZOS", bezos.name 57 | end 58 | 59 | def refute_save(bezos, error:) 60 | bezos.name = "JEFF BEZOS" 61 | 62 | assert_raise(error) { bezos.save(validate: false) } 63 | 64 | under_tenant amazon do 65 | assert_equal "Jeff Bezos", bezos.reload.name 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_all_on_collection_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteAllOnCollectionProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .delete_all 8 | #### 9 | test "can only delete records under the tenant" do 10 | within_a_request_of amazon do 11 | assert_delete_all(-1) 12 | end 13 | end 14 | 15 | test "fail to delete when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_delete_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot delete by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_delete_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can delete scoped records by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_delete_all(-1) 33 | end 34 | end 35 | end 36 | 37 | test 'can delete all records by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_delete_all(-3) 41 | end 42 | end 43 | end 44 | 45 | def assert_delete_all(number_change) 46 | assert_difference "UserTag.unscope_tenant.count", number_change do 47 | tags(:entrepreneur).users.delete_all 48 | end 49 | 50 | as_super_admin do 51 | assert_equal (3 + number_change), UserTag.unscope_tenant.count 52 | assert_equal 3, User.unscope_tenant.count 53 | end 54 | end 55 | 56 | def refute_delete_all(error = nil) 57 | assert_raise(error) { tags(:entrepreneur).users.delete_all } 58 | 59 | as_super_admin do 60 | assert_equal 3, User.count 61 | assert_equal 3, UserTag.count 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/save_after_write_attribute_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateBySaveAfterWriteAttributeProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #save after #write_attribute 8 | #### 9 | test "can save by tenant" do 10 | within_a_request_of amazon do 11 | assert_save bezos 12 | end 13 | end 14 | 15 | test "cannot save when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_save bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot save by other tenant" do 24 | within_a_request_of apple do 25 | refute_save bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot save by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_save bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can save by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_save bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can save by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_save bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_save(bezos) 54 | bezos.write_attribute(:name, "JEFF BEZOS") 55 | assert bezos.save 56 | assert_equal "JEFF BEZOS", bezos.name 57 | end 58 | 59 | def refute_save(bezos, error:) 60 | bezos.write_attribute(:name, "JEFF BEZOS") 61 | 62 | assert_raise(error) { bezos.save } 63 | 64 | under_tenant amazon do 65 | assert_equal "Jeff Bezos", bezos.reload.name 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_on_class_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteOnClassProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .delete 8 | #### 9 | test "can delete by tenant" do 10 | within_a_request_of amazon do 11 | assert_delete bezos 12 | end 13 | end 14 | 15 | test "cannot delete when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_delete bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot delete by other tenant" do 24 | within_a_request_of apple do 25 | refute_delete bezos 26 | end 27 | end 28 | 29 | test 'cannot delete by super admin default' do 30 | within_a_request_of super_admin do 31 | refute_delete bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can delete by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_delete bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can delete by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_delete bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_delete(user) 54 | assert_difference "User.unscope_tenant.count", -1 do 55 | User.delete(user.id) 56 | end 57 | end 58 | 59 | def refute_delete(user, error: nil) 60 | if error 61 | assert_raise(error) { User.delete(user.id) } 62 | else 63 | assert_no_difference "User.unscope_tenant.count" do 64 | User.delete(user.id) 65 | end 66 | end 67 | 68 | under_tenant user.account do 69 | assert user.reload.persisted? 70 | end 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/destroy_all_on_collection_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDestroyAllOnCollectionProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .destroy_all 8 | #### 9 | test "can only destroy records under the tenant" do 10 | within_a_request_of amazon do 11 | assert_destroy_all(-1) 12 | end 13 | end 14 | 15 | test "fail to destroy when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_destroy_all MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot destroy by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_destroy_all MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can destroy scoped records by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant amazon do 32 | assert_destroy_all(-1) 33 | end 34 | end 35 | end 36 | 37 | test 'can destroy all records by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_destroy_all(-3) 41 | end 42 | end 43 | end 44 | 45 | def assert_destroy_all(number_change) 46 | assert_difference "UserTag.unscope_tenant.count", number_change do 47 | tags(:entrepreneur).users.destroy_all 48 | end 49 | 50 | as_super_admin do 51 | assert_equal (3 + number_change), UserTag.unscope_tenant.count 52 | assert_equal 3, User.unscope_tenant.count 53 | end 54 | end 55 | 56 | def refute_destroy_all(error = nil) 57 | assert_raise(error) { tags(:entrepreneur).users.destroy_all } 58 | 59 | as_super_admin do 60 | assert_equal 3, User.count 61 | assert_equal 3, UserTag.count 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'CHANGELOG.md' 9 | - 'docker-compose.yml' 10 | - 'Makefile' 11 | - 'MIT-LICENSE' 12 | - 'README.md' 13 | - '.gitignore' 14 | - 'hero.png' 15 | pull_request: 16 | branches: 17 | - "*" 18 | paths-ignore: 19 | - 'CHANGELOG.md' 20 | - 'docker-compose.yml' 21 | - 'Makefile' 22 | - 'MIT-LICENSE' 23 | - 'README.md' 24 | - '.gitignore' 25 | - 'hero.png' 26 | 27 | jobs: 28 | test: 29 | runs-on: ubuntu-latest 30 | name: ruby-${{ matrix.ruby }} ${{ matrix.rails }} 31 | strategy: 32 | matrix: 33 | ruby: ['2.6', '2.7', '3.0', '3.1'] 34 | rails: ['6.1', '7.0'] 35 | exclude: 36 | - ruby: '2.6' 37 | rails: '7.0' 38 | 39 | env: 40 | MIGRATION_VESRION: ${{ matrix.rails }} 41 | RAILS_VERSION: ~> ${{ matrix.rails }}.0 42 | BUNDLE_PATH_RELATIVE_TO_CWD: true 43 | DATABASE_USERNAME: postgres 44 | REDIS_HOST: '0.0.0.0' 45 | 46 | steps: 47 | - 48 | uses: actions/checkout@v2 49 | - 50 | name: Set up Ruby 51 | uses: ruby/setup-ruby@v1 52 | with: 53 | ruby-version: ${{ matrix.ruby }} 54 | bundler-cache: true 55 | - 56 | name: Bundle install 57 | run: | 58 | bundle install 59 | - 60 | name: Setup Redis 61 | run: | 62 | docker-compose -f docker-compose-github-action.yml up -d redis 63 | - 64 | name: Setup PostgreSQL 65 | run: | 66 | docker-compose -f docker-compose-github-action.yml up -d postgres 67 | - 68 | name: Run DB migration 69 | run: | 70 | cd test/dummy 71 | bundle exec rails db:create db:migrate 72 | - 73 | name: Run minitest 74 | run: | 75 | bundle exec rspec test 76 | bundle exec rake 77 | -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/update_column_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateColumnProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #update_column 8 | #### 9 | test "can update_column by tenant" do 10 | within_a_request_of amazon do 11 | assert_update_column bezos 12 | end 13 | end 14 | 15 | test "cannot update_column when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_update_column bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot update_column by other tenant" do 24 | within_a_request_of apple do 25 | refute_update_column bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot update_column by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_update_column bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can update_column by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_update_column bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can update_column by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_update_column bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_update_column(bezos) 54 | assert bezos.update_column(:name, 'JEFF BEZOS') 55 | 56 | under_tenant amazon do 57 | assert_equal "JEFF BEZOS", bezos.reload.name 58 | end 59 | end 60 | 61 | def refute_update_column(bezos, error:) 62 | assert_raise(error) { bezos.update_column(:name, 'JEFF BEZOS') } 63 | 64 | under_tenant amazon do 65 | assert_equal "Jeff Bezos", bezos.reload.name 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/update_columns_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateColumnsProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #update_columns 8 | #### 9 | test "can update_columns by tenant" do 10 | within_a_request_of amazon do 11 | assert_update_columns bezos 12 | end 13 | end 14 | 15 | test "cannot update_columns when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_update_columns bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot update_columns by other tenant" do 24 | within_a_request_of apple do 25 | refute_update_columns bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot update_columns by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_update_columns bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can update_columns by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_update_columns bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can update_columns by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_update_columns bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_update_columns(bezos) 54 | assert bezos.update_columns(name: 'JEFF BEZOS') 55 | 56 | under_tenant amazon do 57 | assert_equal "JEFF BEZOS", bezos.reload.name 58 | end 59 | end 60 | 61 | def refute_update_columns(bezos, error:) 62 | assert_raise(error) { bezos.update_columns(name: 'JEFF BEZOS') } 63 | 64 | under_tenant amazon do 65 | assert_equal "Jeff Bezos", bezos.reload.name 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/delete_by_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDeleteByProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .delete_by 8 | #### 9 | test "can delete by tenant" do 10 | within_a_request_of amazon do 11 | assert_delete_by name: 'Jeff Bezos' 12 | end 13 | end 14 | 15 | test "cannot delete when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_delete_by name: 'Jeff Bezos', error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot delete by other tenant" do 24 | within_a_request_of facebook do 25 | refute_delete_by name: 'Jeff Bezos' 26 | end 27 | end 28 | 29 | test 'cannot delete by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_delete_by name: 'Jeff Bezos', error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can delete by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_delete_by name: 'Jeff Bezos' 39 | end 40 | end 41 | end 42 | 43 | test 'can delete by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_delete_by name: 'Jeff Bezos' 47 | end 48 | end 49 | end 50 | 51 | def assert_delete_by(condition) 52 | assert_difference "User.unscope_tenant.count", -1 do 53 | User.delete_by(condition) 54 | end 55 | end 56 | 57 | def refute_delete_by(name:, error: nil) 58 | if error 59 | assert_raise(error) { User.delete_by(name: name) } 60 | else 61 | assert_no_difference "User.unscope_tenant.count" do 62 | User.delete_by(name: name) 63 | end 64 | end 65 | 66 | as_super_admin do 67 | assert_equal 3, User.unscope_tenant.count 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /matrixeval.yml: -------------------------------------------------------------------------------- 1 | version: 0.4 2 | target: ruby 3 | project_name: multi-tenant-support 4 | parallel_workers: number_of_processors 5 | # commands: 6 | # - ps 7 | # - top 8 | # - an_additional_command 9 | # mounts: 10 | # - /a/path/need/to/mount:/a/path/mount/to 11 | env: 12 | DATABASE_HOST: postgres 13 | DATABASE_USERNAME: postgres 14 | REDIS_HOST: redis 15 | CI: 1 16 | matrix: 17 | ruby: 18 | variants: 19 | - key: 2.6 20 | container: 21 | image: ruby:2.6.9 22 | - key: 2.7 23 | container: 24 | image: ruby:2.7.5 25 | - key: 3.0 26 | default: true 27 | container: 28 | image: ruby:3.0.3 29 | - key: 3.1 30 | container: 31 | image: ruby:3.1.0 32 | # - key: jruby-9.3 33 | # container: 34 | # image: jruby:9.3 35 | # env: 36 | # PATH: "/opt/jruby/bin:/app/bin:/bundle/bin:$PATH" 37 | 38 | rails: 39 | variants: 40 | - key: 6.1 41 | default: true 42 | env: 43 | RAILS_VERSION: "~> 6.1.0" 44 | MIGRATION_VESRION: '6.1' 45 | mounts: 46 | - .matrixeval/schema/rails_6_1.rb:/app/test/dummy/db/schema.rb 47 | - key: 7.0 48 | env: 49 | RAILS_VERSION: "~> 7.0.0" 50 | MIGRATION_VESRION: '7.0' 51 | mounts: 52 | - .matrixeval/schema/rails_7_0.rb:/app/test/dummy/db/schema.rb 53 | # another: 54 | # variants: 55 | # - key: key1 56 | # default: true 57 | # env: 58 | # ENV_KEY: 1 59 | # - key: key2 60 | # env: 61 | # ENV_KEY: 2 62 | 63 | exclude: 64 | - ruby: 2.6 65 | rails: 7.0 66 | # - ruby: jruby-9.3 67 | # rails: 7.0 68 | 69 | docker-compose-extend: 70 | services: 71 | postgres: 72 | image: postgres:12.8 73 | volumes: 74 | - postgres12:/var/lib/postgresql/data 75 | environment: 76 | POSTGRES_HOST_AUTH_METHOD: trust 77 | 78 | redis: 79 | image: redis:6.2-alpine 80 | 81 | volumes: 82 | postgres12: -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/destroy_by_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDestroyByProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .destroy_by 8 | #### 9 | test "can destroy by tenant" do 10 | within_a_request_of amazon do 11 | assert_destroy_by name: 'Jeff Bezos' 12 | end 13 | end 14 | 15 | test "cannot destroy when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_destroy_by name: 'Jeff Bezos', error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot destroy by other tenant" do 24 | within_a_request_of facebook do 25 | refute_destroy_by name: 'Jeff Bezos' 26 | end 27 | end 28 | 29 | test 'cannot destroy by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_destroy_by name: 'Jeff Bezos', error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can destroy by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_destroy_by name: 'Jeff Bezos' 39 | end 40 | end 41 | end 42 | 43 | test 'can destroy by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_destroy_by name: 'Jeff Bezos' 47 | end 48 | end 49 | end 50 | 51 | def assert_destroy_by(condition) 52 | assert_difference "User.unscope_tenant.count", -1 do 53 | User.destroy_by(condition) 54 | end 55 | end 56 | 57 | def refute_destroy_by(name:, error: nil) 58 | if error 59 | assert_raise(error) { User.destroy_by(name: name) } 60 | else 61 | assert_no_difference "User.unscope_tenant.count" do 62 | User.destroy_by(name: name) 63 | end 64 | end 65 | 66 | as_super_admin do 67 | assert_equal 3, User.unscope_tenant.count 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/update_attribute_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateAttributeProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #update_attribute 8 | #### 9 | test "can update_attribute by tenant" do 10 | within_a_request_of amazon do 11 | assert_update_attribute bezos 12 | end 13 | end 14 | 15 | test "cannot update_attribute when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_update_attribute bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot update_attribute by other tenant" do 24 | within_a_request_of apple do 25 | refute_update_attribute bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot update_attribute by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_update_attribute bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can update_attribute by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_update_attribute bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can update_attribute by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_update_attribute bezos 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def assert_update_attribute(bezos) 54 | assert bezos.update_attribute(:name, 'JEFF BEZOS') 55 | 56 | under_tenant amazon do 57 | assert_equal "JEFF BEZOS", bezos.reload.name 58 | end 59 | end 60 | 61 | def refute_update_attribute(bezos, error:) 62 | assert_raise(error) { bezos.update_attribute(:name, 'JEFF BEZOS') } 63 | 64 | under_tenant amazon do 65 | assert_equal "Jeff Bezos", bezos.reload.name 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/support/dsl.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | module DSL 3 | 4 | def within_a_request_of(tenant_account, &block) 5 | if tenant_account.is_a?(Symbol) 6 | raise "Unkown tenant" unless tenant_account == super_admin 7 | 8 | as_super_admin do 9 | yield 10 | end 11 | else 12 | under_tenant tenant_account do 13 | yield 14 | end 15 | end 16 | end 17 | 18 | def missing_tenant 19 | without_current_tenant do 20 | yield 21 | end 22 | end 23 | 24 | def under_tenant(tenant_account) 25 | MultiTenantSupport.under_tenant tenant_account do 26 | yield 27 | end 28 | end 29 | 30 | def turn_on_full_protection 31 | MultiTenantSupport.turn_on_full_protection do 32 | yield 33 | end 34 | end 35 | 36 | def allow_read_across_tenant 37 | MultiTenantSupport.allow_read_across_tenant do 38 | yield 39 | end 40 | end 41 | 42 | def without_current_tenant 43 | MultiTenantSupport.without_current_tenant do 44 | yield 45 | end 46 | end 47 | 48 | def turn_off_protection 49 | MultiTenantSupport.turn_off_protection do 50 | yield 51 | end 52 | end 53 | 54 | def as_super_admin 55 | without_current_tenant do 56 | allow_read_across_tenant do 57 | yield 58 | end 59 | end 60 | end 61 | 62 | def super_admin 63 | :super_admin 64 | end 65 | 66 | def amazon 67 | accounts(:amazon) 68 | end 69 | 70 | def apple 71 | accounts(:apple) 72 | end 73 | 74 | def facebook 75 | accounts(:facebook) 76 | end 77 | 78 | def setup_users 79 | allow_read_across_tenant do 80 | @bezos = users(:bezos) 81 | @zuck = users(:zuck) 82 | @steve = users(:steve) 83 | end 84 | end 85 | 86 | def bezos 87 | @bezos 88 | end 89 | 90 | def zuck 91 | @zuck 92 | end 93 | 94 | def steve 95 | @steve 96 | end 97 | end 98 | end -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/load_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_LoadTest < ActiveSupport::TestCase 4 | 5 | test 'bezos can only initialize under amazon' do 6 | MultiTenantSupport.under_tenant accounts(:amazon) do 7 | users(:bezos).reload 8 | end 9 | 10 | MultiTenantSupport.under_tenant accounts(:facebook) do 11 | assert_raise(ActiveRecord::RecordNotFound) { users(:bezos).reload } 12 | end 13 | 14 | MultiTenantSupport.under_tenant accounts(:apple) do 15 | assert_raise(ActiveRecord::RecordNotFound) { users(:bezos).reload } 16 | end 17 | end 18 | 19 | test 'zuck can only initialize under facebook' do 20 | MultiTenantSupport.under_tenant accounts(:amazon) do 21 | assert_raise(ActiveRecord::RecordNotFound) { users(:zuck).reload } 22 | end 23 | 24 | MultiTenantSupport.under_tenant accounts(:facebook) do 25 | users(:zuck).reload 26 | end 27 | 28 | MultiTenantSupport.under_tenant accounts(:apple) do 29 | assert_raise(ActiveRecord::RecordNotFound) { users(:zuck).reload } 30 | end 31 | end 32 | 33 | test 'steve can only initialize under apple' do 34 | MultiTenantSupport.under_tenant accounts(:amazon) do 35 | assert_raise(ActiveRecord::RecordNotFound) { users(:steve).reload } 36 | end 37 | 38 | MultiTenantSupport.under_tenant accounts(:facebook) do 39 | assert_raise(ActiveRecord::RecordNotFound) { users(:steve).reload } 40 | end 41 | 42 | MultiTenantSupport.under_tenant accounts(:apple) do 43 | users(:steve).reload 44 | end 45 | end 46 | 47 | test 'initialize works for all users when allow_read_across_tennat' do 48 | allow_read_across_tenant do 49 | users(:steve).reload 50 | users(:bezos).reload 51 | users(:zuck).reload 52 | end 53 | end 54 | 55 | test 'initialize works for all users when turn off protection' do 56 | turn_off_protection do 57 | users(:steve).reload 58 | users(:bezos).reload 59 | users(:zuck).reload 60 | end 61 | end 62 | 63 | end -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [1.5.0] - 2022-02-21 4 | 5 | - Support Rails >= 6.1 only 6 | - Test with [matrixeval-ruby](https://github.com/MatrixEval/matrixeval-ruby) 7 | 8 | ## [1.4.0] - 2021-10-13 9 | 10 | #### Add 11 | 12 | - `MultiTenantSupport.set_current_tenant` 13 | - `MultiTenantSupport.without_current_tenant` 14 | - `MultiTenantSupport.full_protected?` 15 | - `MultiTenantSupport.unprotected?` 16 | - `MultiTenantSupport.turn_off_protection` 17 | - `MultiTenantSupport.turn_on_full_protection` 18 | 19 | #### Remove 20 | 21 | - `MultiTenantSupport.disallow_read_across_tenant?` 22 | 23 | ## [1.3.1] - 2021-10-10 24 | 25 | - Make ViewHelper work in both controller and view 26 | 27 | ## [1.3.0] - 2021-10-10 28 | 29 | - Integrate with Rails default testing toolchain (Minitest + Capybara) 30 | - Integrate with RSpec + Capybara 31 | 32 | ## [1.2.0] - 2021-10-08 33 | 34 | - Keep current tenant unchange around job perform with SiekiqAdapter,TestAdapter,InlineAdapter,AsyncAdapter 35 | - Add a new console config `allow_read_across_tenant_by_default` 36 | - Add an environment varialbe `ALLOW_READ_ACROSS_TENANT` 37 | 38 | ## [1.1.1] - 2021-10-07 39 | 40 | - Make sure all four job delivery ways work as expected 41 | - MyJob.perform_now('hi') 42 | - MyJob.perform_later('hi') 43 | - MyJob.set(queue: 'high').perform_now('hi') 44 | - MyJob.set(queue: 'high').perform_later('hi') 45 | 46 | ## [1.1.0] - 2021-10-07 47 | 48 | - Make tenant finding strategy customizable 49 | - Override `find_current_tenant_account` in controller 50 | 51 | ## [1.0.5] - 2021-10-06 52 | 53 | - Fix an error caused by call `helper_method` on ActionController::API 54 | 55 | ## [1.0.4] - 2021-10-05 56 | 57 | - Rename "lib/multi_tenant_support.rb" to "lib/multi-tenant-support.rb" 58 | - Breaking: please remove `require 'multi_tenant_support'` from the `config/initializers/multi_tenant_support.rb` 59 | 60 | ## [1.0.3] - 2021-10-04 61 | 62 | - Prevent most ActiveRecord CRUD methods from acting across tenants. 63 | - Support Row-level Multitenancy 64 | - Build on ActiveSupport::CurrentAttributes offered by rails 65 | - Auto set current tenant through subdomain and domain in controller 66 | - Support ActiveJob and Sidekiq 67 | -------------------------------------------------------------------------------- /test/integration/active_job/async_adapter/perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module AsycnAdapter 5 | class PerformLaterTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :async 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | perform_enqueued_jobs do 18 | UserNameUpdateJob.perform_later(bezos) 19 | end 20 | end 21 | 22 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 23 | end 24 | end 25 | 26 | test 'fail to update user when tenant account is missing' do 27 | without_current_tenant do 28 | assert_no_changes 'MultiTenantSupport.current_tenant' do 29 | perform_enqueued_jobs do 30 | begin 31 | UserNameUpdateJob.perform_later(bezos) 32 | rescue ActiveJob::DeserializationError => e 33 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", e.message 34 | end 35 | end 36 | end 37 | end 38 | 39 | under_tenant(amazon) do 40 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 41 | end 42 | end 43 | 44 | test 'fail to update user when tenant account is not match' do 45 | under_tenant(apple) do 46 | assert_no_changes 'MultiTenantSupport.current_tenant' do 47 | perform_enqueued_jobs do 48 | begin 49 | UserNameUpdateJob.perform_later(bezos) 50 | rescue ActiveJob::DeserializationError => e 51 | assert_includes e.message, "Couldn't find User with 'id'" 52 | end 53 | end 54 | end 55 | end 56 | 57 | under_tenant(amazon) do 58 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 59 | end 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /test/integration/active_job/test_adapter/perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module TestAdapter 5 | class PerformLaterTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :test 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | perform_enqueued_jobs do 18 | UserNameUpdateJob.perform_later(bezos) 19 | end 20 | end 21 | 22 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 23 | end 24 | end 25 | 26 | test 'fail to update user when tenant account is missing' do 27 | without_current_tenant do 28 | assert_no_changes 'MultiTenantSupport.current_tenant' do 29 | perform_enqueued_jobs do 30 | begin 31 | UserNameUpdateJob.perform_later(bezos) 32 | rescue ActiveJob::DeserializationError => e 33 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", e.message 34 | end 35 | end 36 | end 37 | end 38 | 39 | under_tenant(amazon) do 40 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 41 | end 42 | end 43 | 44 | test 'fail to update user when tenant account is not match' do 45 | under_tenant(apple) do 46 | assert_no_changes 'MultiTenantSupport.current_tenant' do 47 | perform_enqueued_jobs do 48 | begin 49 | UserNameUpdateJob.perform_later(bezos) 50 | rescue ActiveJob::DeserializationError => e 51 | assert_includes e.message, "Couldn't find User with 'id'" 52 | end 53 | end 54 | end 55 | end 56 | 57 | under_tenant(amazon) do 58 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 59 | end 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /test/integration/active_job/inline_adapter/perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module InlineAdapter 5 | class PerformLaterTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :inline 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | perform_enqueued_jobs do 18 | UserNameUpdateJob.perform_later(bezos) 19 | end 20 | end 21 | 22 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 23 | end 24 | end 25 | 26 | test 'fail to update user when tenant account is missing' do 27 | without_current_tenant do 28 | assert_no_changes 'MultiTenantSupport.current_tenant' do 29 | perform_enqueued_jobs do 30 | begin 31 | UserNameUpdateJob.perform_later(bezos) 32 | rescue ActiveJob::DeserializationError => e 33 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", e.message 34 | end 35 | end 36 | end 37 | end 38 | 39 | under_tenant(amazon) do 40 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 41 | end 42 | end 43 | 44 | test 'fail to update user when tenant account is not match' do 45 | under_tenant(apple) do 46 | assert_no_changes 'MultiTenantSupport.current_tenant' do 47 | perform_enqueued_jobs do 48 | begin 49 | UserNameUpdateJob.perform_later(bezos) 50 | rescue ActiveJob::DeserializationError => e 51 | assert_includes e.message, "Couldn't find User with 'id'" 52 | end 53 | end 54 | end 55 | end 56 | 57 | under_tenant(amazon) do 58 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 59 | end 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /test/integration/active_job/async_adapter/preconfigured_perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module AsycnAdapter 5 | class PreconfiguredPerformLaterTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :async 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | perform_enqueued_jobs do 18 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 19 | end 20 | end 21 | 22 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 23 | end 24 | end 25 | 26 | test 'fail to update user when tenant account is missing' do 27 | without_current_tenant do 28 | assert_no_changes 'MultiTenantSupport.current_tenant' do 29 | perform_enqueued_jobs do 30 | begin 31 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 32 | rescue ActiveJob::DeserializationError => e 33 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", e.message 34 | end 35 | end 36 | end 37 | end 38 | 39 | under_tenant(amazon) do 40 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 41 | end 42 | end 43 | 44 | test 'fail to update user when tenant account is not match' do 45 | under_tenant(apple) do 46 | assert_no_changes 'MultiTenantSupport.current_tenant' do 47 | perform_enqueued_jobs do 48 | begin 49 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 50 | rescue ActiveJob::DeserializationError => e 51 | assert_includes e.message, "Couldn't find User with 'id'" 52 | end 53 | end 54 | end 55 | end 56 | 57 | under_tenant(amazon) do 58 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 59 | end 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /test/integration/active_job/test_adapter/preconfigured_perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module TestAdapter 5 | class PreconfiguredPerformLaterTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :test 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | perform_enqueued_jobs do 18 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 19 | end 20 | end 21 | 22 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 23 | end 24 | end 25 | 26 | test 'fail to update user when tenant account is missing' do 27 | without_current_tenant do 28 | assert_no_changes 'MultiTenantSupport.current_tenant' do 29 | perform_enqueued_jobs do 30 | begin 31 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 32 | rescue ActiveJob::DeserializationError => e 33 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", e.message 34 | end 35 | end 36 | end 37 | end 38 | 39 | under_tenant(amazon) do 40 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 41 | end 42 | end 43 | 44 | test 'fail to update user when tenant account is not match' do 45 | under_tenant(apple) do 46 | assert_no_changes 'MultiTenantSupport.current_tenant' do 47 | perform_enqueued_jobs do 48 | begin 49 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 50 | rescue ActiveJob::DeserializationError => e 51 | assert_includes e.message, "Couldn't find User with 'id'" 52 | end 53 | end 54 | end 55 | end 56 | 57 | under_tenant(amazon) do 58 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 59 | end 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /test/integration/active_job/inline_adapter/preconfigured_perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module TestActiveJob 4 | module InlineAdapter 5 | class PreconfiguredPerformLaterTest < ActiveSupport::TestCase 6 | include ActiveJob::TestHelper 7 | 8 | setup do 9 | Rails.application.configure do 10 | config.active_job.queue_adapter = :inline 11 | end 12 | end 13 | 14 | test 'update succes update user when tenant account match' do 15 | under_tenant(amazon) do 16 | assert_no_changes 'MultiTenantSupport.current_tenant' do 17 | perform_enqueued_jobs do 18 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 19 | end 20 | end 21 | 22 | assert_equal "Jeff Bezos UPDATE", bezos.reload.name 23 | end 24 | end 25 | 26 | test 'fail to update user when tenant account is missing' do 27 | without_current_tenant do 28 | assert_no_changes 'MultiTenantSupport.current_tenant' do 29 | perform_enqueued_jobs do 30 | begin 31 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 32 | rescue ActiveJob::DeserializationError => e 33 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", e.message 34 | end 35 | end 36 | end 37 | end 38 | 39 | under_tenant(amazon) do 40 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 41 | end 42 | end 43 | 44 | test 'fail to update user when tenant account is not match' do 45 | under_tenant(apple) do 46 | assert_no_changes 'MultiTenantSupport.current_tenant' do 47 | perform_enqueued_jobs do 48 | begin 49 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 50 | rescue ActiveJob::DeserializationError => e 51 | assert_includes e.message, "Couldn't find User with 'id'" 52 | end 53 | end 54 | end 55 | end 56 | 57 | under_tenant(amazon) do 58 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 59 | end 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/tenant_account_cannot_be_nil_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_TenantAccountCannotBeNil < ActiveSupport::TestCase 4 | 5 | test "raise error when assign nil to account" do 6 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 7 | assert_raise(MultiTenantSupport::NilTenantError) { users(:bezos).account = nil } 8 | end 9 | end 10 | 11 | test "raise error when assign nil to account_id" do 12 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 13 | assert_raise(MultiTenantSupport::NilTenantError) { users(:bezos).account_id = nil } 14 | end 15 | end 16 | 17 | test "raise error when change account to nil through .update" do 18 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 19 | assert_raise(MultiTenantSupport::NilTenantError) { users(:bezos).update(account: nil) } 20 | end 21 | end 22 | 23 | test "raise error when change account_id to nil through .update" do 24 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 25 | assert_raise(MultiTenantSupport::NilTenantError) { users(:bezos).update(account_id: nil) } 26 | end 27 | end 28 | 29 | test "raise error when change account to nil through .update_attribute" do 30 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 31 | assert_raise(MultiTenantSupport::NilTenantError) { users(:bezos).update_attribute(:account, nil) } 32 | end 33 | end 34 | 35 | test "raise error when change account_id to nil through .update_attribute" do 36 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 37 | assert_raise("account_id is marked as readonly") { users(:bezos).update_attribute(:account_id, nil) } 38 | end 39 | end 40 | 41 | test "raise error when change account_id to nil through .update_columns" do 42 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 43 | assert_raise("account_id is marked as readonly") { users(:bezos).update_columns(account_id: nil) } 44 | end 45 | end 46 | 47 | test "raise error when change account_id to nil through .update_column" do 48 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 49 | assert_raise("account_id is marked as readonly") { users(:bezos).update_column(:account_id, nil) } 50 | end 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/readonly_tenant_account_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MultiTenantSupport::ModelConcern::BelongsToTenant_ReadonlyTenantAccountTest < ActiveSupport::TestCase 4 | 5 | test "raise error when assign user.account" do 6 | MultiTenantSupport.under_tenant accounts(:amazon) do 7 | assert_raise(MultiTenantSupport::ImmutableTenantError) { users(:bezos).account = accounts(:facebook) } 8 | end 9 | end 10 | 11 | test "raise error when assign user.account_id" do 12 | MultiTenantSupport.under_tenant accounts(:amazon) do 13 | assert_raise(MultiTenantSupport::ImmutableTenantError) { users(:bezos).account_id = accounts(:facebook).id } 14 | end 15 | end 16 | 17 | test "raise error when change account through .update" do 18 | MultiTenantSupport.under_tenant accounts(:amazon) do 19 | assert_raise(MultiTenantSupport::ImmutableTenantError) { users(:bezos).update(account: accounts(:facebook)) } 20 | end 21 | end 22 | 23 | test "raise error when change account_id through .update" do 24 | MultiTenantSupport.under_tenant accounts(:amazon) do 25 | assert_raise(MultiTenantSupport::ImmutableTenantError) { users(:bezos).update(account_id: accounts(:facebook).id) } 26 | end 27 | end 28 | 29 | test "raise error when change account through .update_attribute" do 30 | MultiTenantSupport.under_tenant accounts(:amazon) do 31 | assert_raise(MultiTenantSupport::ImmutableTenantError) { users(:bezos).update_attribute(:account, accounts(:facebook)) } 32 | end 33 | end 34 | 35 | test "raise error when change account_id through .update_attribute" do 36 | MultiTenantSupport.under_tenant accounts(:amazon) do 37 | assert_raise("account_id is marked as readonly") { users(:bezos).update_attribute(:account_id, accounts(:facebook).id) } 38 | end 39 | end 40 | 41 | test "raise error when change account_id through .update_columns" do 42 | MultiTenantSupport.under_tenant accounts(:amazon) do 43 | assert_raise("account_id is marked as readonly") { users(:bezos).update_columns(account_id: accounts(:facebook).id) } 44 | end 45 | end 46 | 47 | test "raise error when change account_id through .update_column" do 48 | MultiTenantSupport.under_tenant accounts(:amazon) do 49 | assert_raise("account_id is marked as readonly") { users(:bezos).update_column(:account_id, accounts(:facebook).id) } 50 | end 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | config.cache_classes = true 12 | 13 | # Do not eager load code on boot. This avoids loading your whole application 14 | # just for the purpose of running a single test. If you are using a tool that 15 | # preloads Rails for running tests, you may have to set it to true. 16 | config.eager_load = false 17 | 18 | # Configure public file server for tests with Cache-Control for performance. 19 | config.public_file_server.enabled = true 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 22 | } 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | config.cache_store = :null_store 28 | 29 | # Raise exceptions instead of rendering exception templates. 30 | config.action_dispatch.show_exceptions = false 31 | 32 | # Disable request forgery protection in test environment. 33 | config.action_controller.allow_forgery_protection = false 34 | 35 | # Store uploaded files on the local file system in a temporary directory. 36 | config.active_storage.service = :test 37 | 38 | config.action_mailer.perform_caching = false 39 | 40 | # Tell Action Mailer not to deliver emails to the real world. 41 | # The :test delivery method accumulates sent emails in the 42 | # ActionMailer::Base.deliveries array. 43 | config.action_mailer.delivery_method = :test 44 | 45 | # Print deprecation notices to the stderr. 46 | config.active_support.deprecation = :stderr 47 | 48 | # Raise exceptions for disallowed deprecations. 49 | config.active_support.disallowed_deprecation = :raise 50 | 51 | # Tell Active Support which deprecation messages to disallow. 52 | config.active_support.disallowed_deprecation_warnings = [] 53 | 54 | # Raises error for missing translations. 55 | # config.i18n.raise_on_missing_translations = true 56 | 57 | # Annotate rendered view with file names. 58 | # config.action_view.annotate_rendered_view_with_filenames = true 59 | 60 | config.active_job.queue_adapter = :sidekiq 61 | end 62 | -------------------------------------------------------------------------------- /lib/multi-tenant-support.rb: -------------------------------------------------------------------------------- 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/config/console" 8 | require "multi_tenant_support/current" 9 | require "multi_tenant_support/find_tenant_account" 10 | require "multi_tenant_support/concern/controller_concern" 11 | require "multi_tenant_support/concern/model_concern" 12 | 13 | module MultiTenantSupport 14 | 15 | module_function 16 | 17 | def configure(&block) 18 | instance_eval(&block) 19 | end 20 | 21 | def current_tenant 22 | Current.tenant_account 23 | end 24 | 25 | def current_tenant_id 26 | Current.tenant_account&.send(model.tenant_account_primary_key) 27 | end 28 | 29 | def set_current_tenant(tenant) 30 | Current.tenant_account = tenant 31 | Current.protection_state = PROTECTED 32 | end 33 | 34 | def under_tenant(tenant_account, &block) 35 | raise ArgumentError, "block is missing" if block.nil? 36 | 37 | Current.set(tenant_account: tenant_account, protection_state: PROTECTED) do 38 | yield 39 | end 40 | end 41 | 42 | def without_current_tenant(&block) 43 | raise ArgumentError, "block is missing" if block.nil? 44 | 45 | Current.set(tenant_account: nil) do 46 | yield 47 | end 48 | end 49 | 50 | def full_protected? 51 | current_tenant || Current.protection_state == PROTECTED 52 | end 53 | 54 | def allow_read_across_tenant? 55 | current_tenant.nil? && [PROTECTED_EXCEPT_READ, UNPROTECTED].include?(Current.protection_state) 56 | end 57 | 58 | def unprotected? 59 | current_tenant.nil? && Current.protection_state == UNPROTECTED 60 | end 61 | 62 | def turn_off_protection 63 | raise 'Cannot turn off protection, try wrap in without_current_tenant' if current_tenant 64 | 65 | if block_given? 66 | Current.set(protection_state: UNPROTECTED) do 67 | yield 68 | end 69 | else 70 | Current.protection_state = UNPROTECTED 71 | end 72 | end 73 | 74 | def allow_read_across_tenant 75 | raise 'Cannot read across tenant, try wrap in without_current_tenant' if current_tenant 76 | 77 | if block_given? 78 | Current.set(protection_state: PROTECTED_EXCEPT_READ) do 79 | yield 80 | end 81 | else 82 | Current.protection_state = PROTECTED_EXCEPT_READ 83 | end 84 | end 85 | 86 | def turn_on_full_protection 87 | if block_given? 88 | Current.set(protection_state: PROTECTED) do 89 | yield 90 | end 91 | else 92 | Current.protection_state = PROTECTED 93 | end 94 | end 95 | 96 | end 97 | -------------------------------------------------------------------------------- /test/integration/active_job/sidekiq_adapter/preconfigured_perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'sidekiq' 3 | require 'sidekiq/testing' 4 | 5 | module TestActiveJob 6 | module SidekiqAdapter 7 | class PreconfiguredPeroformLaterTest < ActiveSupport::TestCase 8 | 9 | teardown do 10 | Sidekiq::RetrySet.new.clear 11 | end 12 | 13 | test 'update succes update user when tenant account match' do 14 | under_tenant(amazon) do 15 | assert_no_changes 'MultiTenantSupport.current_tenant' do 16 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 17 | end 18 | end 19 | 20 | under_tenant(amazon) do 21 | multi_attempt_assert("Failed to update Bezos's name") do 22 | "Jeff Bezos UPDATE" == bezos.reload.name 23 | end 24 | end 25 | end 26 | 27 | test 'fail to update user when tenant account is missing on enqueue' do 28 | assert_no_changes 'MultiTenantSupport.current_tenant' do 29 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 30 | end 31 | 32 | Sidekiq.redis do |connection| 33 | retries = [] 34 | wait_until do 35 | retries = connection.zrange "retry", 0, -1 36 | !retries.nil? 37 | end 38 | assert 1, retries.count 39 | failed_job_data = JSON.parse(retries.last) 40 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", failed_job_data["error_message"] 41 | assert_equal "ActiveJob::DeserializationError", failed_job_data["error_class"] 42 | end 43 | 44 | under_tenant(amazon) do 45 | assert_equal "Jeff Bezos", bezos.reload.name # Does not change 46 | end 47 | end 48 | 49 | test 'fail to update user when tenant account is not match' do 50 | under_tenant(apple) do 51 | assert_no_changes 'MultiTenantSupport.current_tenant' do 52 | UserNameUpdateJob.set(queue: :integration_tests).perform_later(bezos) 53 | end 54 | end 55 | 56 | Sidekiq.redis do |connection| 57 | retries = [] 58 | wait_until do 59 | retries = connection.zrange "retry", 0, -1 60 | !retries.nil? 61 | end 62 | assert 1, retries.count 63 | failed_job_data = JSON.parse(retries.last) 64 | assert_equal %Q{Error while trying to deserialize arguments: Couldn't find User with 'id'=#{bezos.id} [WHERE "users"."account_id" = $1]}, failed_job_data["error_message"] 65 | assert_equal "ActiveJob::DeserializationError", failed_job_data["error_class"] 66 | end 67 | 68 | under_tenant(amazon) do 69 | assert_equal "Jeff Bezos", bezos.reload.name # Does not change 70 | end 71 | end 72 | 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/multi_tenant_support/active_job.rb: -------------------------------------------------------------------------------- 1 | module MultiTenantSupport 2 | 3 | module ActiveJob 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | attr_accessor :current_tenant 8 | end 9 | 10 | class_methods do 11 | 12 | if Gem::Version.new(Rails.version) < Gem::Version.new("7.0.0.alpha1") 13 | def perform_now(*args) 14 | job = job_or_instantiate(*args) 15 | job.current_tenant = MultiTenantSupport.current_tenant 16 | job.perform_now 17 | end 18 | else 19 | eval(" 20 | def perform_now(...) 21 | job = job_or_instantiate(...) 22 | job.current_tenant = MultiTenantSupport.current_tenant 23 | job.perform_now 24 | end 25 | ") 26 | end 27 | 28 | def execute(job_data) 29 | keep_current_tenant_unchange do 30 | super(job_data) 31 | end 32 | end 33 | 34 | private 35 | 36 | def keep_current_tenant_unchange 37 | _current_tenant = MultiTenantSupport::Current.tenant_account 38 | yield 39 | ensure 40 | MultiTenantSupport::Current.tenant_account = _current_tenant 41 | end 42 | end 43 | 44 | def perform_now 45 | MultiTenantSupport.under_tenant(current_tenant) do 46 | super 47 | end 48 | end 49 | 50 | def serialize 51 | if MultiTenantSupport.current_tenant 52 | super.merge({ 53 | "multi_tenant_support" => { 54 | "id" => MultiTenantSupport.current_tenant.id, 55 | "class" => MultiTenantSupport.current_tenant.class.name 56 | } 57 | }) 58 | else 59 | super 60 | end 61 | end 62 | 63 | def deserialize(job_data) 64 | self.current_tenant = find_current_tenant(job_data) 65 | MultiTenantSupport.under_tenant(current_tenant) do 66 | super 67 | end 68 | end 69 | 70 | private 71 | 72 | def find_current_tenant(data) 73 | return unless data.has_key?("multi_tenant_support") 74 | 75 | tenant_klass = data["multi_tenant_support"]["class"].constantize 76 | tenant_id = data["multi_tenant_support"]["id"] 77 | 78 | tenant_klass.find tenant_id 79 | end 80 | end 81 | 82 | module ConfiguredJob 83 | if Gem::Version.new(Rails.version) < Gem::Version.new("7.0.0.alpha1") 84 | def perform_now(*args) 85 | job = @job_class.new(*args) 86 | job.current_tenant = MultiTenantSupport.current_tenant 87 | job.perform_now 88 | end 89 | else 90 | eval(" 91 | def perform_now(...) 92 | job = @job_class.new(...) 93 | job.current_tenant = MultiTenantSupport.current_tenant 94 | job.perform_now 95 | end 96 | ") 97 | end 98 | end 99 | end 100 | 101 | ActiveJob::Base.include(MultiTenantSupport::ActiveJob) 102 | ActiveJob::ConfiguredJob.prepend(MultiTenantSupport::ConfiguredJob) -------------------------------------------------------------------------------- /test/support/sidekiq_jobs_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sidekiq/api" 4 | 5 | require "sidekiq/testing" 6 | Sidekiq::Testing.disable! 7 | 8 | require "sidekiq/util" 9 | 10 | class SidekiqJobsManager 11 | include Sidekiq::Util 12 | include Singleton 13 | 14 | def setup 15 | ActiveJob::Base.queue_adapter = :sidekiq 16 | unless can_run? 17 | puts "Cannot run integration tests for sidekiq. To be able to run integration tests for sidekiq you need to install and start redis.\n" 18 | status = ENV["CI"] ? false : true 19 | exit status 20 | end 21 | end 22 | 23 | def clear_jobs 24 | Sidekiq::ScheduledSet.new.clear 25 | Sidekiq::Queue.new("integration_tests").clear 26 | Sidekiq::RetrySet.new.clear 27 | end 28 | 29 | def start_workers 30 | continue_read, continue_write = IO.pipe 31 | death_read, death_write = IO.pipe 32 | 33 | @pid = fork do 34 | continue_read.close 35 | death_write.close 36 | 37 | # Sidekiq is not warning-clean :( 38 | $VERBOSE = false 39 | 40 | $stdin.reopen(File::NULL) 41 | $stdout.sync = true 42 | $stderr.sync = true 43 | 44 | logfile = Rails.root.join("log/sidekiq.log").to_s 45 | Sidekiq.logger = Sidekiq::Logger.new(logfile) 46 | 47 | self_read, self_write = IO.pipe 48 | trap "TERM" do 49 | self_write.puts("TERM") 50 | end 51 | 52 | Thread.new do 53 | begin 54 | death_read.read 55 | rescue Exception 56 | end 57 | self_write.puts("TERM") 58 | end 59 | 60 | require "sidekiq/cli" 61 | require "sidekiq/launcher" 62 | 63 | fire_event(:startup, reverse: false, reraise: true) 64 | 65 | sidekiq = Sidekiq::Launcher.new(queues: ["integration_tests"], 66 | environment: "test", 67 | concurrency: 1, 68 | timeout: 1) 69 | Sidekiq.average_scheduled_poll_interval = 0.5 70 | Sidekiq.options[:poll_interval_average] = 1 71 | begin 72 | sidekiq.run 73 | continue_write.puts "started" 74 | while readable_io = IO.select([self_read]) 75 | signal = readable_io.first[0].gets.strip 76 | raise Interrupt if signal == "TERM" 77 | end 78 | rescue Interrupt 79 | end 80 | 81 | sidekiq.stop 82 | exit! 83 | end 84 | continue_write.close 85 | death_read.close 86 | @worker_lifeline = death_write 87 | 88 | raise "Failed to start worker" unless continue_read.gets == "started\n" 89 | end 90 | 91 | def stop_workers 92 | if @pid 93 | Process.kill "TERM", @pid 94 | Process.wait @pid 95 | end 96 | end 97 | 98 | def can_run? 99 | begin 100 | Sidekiq.redis(&:info) 101 | Sidekiq.logger = nil 102 | rescue 103 | return false 104 | end 105 | true 106 | end 107 | end -------------------------------------------------------------------------------- /test/integration/active_job/sidekiq_adapter/perform_later_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'sidekiq' 3 | require 'sidekiq/testing' 4 | 5 | module TestActiveJob 6 | module SidekiqAdapter 7 | class PeroformLaterTest < ActiveSupport::TestCase 8 | 9 | setup do 10 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 11 | @bezos = users(:bezos) 12 | end 13 | end 14 | 15 | teardown do 16 | Sidekiq::RetrySet.new.clear 17 | end 18 | 19 | test 'update succes update user when tenant account match' do 20 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 21 | assert_no_changes 'MultiTenantSupport.current_tenant' do 22 | UserNameUpdateJob.perform_later(@bezos) 23 | end 24 | end 25 | 26 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 27 | multi_attempt_assert("Failed to update Bezos's name") do 28 | @bezos.reload.name == "Jeff Bezos UPDATE" 29 | end 30 | end 31 | end 32 | 33 | test 'fail to update user when tenant account is missing on enqueue' do 34 | assert_no_changes 'MultiTenantSupport.current_tenant' do 35 | UserNameUpdateJob.perform_later(@bezos) 36 | end 37 | 38 | Sidekiq.redis do |connection| 39 | retries = [] 40 | wait_until do 41 | retries = connection.zrange "retry", 0, -1 42 | !retries.last.nil? 43 | end 44 | assert 1, retries.count 45 | failed_job_data = JSON.parse(retries.last) 46 | assert_equal "Error while trying to deserialize arguments: MultiTenantSupport::MissingTenantError", failed_job_data["error_message"] 47 | assert_equal "ActiveJob::DeserializationError", failed_job_data["error_class"] 48 | end 49 | 50 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 51 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 52 | end 53 | end 54 | 55 | test 'fail to update user when tenant account is not match' do 56 | MultiTenantSupport.under_tenant(accounts(:apple)) do 57 | assert_no_changes 'MultiTenantSupport.current_tenant' do 58 | UserNameUpdateJob.perform_later(@bezos) 59 | end 60 | end 61 | 62 | Sidekiq.redis do |connection| 63 | retries = [] 64 | wait_until do 65 | retries = connection.zrange "retry", 0, -1 66 | !retries.last.nil? 67 | end 68 | assert 1, retries.count 69 | failed_job_data = JSON.parse(retries.last) 70 | assert_equal %Q{Error while trying to deserialize arguments: Couldn't find User with 'id'=#{@bezos.id} [WHERE "users"."account_id" = $1]}, failed_job_data["error_message"] 71 | assert_equal "ActiveJob::DeserializationError", failed_job_data["error_class"] 72 | end 73 | 74 | MultiTenantSupport.under_tenant(accounts(:amazon)) do 75 | assert_equal "Jeff Bezos", @bezos.reload.name # Does not change 76 | end 77 | end 78 | 79 | end 80 | end 81 | end -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 20 | config.action_controller.perform_caching = true 21 | config.action_controller.enable_fragment_cache_logging = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Store uploaded files on the local file system (see config/storage.yml for options). 34 | config.active_storage.service = :local 35 | 36 | # Don't care if the mailer can't send. 37 | config.action_mailer.raise_delivery_errors = false 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise exceptions for disallowed deprecations. 45 | config.active_support.disallowed_deprecation = :raise 46 | 47 | # Tell Active Support which deprecation messages to disallow. 48 | config.active_support.disallowed_deprecation_warnings = [] 49 | 50 | # Raise an error on page load if there are pending migrations. 51 | config.active_record.migration_error = :page_load 52 | 53 | # Highlight code that triggered database queries in logs. 54 | config.active_record.verbose_query_logs = true 55 | 56 | # Debug mode disables concatenation and preprocessing of assets. 57 | # This option may cause significant delays in view rendering with a large 58 | # number of complex assets. 59 | 60 | if Gem::Version.new(Rails.version) < Gem::Version.new("7.0.0.alpha2") 61 | config.assets.debug = true 62 | 63 | # Suppress logger output for asset requests. 64 | config.assets.quiet = true 65 | end 66 | 67 | # Raises error for missing translations. 68 | # config.i18n.raise_on_missing_translations = true 69 | 70 | # Annotate rendered view with file names. 71 | # config.action_view.annotate_rendered_view_with_filenames = true 72 | 73 | # Use an evented file watcher to asynchronously detect changes in source code, 74 | # routes, locales, etc. This feature depends on the listen gem. 75 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 76 | 77 | # Uncomment if you wish to allow Action Cable access from any origin. 78 | # config.action_cable.disable_request_forgery_protection = true 79 | end 80 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/create/save_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelCreateBySaveProtectTest < ActiveSupport::TestCase 5 | 6 | setup do 7 | under_tenant apple do 8 | @cook = User.new(name: 'Tim Cook', email: 'cook@example.com') 9 | end 10 | end 11 | 12 | #### 13 | # create by #save 14 | #### 15 | test "can save by tenant" do 16 | within_a_request_of apple do 17 | assert_save @cook 18 | end 19 | end 20 | 21 | test "cannot save when tenant is missing" do 22 | turn_on_full_protection do 23 | missing_tenant do 24 | refute_save @cook, error: MultiTenantSupport::MissingTenantError 25 | end 26 | end 27 | end 28 | 29 | test 'cannot save by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_save @cook, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can save by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant apple do 38 | assert_save @cook 39 | end 40 | end 41 | end 42 | 43 | test 'can save by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_save @cook 47 | end 48 | end 49 | end 50 | 51 | #### 52 | # #save! 53 | #### 54 | test "can save! by tenant" do 55 | within_a_request_of apple do 56 | assert_save! @cook 57 | end 58 | end 59 | 60 | test "cannot save! when tenant is missing" do 61 | turn_on_full_protection do 62 | missing_tenant do 63 | refute_save! @cook, error: MultiTenantSupport::MissingTenantError 64 | end 65 | end 66 | end 67 | 68 | test 'cannot save! by super admin (default)' do 69 | within_a_request_of super_admin do 70 | refute_save! @cook, error: MultiTenantSupport::MissingTenantError 71 | end 72 | end 73 | 74 | test 'can save! by super admin through manual set current tenant' do 75 | within_a_request_of super_admin do 76 | under_tenant apple do 77 | assert_save! @cook 78 | end 79 | end 80 | end 81 | 82 | test 'can save! by super admin through manual turn off protection' do 83 | within_a_request_of super_admin do 84 | turn_off_protection do 85 | assert_save! @cook 86 | end 87 | end 88 | end 89 | 90 | private 91 | 92 | def assert_save(user) 93 | assert_difference "User.unscope_tenant.count", 1 do 94 | assert user.save 95 | assert apple, user.account 96 | end 97 | end 98 | 99 | def refute_save(user, error:) 100 | assert_raise(error) { user.save } 101 | 102 | as_super_admin do 103 | assert_equal 3, User.unscope_tenant.count 104 | end 105 | end 106 | 107 | def assert_save!(user) 108 | assert_difference "User.unscope_tenant.count", 1 do 109 | assert user.save! 110 | assert apple, user.account 111 | end 112 | end 113 | 114 | def refute_save!(user, error:) 115 | assert_raise(error) { user.save! } 116 | 117 | as_super_admin do 118 | assert_equal 3, User.unscope_tenant.count 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/save_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelUpdateBySaveProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #save 8 | #### 9 | test "can save by tenant" do 10 | within_a_request_of amazon do 11 | assert_save bezos 12 | end 13 | end 14 | 15 | test "cannot save when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_save bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot save by other tenant" do 24 | within_a_request_of apple do 25 | refute_save bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot save by super admin (default)' do 30 | within_a_request_of super_admin do 31 | refute_save bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can save by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_save bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can save by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_save bezos 47 | end 48 | end 49 | end 50 | 51 | #### 52 | # #save! 53 | #### 54 | test "can save! by tenant" do 55 | within_a_request_of amazon do 56 | assert_save! bezos 57 | end 58 | end 59 | 60 | test "cannot save! when tenant is missing" do 61 | turn_on_full_protection do 62 | missing_tenant do 63 | refute_save! bezos, error: MultiTenantSupport::MissingTenantError 64 | end 65 | end 66 | end 67 | 68 | test "cannot save! by other tenant" do 69 | within_a_request_of apple do 70 | refute_save! bezos, error: MultiTenantSupport::InvalidTenantAccess 71 | end 72 | end 73 | 74 | test 'cannot save! by super admin (default)' do 75 | within_a_request_of super_admin do 76 | refute_save! bezos, error: MultiTenantSupport::MissingTenantError 77 | end 78 | end 79 | 80 | test 'can save! by super admin through manual set current tenant' do 81 | within_a_request_of super_admin do 82 | under_tenant amazon do 83 | assert_save! bezos 84 | end 85 | end 86 | end 87 | 88 | private 89 | 90 | def assert_save(bezos) 91 | bezos.name = "JEFF BEZOS" 92 | assert bezos.save 93 | assert_equal "JEFF BEZOS", bezos.name 94 | end 95 | 96 | def refute_save(bezos, error:) 97 | bezos.name = "JEFF BEZOS" 98 | 99 | assert_raise(error) { bezos.save } 100 | 101 | under_tenant amazon do 102 | assert_equal "Jeff Bezos", bezos.reload.name 103 | end 104 | end 105 | 106 | def assert_save!(user) 107 | user.name = "JEFF BEZOS" 108 | assert user.save! 109 | assert_equal "JEFF BEZOS", user.name 110 | end 111 | 112 | def refute_save!(user, error:) 113 | bezos.name = "JEFF BEZOS" 114 | 115 | assert_raise(error) { bezos.save! } 116 | 117 | under_tenant amazon do 118 | assert_equal "Jeff Bezos", bezos.reload.name 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/delete/destroy_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelDestroyProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # #destroy 8 | #### 9 | test "can destroy by tenant" do 10 | within_a_request_of amazon do 11 | assert_destroy bezos 12 | end 13 | end 14 | 15 | test "cannot destroy when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_destroy bezos, error: MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test "cannot destroy by other tenant" do 24 | within_a_request_of apple do 25 | refute_destroy bezos, error: MultiTenantSupport::InvalidTenantAccess 26 | end 27 | end 28 | 29 | test 'cannot destroy by super admin default' do 30 | within_a_request_of super_admin do 31 | refute_destroy bezos, error: MultiTenantSupport::MissingTenantError 32 | end 33 | end 34 | 35 | test 'can destroy by super admin through manual set current tenant' do 36 | within_a_request_of super_admin do 37 | under_tenant amazon do 38 | assert_destroy bezos 39 | end 40 | end 41 | end 42 | 43 | test 'can destroy by super admin through manual turn off protection' do 44 | within_a_request_of super_admin do 45 | turn_off_protection do 46 | assert_destroy bezos 47 | end 48 | end 49 | end 50 | 51 | #### 52 | # #destroy! 53 | #### 54 | test "can destroy! by tenant" do 55 | within_a_request_of amazon do 56 | assert_destroy! bezos 57 | end 58 | end 59 | 60 | test "cannot destroy! when tenant is missing" do 61 | turn_on_full_protection do 62 | missing_tenant do 63 | refute_destroy! bezos, error: MultiTenantSupport::MissingTenantError 64 | end 65 | end 66 | end 67 | 68 | test "cannot destroy! by other tenant" do 69 | within_a_request_of apple do 70 | refute_destroy! bezos, error: MultiTenantSupport::InvalidTenantAccess 71 | end 72 | end 73 | 74 | test 'cannot destroy! by super admin default' do 75 | within_a_request_of super_admin do 76 | refute_destroy! bezos, error: MultiTenantSupport::MissingTenantError 77 | end 78 | end 79 | 80 | test 'can destroy! by super admin through manual set current tenant' do 81 | within_a_request_of super_admin do 82 | under_tenant amazon do 83 | assert_destroy! bezos 84 | end 85 | end 86 | end 87 | 88 | test 'can destroy! by super admin through manual turn off protection' do 89 | within_a_request_of super_admin do 90 | turn_off_protection do 91 | assert_destroy! bezos 92 | end 93 | end 94 | end 95 | 96 | def assert_destroy(user) 97 | assert_difference "User.count", -1 do 98 | user.destroy 99 | end 100 | end 101 | 102 | def refute_destroy(user, error:) 103 | assert_raise(error) { user.destroy } 104 | 105 | under_tenant user.account do 106 | assert user.reload.persisted? 107 | end 108 | end 109 | 110 | def assert_destroy!(user) 111 | assert_difference "User.count", -1 do 112 | user.destroy! 113 | end 114 | end 115 | 116 | def refute_destroy!(user, error:) 117 | assert_raise(error) { user.destroy! } 118 | 119 | under_tenant user.account do 120 | assert user.reload.persisted? 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/create/create_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | class ModelCreateProtectTest < ActiveSupport::TestCase 5 | 6 | #### 7 | # .create 8 | #### 9 | test "can create by tenant" do 10 | within_a_request_of apple do 11 | assert_create 12 | end 13 | end 14 | 15 | test "cannot create when tenant is missing" do 16 | turn_on_full_protection do 17 | missing_tenant do 18 | refute_create MultiTenantSupport::MissingTenantError 19 | end 20 | end 21 | end 22 | 23 | test 'cannot create by super admin (default)' do 24 | within_a_request_of super_admin do 25 | refute_create MultiTenantSupport::MissingTenantError 26 | end 27 | end 28 | 29 | test 'can create by super admin through manual set current tenant' do 30 | within_a_request_of super_admin do 31 | under_tenant apple do 32 | assert_create 33 | end 34 | end 35 | end 36 | 37 | test 'cannot create without account by super admin through manual turn off protection' do 38 | within_a_request_of super_admin do 39 | turn_off_protection do 40 | assert_no_difference "User.unscope_tenant.count" do 41 | cook = User.create(name: 'Tim Cook', email: 'cook@example.com') 42 | assert_equal ["Account must exist"], cook.errors.full_messages 43 | end 44 | end 45 | end 46 | end 47 | 48 | #### 49 | # #create! 50 | #### 51 | test "can create! by tenant" do 52 | within_a_request_of apple do 53 | assert_create! 54 | end 55 | end 56 | 57 | test "cannot create! when tenant is missing" do 58 | turn_on_full_protection do 59 | missing_tenant do 60 | refute_create! MultiTenantSupport::MissingTenantError 61 | end 62 | end 63 | end 64 | 65 | test 'cannot create! by super admin (default)' do 66 | within_a_request_of super_admin do 67 | refute_create! MultiTenantSupport::MissingTenantError 68 | end 69 | end 70 | 71 | test 'can create! by super admin through manual set current tenant' do 72 | within_a_request_of super_admin do 73 | under_tenant apple do 74 | assert_create! 75 | end 76 | end 77 | end 78 | 79 | test 'cannot create! without account by super admin through manual turn off protection' do 80 | within_a_request_of super_admin do 81 | turn_off_protection do 82 | assert_no_difference "User.unscope_tenant.count" do 83 | assert_raise "Account must exist" do 84 | User.create!(name: 'Tim Cook', email: 'cook@example.com') 85 | end 86 | end 87 | end 88 | end 89 | end 90 | 91 | private 92 | 93 | def assert_create 94 | assert_difference "User.unscope_tenant.count", 1 do 95 | cook = User.create(name: 'Tim Cook', email: 'cook@example.com') 96 | assert_equal apple, cook.account 97 | end 98 | end 99 | 100 | def refute_create(error) 101 | assert_raise(error) do 102 | User.create(name: 'Tim Cook', email: 'cook@example.com') 103 | end 104 | 105 | as_super_admin do 106 | assert_equal 3, User.unscope_tenant.count 107 | end 108 | end 109 | 110 | def assert_create! 111 | assert_difference "User.unscope_tenant.count", 1 do 112 | cook = User.create!(name: 'Tim Cook', email: 'cook@example.com') 113 | assert_equal apple, cook.account 114 | end 115 | end 116 | 117 | def refute_create!(error) 118 | assert_raise(error) do 119 | User.create!(name: 'Tim Cook', email: 'cook@example.com') 120 | end 121 | 122 | as_super_admin do 123 | assert_equal 3, User.unscope_tenant.count 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/multi_tenant_support/concern/model_concern/belongs_to_tenant/update/upsert_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ModelUpsertProtectTest < ActiveSupport::TestCase 4 | 5 | #### 6 | # .upsert 7 | #### 8 | test "will not protect upsert when call under the tenant" do 9 | within_a_request_of amazon do 10 | assert_not_protect_upsert with_warn: "[WARNING] You are using upsert_all(or upsert) which may update records across tenants\n" 11 | end 12 | end 13 | 14 | test "fail to upsert when tenant is missing" do 15 | turn_on_full_protection do 16 | missing_tenant do 17 | refute_upsert MultiTenantSupport::MissingTenantError, with_warn: "[WARNING] You are using upsert_all(or upsert) which may update records across tenants\n" 18 | end 19 | end 20 | end 21 | 22 | test 'will not protect upsert when call by super admin' do 23 | within_a_request_of super_admin do 24 | assert_not_protect_upsert with_warn: "[WARNING] You are using upsert_all(or upsert) which may update records across tenants\n" 25 | end 26 | end 27 | 28 | test 'will not protect upsert when call by super admin even manual set current tenant' do 29 | within_a_request_of super_admin do 30 | under_tenant amazon do 31 | assert_not_protect_upsert with_warn: "[WARNING] You are using upsert_all(or upsert) which may update records across tenants\n" 32 | end 33 | end 34 | end 35 | 36 | test 'will not get warn on call upsert when turn off protection' do 37 | within_a_request_of super_admin do 38 | turn_off_protection do 39 | assert_not_protect_upsert with_warn: nil 40 | end 41 | end 42 | end 43 | 44 | private 45 | 46 | def assert_not_protect_upsert(with_warn:) 47 | as_super_admin do 48 | assert_equal 3, User.unscope_tenant.count 49 | end 50 | 51 | _, warn_message = capture_io do 52 | User.upsert({name: 'JEFF BEZOS', email: 'bezos@example.com', created_at: Time.current, updated_at: Time.current}, unique_by: :email) 53 | User.upsert({name: 'MARK ZUCKERBERG', email: 'zuck@example.com', created_at: Time.current, updated_at: Time.current}, unique_by: :email) 54 | User.upsert({name: 'New User', email: 'new.user@example.com', created_at: Time.current, updated_at: Time.current}, unique_by: :email) 55 | end 56 | 57 | if with_warn 58 | assert_includes warn_message, with_warn 59 | else 60 | assert_no_match /WARNING/, warn_message 61 | end 62 | 63 | as_super_admin do 64 | without_current_tenant do 65 | assert_equal 4, User.unscope_tenant.count 66 | assert_equal 'JEFF BEZOS', bezos.reload.name 67 | assert_equal 'MARK ZUCKERBERG', zuck.reload.name 68 | assert_equal 'New User', User.find_by(email: 'new.user@example.com').name 69 | end 70 | end 71 | end 72 | 73 | def refute_upsert(error, with_warn:) 74 | as_super_admin do 75 | assert_equal 3, User.unscope_tenant.count 76 | end 77 | 78 | _, warn_message = capture_io do 79 | assert_raise error do 80 | User.upsert({name: 'JEFF BEZOS', email: 'bezos@example.com', created_at: Time.current, updated_at: Time.current}, unique_by: :email) 81 | User.upsert({name: 'MARK ZUCKERBERG', email: 'zuck@example.com', created_at: Time.current, updated_at: Time.current}, unique_by: :email) 82 | User.upsert({name: 'New User', email: 'new.user@example.com', created_at: Time.current, updated_at: Time.current}, unique_by: :email) 83 | end 84 | end 85 | 86 | assert_includes warn_message, with_warn 87 | 88 | as_super_admin do 89 | without_current_tenant do 90 | assert_equal 3, User.unscope_tenant.count 91 | assert_equal 'Jeff Bezos', bezos.reload.name 92 | assert_equal 'Mark Zuckerberg', zuck.reload.name 93 | end 94 | end 95 | end 96 | 97 | end 98 | --------------------------------------------------------------------------------