├── 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 |
--------------------------------------------------------------------------------