is_multitenant 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2462d924c58beb5fd2f45a4f83787b650e493d89
4
+ data.tar.gz: b7a67bf5f21d258ce07d95b52880be39798c46c0
5
+ SHA512:
6
+ metadata.gz: e8930b41dd1b3150af8d19fa700b5eb3772a7f34ba4c5cbd249154ff123c1084a20212f036db3fd389c41bda92777067bcae49692e583a04abd62bbcefea4bce
7
+ data.tar.gz: de0efb6513e84bb7c3313172fd27b7a2ee0b518e950b2cd67f9f99787c0007724bdbac43ec6302fbe86d82dcf926b85dc537547c372cd7b7dc3719111d783c42
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "activerecord", "~>3.2"
4
+ gem "activesupport", "~>3.2"
5
+
6
+ gem 'scope_injector', '~>0'
7
+
8
+
9
+ # Add dependencies to develop your gem here.
10
+ # Include everything needed to run rake, tests, features, etc.
11
+ group :development do
12
+ gem "rspec", "~> 2.14"
13
+ gem "rdoc", "~> 3.12"
14
+ gem "bundler", "~> 1"
15
+ gem "jeweler", "~> 2.0"
16
+ gem "simplecov", "~> 0"
17
+ gem "sqlite3", "~> 1"
18
+ gem 'database_cleaner', '~>1'
19
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,96 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.2.17)
5
+ activesupport (= 3.2.17)
6
+ builder (~> 3.0.0)
7
+ activerecord (3.2.17)
8
+ activemodel (= 3.2.17)
9
+ activesupport (= 3.2.17)
10
+ arel (~> 3.0.2)
11
+ tzinfo (~> 0.3.29)
12
+ activesupport (3.2.17)
13
+ i18n (~> 0.6, >= 0.6.4)
14
+ multi_json (~> 1.0)
15
+ addressable (2.3.5)
16
+ arel (3.0.3)
17
+ builder (3.0.4)
18
+ database_cleaner (1.2.0)
19
+ descendants_tracker (0.0.3)
20
+ diff-lcs (1.2.5)
21
+ docile (1.1.3)
22
+ faraday (0.9.0)
23
+ multipart-post (>= 1.2, < 3)
24
+ git (1.2.6)
25
+ github_api (0.11.3)
26
+ addressable (~> 2.3)
27
+ descendants_tracker (~> 0.0.1)
28
+ faraday (~> 0.8, < 0.10)
29
+ hashie (>= 1.2)
30
+ multi_json (>= 1.7.5, < 2.0)
31
+ nokogiri (~> 1.6.0)
32
+ oauth2
33
+ hashie (2.0.5)
34
+ highline (1.6.21)
35
+ i18n (0.6.9)
36
+ jeweler (2.0.1)
37
+ builder
38
+ bundler (>= 1.0)
39
+ git (>= 1.2.5)
40
+ github_api
41
+ highline (>= 1.6.15)
42
+ nokogiri (>= 1.5.10)
43
+ rake
44
+ rdoc
45
+ json (1.8.1)
46
+ jwt (0.1.11)
47
+ multi_json (>= 1.5)
48
+ mini_portile (0.5.2)
49
+ multi_json (1.9.0)
50
+ multi_xml (0.5.5)
51
+ multipart-post (2.0.0)
52
+ nokogiri (1.6.1)
53
+ mini_portile (~> 0.5.0)
54
+ oauth2 (0.9.3)
55
+ faraday (>= 0.8, < 0.10)
56
+ jwt (~> 0.1.8)
57
+ multi_json (~> 1.3)
58
+ multi_xml (~> 0.5)
59
+ rack (~> 1.2)
60
+ rack (1.5.2)
61
+ rake (10.1.1)
62
+ rdoc (3.12.2)
63
+ json (~> 1.4)
64
+ rspec (2.14.1)
65
+ rspec-core (~> 2.14.0)
66
+ rspec-expectations (~> 2.14.0)
67
+ rspec-mocks (~> 2.14.0)
68
+ rspec-core (2.14.7)
69
+ rspec-expectations (2.14.5)
70
+ diff-lcs (>= 1.1.3, < 2.0)
71
+ rspec-mocks (2.14.6)
72
+ scope_injector (0.3.1)
73
+ activerecord (~> 3.2)
74
+ activesupport (~> 3.2)
75
+ simplecov (0.8.2)
76
+ docile (~> 1.1.0)
77
+ multi_json
78
+ simplecov-html (~> 0.8.0)
79
+ simplecov-html (0.8.0)
80
+ sqlite3 (1.3.9)
81
+ tzinfo (0.3.39)
82
+
83
+ PLATFORMS
84
+ ruby
85
+
86
+ DEPENDENCIES
87
+ activerecord (~> 3.2)
88
+ activesupport (~> 3.2)
89
+ bundler (~> 1)
90
+ database_cleaner (~> 1)
91
+ jeweler (~> 2.0)
92
+ rdoc (~> 3.12)
93
+ rspec (~> 2.14)
94
+ scope_injector (~> 0)
95
+ simplecov (~> 0)
96
+ sqlite3 (~> 1)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Tom Smith
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,20 @@
1
+ = is_multitenant
2
+
3
+ <Description goes here...>
4
+
5
+
6
+ == Contributing to is_multitenant
7
+
8
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
9
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
10
+ * Fork the project.
11
+ * Start a feature/bugfix branch.
12
+ * Commit and push until you are happy with your contribution.
13
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
14
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2014 Tom Smith. See LICENSE.txt for
19
+ further details.
20
+
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "is_multitenant"
18
+ gem.homepage = "http://github.com/rtomsmith/is_multitenant"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Rails ActiveRecord extension for enforcing multi-tenancy in a shared database}
21
+ gem.description = %Q{Rails ActiveRecord extension for enforcing multi-tenancy in a shared database. Does NOT use default_scope, instead reyling on the scope_injector gem.}
22
+ gem.email = "tsmith@landfall.com"
23
+ gem.authors = ["Tom Smith"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ desc "Code coverage detail"
35
+ task :simplecov do
36
+ ENV['COVERAGE'] = "true"
37
+ Rake::Task['spec'].execute
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'rdoc/task'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "is_multitenant #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -0,0 +1,82 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: is_multitenant 0.5.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "is_multitenant"
9
+ s.version = "0.5.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Tom Smith"]
14
+ s.date = "2014-03-11"
15
+ s.description = "Rails ActiveRecord extension for enforcing multi-tenancy in a shared database. Does NOT use default_scope, instead reyling on the scope_injector gem."
16
+ s.email = "tsmith@landfall.com"
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ "Gemfile",
25
+ "Gemfile.lock",
26
+ "LICENSE.txt",
27
+ "README.rdoc",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "is_multitenant.gemspec",
31
+ "lib/is_multitenant.rb",
32
+ "spec/database.yml",
33
+ "spec/is_multitenant_spec.rb",
34
+ "spec/spec_helper.rb",
35
+ "spec/support/models.rb",
36
+ "spec/support/schema.rb"
37
+ ]
38
+ s.homepage = "http://github.com/rtomsmith/is_multitenant"
39
+ s.licenses = ["MIT"]
40
+ s.rubygems_version = "2.2.2"
41
+ s.summary = "Rails ActiveRecord extension for enforcing multi-tenancy in a shared database"
42
+
43
+ if s.respond_to? :specification_version then
44
+ s.specification_version = 4
45
+
46
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<activerecord>, ["~> 3.2"])
48
+ s.add_runtime_dependency(%q<activesupport>, ["~> 3.2"])
49
+ s.add_runtime_dependency(%q<scope_injector>, ["~> 0"])
50
+ s.add_development_dependency(%q<rspec>, ["~> 2.14"])
51
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
52
+ s.add_development_dependency(%q<bundler>, ["~> 1"])
53
+ s.add_development_dependency(%q<jeweler>, ["~> 2.0"])
54
+ s.add_development_dependency(%q<simplecov>, ["~> 0"])
55
+ s.add_development_dependency(%q<sqlite3>, ["~> 1"])
56
+ s.add_development_dependency(%q<database_cleaner>, ["~> 1"])
57
+ else
58
+ s.add_dependency(%q<activerecord>, ["~> 3.2"])
59
+ s.add_dependency(%q<activesupport>, ["~> 3.2"])
60
+ s.add_dependency(%q<scope_injector>, ["~> 0"])
61
+ s.add_dependency(%q<rspec>, ["~> 2.14"])
62
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
63
+ s.add_dependency(%q<bundler>, ["~> 1"])
64
+ s.add_dependency(%q<jeweler>, ["~> 2.0"])
65
+ s.add_dependency(%q<simplecov>, ["~> 0"])
66
+ s.add_dependency(%q<sqlite3>, ["~> 1"])
67
+ s.add_dependency(%q<database_cleaner>, ["~> 1"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<activerecord>, ["~> 3.2"])
71
+ s.add_dependency(%q<activesupport>, ["~> 3.2"])
72
+ s.add_dependency(%q<scope_injector>, ["~> 0"])
73
+ s.add_dependency(%q<rspec>, ["~> 2.14"])
74
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
75
+ s.add_dependency(%q<bundler>, ["~> 1"])
76
+ s.add_dependency(%q<jeweler>, ["~> 2.0"])
77
+ s.add_dependency(%q<simplecov>, ["~> 0"])
78
+ s.add_dependency(%q<sqlite3>, ["~> 1"])
79
+ s.add_dependency(%q<database_cleaner>, ["~> 1"])
80
+ end
81
+ end
82
+
@@ -0,0 +1,168 @@
1
+ require 'scope_injector'
2
+
3
+ module Multitenant
4
+
5
+ # Use this method to set the current tenant id. Typically this would be called from
6
+ # a controller. For example:
7
+ #
8
+ # class ApplicationController < ActionController::Base
9
+ # before_filter :set_the_current_tenant
10
+ #
11
+ # def set_the_current_tenant
12
+ # Multitenant.current_tenant_id = current_user.account_id
13
+ # end
14
+ # end
15
+ #
16
+ def self.current_tenant_id=(tenant_id)
17
+ Thread.current[:current_tenant_id] = tenant_id
18
+ end
19
+
20
+ # Returns the currently set tenant id. Used by models to scope operations.
21
+ def self.current_tenant_id
22
+ Thread.current[:current_tenant_id] ||= (defined?(Rails::Console) ? Account.first.id : nil)
23
+ end
24
+
25
+ # Sometimes you need to execute code in the scope of a tenant is different
26
+ # than the current tenant. This can be useful for admin tasks in Rake, or
27
+ # in tests. For example:
28
+ #
29
+ # Multitenant.with_tenant_id(override_tenant_id) do
30
+ # ...do some AR stuff...
31
+ # end
32
+ #
33
+ # forces the specified tenant id to be used to scope all the ActiveRecord
34
+ # operations inside the block.
35
+ #
36
+ def self.with_tenant_id(tenant_id)
37
+ begin
38
+ old_tenant, self.current_tenant_id = self.current_tenant_id, tenant_id
39
+ yield
40
+ ensure
41
+ self.current_tenant_id = old_tenant
42
+ end
43
+ end
44
+
45
+ # Disables the multitenant scoping for all model operations within the block. The
46
+ # scoping is temporarily disabled for ALL model classes involved.
47
+ def self.without_multitenant_scope(&blk)
48
+ ScopeInjector.without_injected_scope(:multitenant, &blk)
49
+ end
50
+
51
+
52
+ # Add support to ActiveRecord::Base for the +is_mulitenant+ macro
53
+ module IsMultitenant
54
+
55
+ def self.included(base)
56
+ base.extend ClassMethods
57
+ end
58
+
59
+ module ClassMethods
60
+ # When invoked from an ActiveRecord model class, +is_multitenant+ specifies
61
+ # that the model's database operations always be scoped to the application
62
+ # request's current tenant. Usage is as follows:
63
+ #
64
+ # class Contact < ActiveRecord::Base
65
+ # is_multitenant :with_attribute => :account_id
66
+ # -OR-
67
+ # is_multitenant :class => :account
68
+ # -OR-
69
+ # is_multitenant :class => :account, :with_attribute => :account_key
70
+ # end
71
+ #
72
+ def is_multitenant(options = {})
73
+ raise 'Cannot use is_multitenant more than once per model class' if is_multitenant?
74
+
75
+ cattr_accessor :tenant_class_name
76
+ cattr_accessor :tenant_attribute
77
+
78
+ self.tenant_attribute = options[:with_attribute]
79
+ raise ArgumentError, "Must specify :class and/or :with_attribute" if options[:class].blank? && self.tenant_attribute.blank?
80
+ class_name = self.tenant_attribute.to_s.ends_with?('_id') ? self.tenant_attribute.to_s.sub('_id','').underscore.to_sym : nil
81
+ self.tenant_class_name = (options[:class] || class_name).to_s.classify
82
+ self.tenant_attribute = self.tenant_class_name.foreign_key.to_sym if self.tenant_attribute.nil?
83
+
84
+ extend IsMultitenant::SingletonMethods
85
+ include IsMultitenant::InstanceMethods
86
+
87
+ inject_scope :multitenant, :for_current_tenant, :apply_to => :all
88
+
89
+ scope :for_current_tenant, ->() { where(tenant_condition) }
90
+ scope :for_tenant, ->(tenant_id) { where(tenant_condition(tenant_id)) }
91
+
92
+ validate :associations_have_same_tenant
93
+
94
+ before_validation :force_current_tenant_id
95
+ before_save :force_current_tenant_id
96
+
97
+ define_tenant_id_writer(tenant_attribute)
98
+ end
99
+
100
+ def is_multitenant?
101
+ false
102
+ end
103
+ end
104
+
105
+ # These methods are available as class level methods to the models that
106
+ # invoke is_multitenant, and only to those models.
107
+ module SingletonMethods
108
+
109
+ public
110
+
111
+ def is_multitenant?
112
+ true
113
+ end
114
+
115
+ def tenant_condition(tenant_id = nil)
116
+ raise 'ERROR: the current tenant id has not been set' unless tenant_id || current_tenant_id
117
+ {self.tenant_attribute => tenant_id || current_tenant_id}
118
+ end
119
+
120
+ def current_tenant_id
121
+ Multitenant.current_tenant_id
122
+ end
123
+
124
+ private
125
+
126
+ def define_tenant_id_writer(tenant_attribute)
127
+ define_method("#{tenant_attribute}=") do |value|
128
+ if self.class.without_multitenant_scope? || new_record? || send(tenant_attribute).nil?
129
+ write_attribute(:"#{tenant_attribute}", value)
130
+ else
131
+ raise "Unauthorized assignment to :#{tenant_attribute}. This field is protected by is_multitenant and is set automatically."
132
+ end
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ # All instances of is_multitenant models have access to the following
139
+ # methods.
140
+ module InstanceMethods
141
+ def self.included(base)
142
+
143
+ protected
144
+
145
+ def set_tenant_id(tenant_id = nil)
146
+ write_attribute(self.class.tenant_attribute, tenant_id || self.class.current_tenant_id)
147
+ end
148
+
149
+ def force_current_tenant_id
150
+ set_tenant_id unless self.class.without_multitenant_scope?
151
+ end
152
+
153
+ def associations_have_same_tenant
154
+ self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
155
+ if assoc.klass.is_multitenant? && assoc.class_name != self.tenant_class_name
156
+ value = send(assoc.foreign_key)
157
+ errors.add assoc.foreign_key, "multitenant association #{assoc.name} has different tenant" unless value.nil? || assoc.klass.where(:id => value).exists?
158
+ end
159
+ end
160
+ end
161
+
162
+ end
163
+ end
164
+
165
+ end
166
+ end
167
+
168
+ ActiveRecord::Base.send :include, Multitenant::IsMultitenant
data/spec/database.yml ADDED
@@ -0,0 +1,17 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ':memory:'
4
+
5
+ mysql:
6
+ adapter: mysql
7
+ hostname: localhost
8
+ username: root
9
+ password:
10
+ database: delineate_test
11
+
12
+ postgresql:
13
+ adapter: postgresql
14
+ hostname: localhost
15
+ username: postgres
16
+ password:
17
+ database: delineate_test
@@ -0,0 +1,253 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Multitenant do
4
+ before { Multitenant.current_tenant_id = 1 }
5
+ after { Multitenant.current_tenant_id = nil }
6
+
7
+ # Setting and getting
8
+ describe 'setting the current tenant id' do
9
+ before { Multitenant.current_tenant_id = 9876 }
10
+ it { expect(Multitenant.current_tenant_id).to eq(9876) }
11
+ end
12
+
13
+ describe 'models should report support for multi-tenancy' do
14
+ it {Project.is_multitenant?.should be_true}
15
+ it {Project.respond_to?(:inject_scope_multitenant?).should be_true}
16
+ it {UnscopedModel.is_multitenant?.should_not be_true}
17
+ end
18
+
19
+ describe "tenant required" do
20
+ describe "raises exception if no tenant specified" do
21
+ before do
22
+ @account1 = Account.create!(:name => 'foo')
23
+ Multitenant.current_tenant_id = @account1.id
24
+ @project1 = @account1.projects.create!(:name => 'foobar')
25
+ end
26
+
27
+ it "should raise an error when no tenant is provided" do
28
+ Multitenant.current_tenant_id = nil
29
+ expect { Project.all.to_a }.to raise_error(RuntimeError, /current tenant/)
30
+ end
31
+ end
32
+ end
33
+
34
+ describe 'tenant_id should be immutable, if already set' do
35
+ before do
36
+ @account = Account.create!(:name => 'Test account')
37
+ Multitenant.current_tenant_id = @account.id
38
+ @project = @account.projects.create!(:name => 'Test project')
39
+ end
40
+
41
+ it { running {@project.account_id = @account.id + 1}.should raise_error }
42
+ end
43
+
44
+ describe 'tenant_id should be mutable, if not already set' do
45
+ before do
46
+ @account = Account.create!(:name => 'foo')
47
+ Multitenant.current_tenant_id = @account.id
48
+ @project = Project.create!(:name => 'bar')
49
+ @project.update_column('account_id', nil)
50
+ end
51
+
52
+ it { @project.account_id.should be_nil }
53
+ it { running { @project.account_id = @account.id }.should_not raise_error }
54
+ end
55
+
56
+ describe 'allows custom foreign_key on is_multitenant' do
57
+ before do
58
+ @account = Account.create!(:name => 'foo')
59
+ Multitenant.current_tenant_id = @account.id
60
+ @custom_foreign_key_task = CustomForeignKeyTask.create!(:name => 'foo')
61
+ end
62
+
63
+ it { @custom_foreign_key_task.accountID.should == @account.id }
64
+ end
65
+
66
+ # Scoping models
67
+ describe 'Project.all should be scoped to the current tenant if set' do
68
+ before do
69
+ @account1 = Account.create!(:name => 'foo')
70
+ @account2 = Account.create!(:name => 'bar')
71
+
72
+ Multitenant.current_tenant_id = @account1.id
73
+ @project1 = @account1.projects.create!(:name => 'foobar')
74
+ Multitenant.current_tenant_id = @account2.id
75
+ @project2 = @account2.projects.create!(:name => 'baz')
76
+
77
+ Multitenant.current_tenant_id = @account1.id
78
+ @projects = Project.all
79
+ end
80
+
81
+ it { @projects.length.should == 1 }
82
+ it { @projects.should == [@project1] }
83
+ end
84
+
85
+ describe 'unscoping operations should work' do
86
+ before do
87
+ @account1 = Account.create!(:name => 'foo')
88
+ @account2 = Account.create!(:name => 'bar')
89
+
90
+ Multitenant.current_tenant_id = @account1.id
91
+ @project1 = @account1.projects.create!(:name => 'foobar')
92
+ Multitenant.current_tenant_id = @account2.id
93
+ @project2 = @account2.projects.create!(:name => 'baz')
94
+
95
+ Multitenant.current_tenant_id = @account1.id
96
+ @projects_unscoped = Project.unscoped.all
97
+ @projects_without_multitenant_scope = Project.without_multitenant_scope{Project.all}
98
+ end
99
+
100
+ it { @projects_unscoped.count.should == 1 } # unscoping default_scope should have no affect
101
+ it { @projects_without_multitenant_scope.count.should == 2 }
102
+ end
103
+
104
+ describe 'Associations should be correctly scoped by current tenant' do
105
+ before do
106
+ @account = Account.create!(:name => 'foo')
107
+ Multitenant.current_tenant_id = @account.id
108
+ @project = Project.create!(:name => 'foobar', :account_id => @account.id )
109
+
110
+ @task1 = Task.create!(:name => 'no_tenant', :project => @project)
111
+ @task1.update_column('account_id', nil)
112
+
113
+ @task2 = @project.tasks.create!(:name => 'baz')
114
+ @tasks = @project.tasks
115
+ end
116
+
117
+ it 'should correctly set the tenant on the task created with current_tenant set' do
118
+ @task2.account_id.should == @account.id
119
+ end
120
+
121
+ it 'should filter out the non-tenant task from the project' do
122
+ @tasks.length.should == 1
123
+ end
124
+ end
125
+
126
+ describe 'Associations can only be made with in-scope objects' do
127
+ before do
128
+ @account = Account.create!(:name => 'foo')
129
+ Multitenant.current_tenant_id = @account.id+1
130
+ @project1 = Project.create!(:name => 'inaccessible_project')
131
+
132
+ Multitenant.current_tenant_id = @account.id
133
+ @project2 = Project.create!(:name => 'accessible_project')
134
+ @task = @project2.tasks.create!(:name => 'bar')
135
+ end
136
+
137
+ it do
138
+ @task.update_attributes(:project_id => @project1.id).should be_false
139
+ end
140
+
141
+ end
142
+
143
+ describe 'Create and save a multitenant child without it having a parent' do
144
+ @account = Account.create!(:name => 'baz')
145
+ Multitenant.current_tenant_id = @account.id
146
+
147
+ it {Task.create(:name => 'bar').valid?.should == true}
148
+ end
149
+
150
+ describe 'It should be possible to use aliased associations' do
151
+ it { AliasedTask.create(:name => 'foo', :project_alias => @project2).valid?.should == true }
152
+ end
153
+
154
+ # Additional default_scopes
155
+ describe 'When dealing with a user defined default_scope' do
156
+ before do
157
+ @account = Account.create!(:name => 'foo')
158
+ Multitenant.current_tenant_id = @account.id+1
159
+ @project1 = Project.create!(:name => 'inaccessible')
160
+ @task1 = Task.create!(:name => 'no_tenant', :project => @project1)
161
+
162
+ Multitenant.current_tenant_id = @account.id
163
+ @project2 = Project.create!(:name => 'accessible')
164
+ @task2 = @project2.tasks.create!(:name => 'bar')
165
+ @task3 = @project2.tasks.create!(:name => 'baz')
166
+ @task4 = @project2.tasks.create!(:name => 'foo')
167
+ @task5 = @project2.tasks.create!(:name => 'foobar', :completed => true )
168
+
169
+ @tasks = Task.all
170
+ end
171
+
172
+ it 'should apply both the tenant scope and the user defined default_scope, including :order' do
173
+ @tasks.length.should == 3
174
+ @tasks.should == [@task2, @task3, @task4]
175
+ @tasks = Task.unscoped.all
176
+ @tasks.length.should == 4
177
+ end
178
+ end
179
+
180
+ # Validates_uniqueness
181
+ describe 'When using validates_uniqueness_of in a multitenant model' do
182
+ before do
183
+ account = Account.create!(:name => 'foo')
184
+ Multitenant.current_tenant_id = account.id
185
+ Project.create!(:name => 'existing_name')
186
+ end
187
+
188
+ it 'should not be possible to create a duplicate within the same tenant' do
189
+ Project.create(:name => 'existing_name').valid?.should == false
190
+ end
191
+
192
+ it 'should be possible to create a duplicate outside the tenant scope' do
193
+ account = Account.create!(:name => 'baz')
194
+ Multitenant.current_tenant_id = account.id
195
+ Project.create(:name => 'existing_name').valid?.should == true
196
+ end
197
+ end
198
+
199
+ describe 'Handles user defined scopes' do
200
+ before do
201
+ UniqueTask.create!(:name => 'foo', :user_defined_scope => 'unique_scope')
202
+ end
203
+
204
+ it { UniqueTask.create(:name => 'foo', :user_defined_scope => 'another_scope').should be_valid }
205
+ it { UniqueTask.create(:name => 'foo', :user_defined_scope => 'unique_scope').should_not be_valid }
206
+ end
207
+
208
+ describe 'When using validates_uniqueness_of in a NON-aat model' do
209
+ before do
210
+ UnscopedModel.create!(:name => 'foo')
211
+ end
212
+ it 'should not be possible to create duplicates' do
213
+ UnscopedModel.create(:name => 'foo').valid?.should == false
214
+ end
215
+ end
216
+
217
+ # with_tenant_id
218
+ describe "Multitenant.with_tenant_id" do
219
+ it "should set current_tenant to the specified tenant inside the block" do
220
+ @account = Account.create!(:name => 'baz')
221
+
222
+ Multitenant.with_tenant_id(@account.id) do
223
+ Multitenant.current_tenant_id.should eq(@account.id)
224
+ end
225
+ end
226
+
227
+ it "should reset current_tenant to the previous tenant once exiting the block" do
228
+ @account1 = Account.create!(:name => 'foo')
229
+ @account2 = Account.create!(:name => 'bar')
230
+
231
+ Multitenant.current_tenant_id = @account1.id
232
+ Multitenant.with_tenant_id @account2.id do
233
+ end
234
+
235
+ Multitenant.current_tenant_id.should eq(@account1.id)
236
+ end
237
+
238
+ it "should return the value of the block" do
239
+ @account1 = Account.create!(:name => 'foo')
240
+ @account2 = Account.create!(:name => 'bar')
241
+
242
+ Multitenant.current_tenant_id = @account1.id
243
+ Multitenant.with_tenant_id(@account2.id) do
244
+ :foo
245
+ end.should eq :foo
246
+ end
247
+
248
+ it "should raise an error when no block is provided" do
249
+ expect { Multitenant.with_tenant_id(1) }.to raise_error(LocalJumpError, /no block given/)
250
+ end
251
+ end
252
+
253
+ end
@@ -0,0 +1,79 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'rubygems'
5
+
6
+ #require 'simplecov'
7
+ #SimpleCov.start
8
+
9
+ ENV['RAILS_ENV'] = 'test'
10
+
11
+ require 'active_support'
12
+ require 'active_support/core_ext/logger'
13
+ require 'active_record'
14
+
15
+ require 'rspec'
16
+ require 'database_cleaner'
17
+
18
+ require 'is_multitenant'
19
+
20
+
21
+ # Requires supporting files with custom matchers and macros, etc,
22
+ # in ./support/ and its subdirectories.
23
+ #Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
24
+
25
+ RSpec.configure do |config|
26
+ config.before(:suite) do
27
+ DatabaseCleaner.strategy = :transaction
28
+ DatabaseCleaner.clean_with(:truncation)
29
+ end
30
+
31
+ config.before(:each) do
32
+ DatabaseCleaner.start
33
+ end
34
+
35
+ config.after(:each) do
36
+ DatabaseCleaner.clean
37
+ end
38
+
39
+ # Run specs in random order to surface order dependencies. If you find an
40
+ # order dependency and want to debug it, you can fix the order by providing
41
+ # the seed, which is printed after each run.
42
+ # --seed 1234
43
+ config.order = "random"
44
+
45
+ # Log entry each test to aid in debugging
46
+ config.before(:each) do |x|
47
+ full_example_description = "#{x.example.metadata[:full_description]}"
48
+ ActiveRecord::Base.logger.info("\n#{full_example_description}\n#{'-' * (full_example_description.length)}")
49
+ end
50
+ end
51
+
52
+ # a little sugar for checking results of a section of code
53
+ alias :running :lambda
54
+
55
+
56
+ # Set the DB var if testing with other than sqlite
57
+ ENV['DB'] ||= 'sqlite3'
58
+
59
+ database_yml = File.expand_path('../database.yml', __FILE__)
60
+ if File.exists?(database_yml)
61
+ active_record_configuration = YAML.load_file(database_yml)[ENV['DB']]
62
+
63
+ ActiveRecord::Base.establish_connection(active_record_configuration)
64
+
65
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "test.log"))
66
+ #ActiveRecord::Base.logger = Logger.new(STDOUT)
67
+
68
+ ActiveRecord::Base.silence do
69
+ ActiveRecord::Migration.verbose = false
70
+
71
+ load(File.dirname(__FILE__) + '/support/schema.rb')
72
+ create_schema!
73
+
74
+ load(File.dirname(__FILE__) + '/support/models.rb')
75
+ clean_database!
76
+ end
77
+ else
78
+ raise "Create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample"
79
+ end
@@ -0,0 +1,50 @@
1
+
2
+ class Account < ActiveRecord::Base
3
+ has_many :projects
4
+ end
5
+
6
+ class Project < ActiveRecord::Base
7
+ is_multitenant :with_attribute => :account_id
8
+
9
+ has_one :manager
10
+ has_many :tasks
11
+
12
+ validates_uniqueness_of :name
13
+ end
14
+
15
+ class Manager < ActiveRecord::Base
16
+ is_multitenant :with_attribute => :account_id
17
+ belongs_to :project
18
+ end
19
+
20
+ class Task < ActiveRecord::Base
21
+ is_multitenant :with_attribute => :account_id
22
+
23
+ belongs_to :project
24
+ default_scope -> { where(:completed => nil).order("name") }
25
+
26
+ validates_uniqueness_of :name
27
+ end
28
+
29
+ class UnscopedModel < ActiveRecord::Base
30
+ validates_uniqueness_of :name
31
+ end
32
+
33
+ class AliasedTask < ActiveRecord::Base
34
+ is_multitenant :with_attribute => :account_id
35
+
36
+ belongs_to :project_alias, :class_name => "Project"
37
+ end
38
+
39
+ class UniqueTask < ActiveRecord::Base
40
+ is_multitenant :with_attribute => :account_id
41
+
42
+ belongs_to :project
43
+ validates_uniqueness_of :name, scope: :user_defined_scope
44
+ end
45
+
46
+ class CustomForeignKeyTask < ActiveRecord::Base
47
+ is_multitenant :with_attribute => :accountID
48
+ validates_uniqueness_of :name
49
+ end
50
+
@@ -0,0 +1,58 @@
1
+ # Support for creating the test schema and seed data
2
+
3
+ def create_schema!
4
+ ActiveRecord::Schema.define(:version => 1) do
5
+ create_table :accounts, :force => true do |t|
6
+ t.column :name, :string
7
+ t.column :subdomain, :string
8
+ end
9
+
10
+ create_table :projects, :force => true do |t|
11
+ t.column :name, :string
12
+ t.column :account_id, :integer
13
+ end
14
+
15
+ create_table :managers, :force => true do |t|
16
+ t.column :name, :string
17
+ t.column :project_id, :integer
18
+ t.column :account_id, :integer
19
+ end
20
+
21
+ create_table :tasks, :force => true do |t|
22
+ t.column :name, :string
23
+ t.column :account_id, :integer
24
+ t.column :project_id, :integer
25
+ t.column :completed, :boolean
26
+ end
27
+
28
+ create_table :countries, :force => true do |t|
29
+ t.column :name, :string
30
+ end
31
+
32
+ create_table :unscoped_models, :force => true do |t|
33
+ t.column :name, :string
34
+ end
35
+
36
+ create_table :aliased_tasks, :force => true do |t|
37
+ t.column :name, :string
38
+ t.column :project_alias_id, :integer
39
+ t.column :account_id, :integer
40
+ end
41
+
42
+ create_table :unique_tasks, :force => true do |t|
43
+ t.column :name, :string
44
+ t.column :user_defined_scope, :string
45
+ t.column :project_id, :integer
46
+ t.column :account_id, :integer
47
+ end
48
+
49
+ create_table :custom_foreign_key_tasks, :force => true do |t|
50
+ t.column :name, :string
51
+ t.column :accountID, :integer
52
+ end
53
+ end
54
+ end
55
+
56
+ def clean_database!
57
+ end
58
+
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: is_multitenant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ version_requirements: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ~>
17
+ - !ruby/object:Gem::Version
18
+ version: '3.2'
19
+ prerelease: false
20
+ name: activerecord
21
+ requirement: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ~>
24
+ - !ruby/object:Gem::Version
25
+ version: '3.2'
26
+ type: :runtime
27
+ - !ruby/object:Gem::Dependency
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '3.2'
33
+ prerelease: false
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: '3.2'
40
+ type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ prerelease: false
48
+ name: scope_injector
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ - !ruby/object:Gem::Dependency
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '2.14'
61
+ prerelease: false
62
+ name: rspec
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: '2.14'
68
+ type: :development
69
+ - !ruby/object:Gem::Dependency
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ~>
73
+ - !ruby/object:Gem::Version
74
+ version: '3.12'
75
+ prerelease: false
76
+ name: rdoc
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ~>
80
+ - !ruby/object:Gem::Version
81
+ version: '3.12'
82
+ type: :development
83
+ - !ruby/object:Gem::Dependency
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ~>
87
+ - !ruby/object:Gem::Version
88
+ version: '1'
89
+ prerelease: false
90
+ name: bundler
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ~>
94
+ - !ruby/object:Gem::Version
95
+ version: '1'
96
+ type: :development
97
+ - !ruby/object:Gem::Dependency
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ~>
101
+ - !ruby/object:Gem::Version
102
+ version: '2.0'
103
+ prerelease: false
104
+ name: jeweler
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '2.0'
110
+ type: :development
111
+ - !ruby/object:Gem::Dependency
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ~>
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ prerelease: false
118
+ name: simplecov
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ~>
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ - !ruby/object:Gem::Dependency
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ~>
129
+ - !ruby/object:Gem::Version
130
+ version: '1'
131
+ prerelease: false
132
+ name: sqlite3
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ~>
136
+ - !ruby/object:Gem::Version
137
+ version: '1'
138
+ type: :development
139
+ - !ruby/object:Gem::Dependency
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ~>
143
+ - !ruby/object:Gem::Version
144
+ version: '1'
145
+ prerelease: false
146
+ name: database_cleaner
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ~>
150
+ - !ruby/object:Gem::Version
151
+ version: '1'
152
+ type: :development
153
+ description: Rails ActiveRecord extension for enforcing multi-tenancy in a shared
154
+ database. Does NOT use default_scope, instead reyling on the scope_injector gem.
155
+ email: tsmith@landfall.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files:
159
+ - LICENSE.txt
160
+ - README.rdoc
161
+ files:
162
+ - .document
163
+ - .rspec
164
+ - Gemfile
165
+ - Gemfile.lock
166
+ - LICENSE.txt
167
+ - README.rdoc
168
+ - Rakefile
169
+ - VERSION
170
+ - is_multitenant.gemspec
171
+ - lib/is_multitenant.rb
172
+ - spec/database.yml
173
+ - spec/is_multitenant_spec.rb
174
+ - spec/spec_helper.rb
175
+ - spec/support/models.rb
176
+ - spec/support/schema.rb
177
+ homepage: http://github.com/rtomsmith/is_multitenant
178
+ licenses:
179
+ - MIT
180
+ metadata: {}
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - '>='
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 2.2.2
198
+ signing_key:
199
+ specification_version: 4
200
+ summary: Rails ActiveRecord extension for enforcing multi-tenancy in a shared database
201
+ test_files: []